code-sandboxes 1.8.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.
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.github/workflows/build.yml +1 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.github/workflows/py-tests.yml +1 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.pre-commit-config.yaml +5 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/CHANGELOG.md +27 -4
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/PKG-INFO +3 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/base.py +13 -2
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/client.py +8 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/commands.py +1 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/datalayer_sandbox.py +10 -10
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/docker_sandbox.py +2 -3
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/e2b_sandbox.py +10 -2
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/__init__.py +5 -0
- code_sandboxes-1.8.2/code_sandboxes/environments/accounts.py +143 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/adapters/datalayer.py +200 -67
- code_sandboxes-1.8.2/code_sandboxes/environments/adapters/daytona.py +90 -0
- code_sandboxes-1.8.2/code_sandboxes/environments/adapters/e2b.py +74 -0
- code_sandboxes-1.8.2/code_sandboxes/environments/adapters/managed.py +242 -0
- code_sandboxes-1.8.2/code_sandboxes/environments/adapters/modal.py +75 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/attest.py +18 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/bases.py +13 -2
- code_sandboxes-1.8.2/code_sandboxes/environments/breaker.py +294 -0
- code_sandboxes-1.8.2/code_sandboxes/environments/build_secrets.py +152 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/builders.py +15 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/conformance.py +94 -5
- code_sandboxes-1.8.2/code_sandboxes/environments/constraints/wheelhouse/README.md +40 -0
- code_sandboxes-1.8.2/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/doctor/datalayer_sandbox.py +3 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/errors.py +16 -0
- code_sandboxes-1.8.2/code_sandboxes/environments/image_import.py +289 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/resolve.py +330 -13
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/spec.py +260 -25
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/jupyter_ingress.py +1 -2
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/jupyter_server_sandbox.py +29 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/modal_sandbox.py +74 -22
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/environments/index.mdx +3 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/environments/specification.mdx +1 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/pyproject.toml +4 -2
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/schemas/environment-v1alpha1.json +75 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +1 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_bridge_mount.py +30 -18
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_client.py +25 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_commands_envelope.py +5 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_contents_live_matrix.py +8 -9
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_datalayer_sandbox.py +3 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_e2b.py +52 -1
- code_sandboxes-1.8.2/tests/test_environment_accounts.py +131 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_attest.py +28 -7
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_bases.py +15 -4
- code_sandboxes-1.8.2/tests/test_environment_breaker.py +251 -0
- code_sandboxes-1.8.2/tests/test_environment_build_secrets.py +217 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_builders.py +40 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_conformance.py +31 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_contract.py +3 -3
- code_sandboxes-1.8.2/tests/test_environment_cross_variant_report.py +153 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_datalayer_builder.py +285 -20
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_errors.py +2 -0
- code_sandboxes-1.8.2/tests/test_environment_image_import.py +202 -0
- code_sandboxes-1.8.2/tests/test_environment_managed_builders.py +350 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_redact.py +1 -3
- code_sandboxes-1.8.2/tests/test_environment_resolve.py +1068 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_spec.py +216 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_eval.py +10 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_jupyter_server.py +149 -0
- code_sandboxes-1.8.2/tests/test_modal_session.py +181 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_the_kill_relaunch_drill.py +9 -9
- code_sandboxes-1.8.0/tests/test_environment_resolve.py +0 -495
- code_sandboxes-1.8.0/tests/test_modal_session.py +0 -60
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.gitignore +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.licenserc.yaml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/.vscode/settings.json +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/Dockerfile +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/LICENSE +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/Makefile +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/README.md +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/google_colab_sandbox.py +1 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/kaggle_sandbox.py +1 -1
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docker/Dockerfile +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/.gitignore +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/LICENSE +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/Makefile +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/README.md +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/babel.config.js +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/package.json +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/sidebars.js +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/README.md +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/__init__.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/conftest.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_builds.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_console.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_factory.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_integration.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_manage.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_models.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.8.0 → code_sandboxes-1.8.2}/tests/test_providers.py +0 -0
- {code_sandboxes-1.8.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,11 +8,35 @@
|
|
|
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
|
+
|
|
11
36
|
## 1.8.0
|
|
12
37
|
|
|
13
38
|
- **The Datalayer builder** (`environments.adapters.datalayer`, PLAN_ENV.md
|
|
14
|
-
E1-07). The Dockerfile is generated from the lock — `uv pip sync
|
|
15
|
-
--require-hashes`, apt at the versions the lock recorded, `env` before
|
|
39
|
+
E1-07). The Dockerfile is generated from the lock — `uv pip sync --require-hashes`, apt at the versions the lock recorded, `env` before
|
|
16
40
|
anything installs, `postInstall` as uid 1000 with no network — and the push
|
|
17
41
|
is by digest with an SBOM and provenance attestation, under the operability
|
|
18
42
|
tag `v<n>-<build_uid>` so a retried build cannot collide with the attempt
|
|
@@ -80,8 +104,7 @@
|
|
|
80
104
|
pinned exceptions left.
|
|
81
105
|
|
|
82
106
|
- A Datalayer sandbox says when it is running code, so an interrupt can reach
|
|
83
|
-
it. `Sandbox.interrupt` refuses before it delegates — `if not
|
|
84
|
-
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
|
|
85
108
|
event, so `is_executing` was always False, every interrupt was refused at the
|
|
86
109
|
door, and the refusal was reported as "no code was running" to a caller
|
|
87
110
|
watching a cell run.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: code_sandboxes
|
|
3
|
-
Version: 1.8.
|
|
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
|
|
@@ -75,6 +75,7 @@ Provides-Extra: e2b
|
|
|
75
75
|
Requires-Dist: e2b-code-interpreter>=2.9.1; extra == 'e2b'
|
|
76
76
|
Provides-Extra: environments-builder
|
|
77
77
|
Requires-Dist: boto3>=1.34; extra == 'environments-builder'
|
|
78
|
+
Requires-Dist: httpx>=0.27; extra == 'environments-builder'
|
|
78
79
|
Provides-Extra: google-colab
|
|
79
80
|
Provides-Extra: kaggle
|
|
80
81
|
Requires-Dist: kaggle>=1.6; extra == 'kaggle'
|
|
@@ -87,6 +88,7 @@ Requires-Dist: modal>=1.5.2; extra == 'modal'
|
|
|
87
88
|
Provides-Extra: monty
|
|
88
89
|
Requires-Dist: pydantic-monty; extra == 'monty'
|
|
89
90
|
Provides-Extra: test
|
|
91
|
+
Requires-Dist: httpx>=0.27; extra == 'test'
|
|
90
92
|
Requires-Dist: ipykernel; extra == 'test'
|
|
91
93
|
Requires-Dist: jupyter-server<3,>=1.6; extra == 'test'
|
|
92
94
|
Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
|
|
@@ -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."""
|
|
@@ -598,7 +609,7 @@ class Sandbox(ABC):
|
|
|
598
609
|
# caller passed itself wins, so naming both is not a surprise.
|
|
599
610
|
from .environments.builders import launch_arguments
|
|
600
611
|
|
|
601
|
-
for name_, value in launch_arguments(artifact).items():
|
|
612
|
+
for name_, value in launch_arguments(artifact, expected_variant=variant_value).items():
|
|
602
613
|
kwargs.setdefault(name_, value)
|
|
603
614
|
|
|
604
615
|
if variant_value == "eval":
|
|
@@ -1078,7 +1089,7 @@ class Sandbox(ABC):
|
|
|
1078
1089
|
from .exceptions import VariableNotFoundError
|
|
1079
1090
|
|
|
1080
1091
|
raise VariableNotFoundError(name)
|
|
1081
|
-
return _json.loads(printed[start + len(self._STDOUT_START):end])
|
|
1092
|
+
return _json.loads(printed[start + len(self._STDOUT_START) : end])
|
|
1082
1093
|
|
|
1083
1094
|
def set_variable(self, name: str, value: Any, context: Context | None = None) -> None:
|
|
1084
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
|
|
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.)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
on a runtime that goes on
|
|
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:
|
|
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(
|
|
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)
|
|
@@ -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
|
-
|
|
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,
|
|
@@ -16,6 +16,7 @@ provider-specific:
|
|
|
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
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);
|
|
19
20
|
- :mod:`.attest` — the scan and the signature it needs before anything runs it;
|
|
20
21
|
- :mod:`.contract` — ``sandbox-contract/v1`` and the Dockerfile validator;
|
|
21
22
|
- :mod:`.lifecycle` — the version states and their legal moves;
|
|
@@ -30,6 +31,7 @@ The builders themselves are in :mod:`.adapters`, loaded by variant.
|
|
|
30
31
|
|
|
31
32
|
from __future__ import annotations
|
|
32
33
|
|
|
34
|
+
from .accounts import account_fingerprints, fingerprint_matches, provider_account
|
|
33
35
|
from .attest import Attestor, attest_artifact, signature_tag
|
|
34
36
|
from .bases import APPROVED_BASES, ApprovedBase
|
|
35
37
|
from .builders import (
|
|
@@ -106,6 +108,7 @@ __all__ = [
|
|
|
106
108
|
"ScanPolicy",
|
|
107
109
|
"ValidationResult",
|
|
108
110
|
"VersionState",
|
|
111
|
+
"account_fingerprints",
|
|
109
112
|
"attest_artifact",
|
|
110
113
|
"build_outcome",
|
|
111
114
|
"can_transition",
|
|
@@ -113,6 +116,7 @@ __all__ = [
|
|
|
113
116
|
"canonical_json",
|
|
114
117
|
"check_dockerfile",
|
|
115
118
|
"decide",
|
|
119
|
+
"fingerprint_matches",
|
|
116
120
|
"get_builder",
|
|
117
121
|
"locked_versions",
|
|
118
122
|
"map_provider_error",
|
|
@@ -120,6 +124,7 @@ __all__ = [
|
|
|
120
124
|
"parse_environment",
|
|
121
125
|
"parse_resolver_failure",
|
|
122
126
|
"protected_pins",
|
|
127
|
+
"provider_account",
|
|
123
128
|
"refuse_if_blocked",
|
|
124
129
|
"resolve_environment",
|
|
125
130
|
"signature_tag",
|
|
@@ -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()}
|