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