code-sandboxes 1.9.19__tar.gz → 1.9.21__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.19 → code_sandboxes-1.9.21}/PKG-INFO +1 -1
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/daytona.py +20 -2
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/e2b.py +18 -1
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/managed.py +37 -1
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/modal.py +1 -1
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/attest.py +30 -1
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/spec.py +49 -13
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/schemas/environment-v1alpha1.json +24 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_attest.py +54 -15
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_daytona_builder.py +18 -2
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_managed_builders.py +54 -15
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_spec.py +35 -3
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.gitignore +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/CHANGELOG.md +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/Dockerfile +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/LICENSE +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/Makefile +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/README.md +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/datalayer.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/bases.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/resolve.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/resolve_conda.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/Makefile +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/README.md +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/package.json +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/README.md +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/pyproject.toml +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_bases.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_datalayer_builder.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_modal_builder.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_resolve.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_environment_resolve_conda.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/tests/test_the_kill_relaunch_drill.py +0 -0
{code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/daytona.py
RENAMED
|
@@ -179,7 +179,12 @@ class Builder(ManagedBuilder):
|
|
|
179
179
|
title = "Daytona"
|
|
180
180
|
#: A `packages` list and, for conda (E3-02), an `environment.yml`
|
|
181
181
|
#: dependency file installed with `micromamba`.
|
|
182
|
-
build_sources = ("packages", "dependencyFile")
|
|
182
|
+
build_sources = ("packages", "dependencyFile", "dockerfile")
|
|
183
|
+
#: None beyond the contract's own (E3-03): `Image.from_dockerfile` keeps
|
|
184
|
+
#: the Dockerfile text as it is and Daytona builds it on a real Docker
|
|
185
|
+
#: builder, so the grammar it accepts is Docker's. Checked in the SDK on
|
|
186
|
+
#: 2026-09-17.
|
|
187
|
+
forbidden_instructions = ()
|
|
183
188
|
dependency_formats = ("conda",)
|
|
184
189
|
#: Daytona runs GPUs, on its own hardware and the owner's account (E2-17).
|
|
185
190
|
#: This builder does not build one yet: see `_own_findings`.
|
|
@@ -419,6 +424,11 @@ class Builder(ManagedBuilder):
|
|
|
419
424
|
self._log(line)
|
|
420
425
|
|
|
421
426
|
resources = self._resources(sdk, request.size_class)
|
|
427
|
+
# A snapshot is region-scoped, and the region that scopes it is
|
|
428
|
+
# Daytona's, not this platform's. `validate` already refuses
|
|
429
|
+
# more than one, so the first is the only one.
|
|
430
|
+
declared = list(request.environment.spec.compatibility.regions)
|
|
431
|
+
region = declared[0] if declared else None
|
|
422
432
|
try:
|
|
423
433
|
snapshot = client.snapshot.create(
|
|
424
434
|
sdk.CreateSnapshotParams(
|
|
@@ -426,7 +436,15 @@ class Builder(ManagedBuilder):
|
|
|
426
436
|
image=image,
|
|
427
437
|
resources=resources,
|
|
428
438
|
entrypoint=_CONTRACT_ENTRYPOINT,
|
|
429
|
-
|
|
439
|
+
# Only a region Daytona knows. `request.region` is
|
|
440
|
+
# *Datalayer's* — `r1` — and sending it answered
|
|
441
|
+
# "Region not found" on the first real Daytona
|
|
442
|
+
# build (2026-09-17). The owner names a Daytona
|
|
443
|
+
# region in `compatibility.regions`; with none,
|
|
444
|
+
# the field is left out and the account's own
|
|
445
|
+
# default decides, which is what every snapshot
|
|
446
|
+
# in the owner's account already has.
|
|
447
|
+
**({"region_id": region} if region else {}),
|
|
430
448
|
),
|
|
431
449
|
on_logs=on_logs,
|
|
432
450
|
timeout=self.max_build_seconds,
|
|
@@ -199,7 +199,24 @@ class Builder(ManagedBuilder):
|
|
|
199
199
|
title = "E2B"
|
|
200
200
|
#: A `packages` list and, for conda (E3-02), an `environment.yml`
|
|
201
201
|
#: dependency file installed with `micromamba`.
|
|
202
|
-
build_sources = ("packages", "dependencyFile")
|
|
202
|
+
build_sources = ("packages", "dependencyFile", "dockerfile")
|
|
203
|
+
#: What E2B's own Dockerfile parser does not handle (E3-03). Read from the
|
|
204
|
+
#: SDK on 2026-09-17: `e2b.template.dockerfile_parser` branches on FROM,
|
|
205
|
+
#: RUN, COPY, ADD, WORKDIR, USER, ENV, ARG, CMD and ENTRYPOINT, and for
|
|
206
|
+
#: anything else **prints `Unsupported instruction` and carries on**. So a
|
|
207
|
+
#: template built from a Dockerfile naming one of these comes back without
|
|
208
|
+
#: it and reports success — which is exactly what a capability report
|
|
209
|
+
#: exists to prevent.
|
|
210
|
+
forbidden_instructions = (
|
|
211
|
+
"VOLUME",
|
|
212
|
+
"EXPOSE",
|
|
213
|
+
"HEALTHCHECK",
|
|
214
|
+
"SHELL",
|
|
215
|
+
"ONBUILD",
|
|
216
|
+
"STOPSIGNAL",
|
|
217
|
+
"LABEL",
|
|
218
|
+
"MAINTAINER",
|
|
219
|
+
)
|
|
203
220
|
dependency_formats = ("conda",)
|
|
204
221
|
#: Firecracker microVMs: no GPU passthrough.
|
|
205
222
|
gpu = False
|
{code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/managed.py
RENAMED
|
@@ -73,7 +73,11 @@ class ManagedBuilder:
|
|
|
73
73
|
#: built for a managed variant yet (E3-01), so it is not listed here.
|
|
74
74
|
dependency_formats: tuple[str, ...] = ()
|
|
75
75
|
package_managers: tuple[str, ...] = ("uv", "pip")
|
|
76
|
-
#: Dockerfile instructions its own builder does not implement (§6).
|
|
76
|
+
#: Dockerfile instructions its own builder does not implement (§6, E3-03).
|
|
77
|
+
#: Checked against a `dockerfile`-sourced spec at `validate`, before
|
|
78
|
+
#: anything is queued — the point being to refuse an instruction a variant
|
|
79
|
+
#: would otherwise drop, rather than hand back an image quietly missing
|
|
80
|
+
#: whatever it asked for.
|
|
77
81
|
forbidden_instructions: tuple[str, ...] = ()
|
|
78
82
|
#: Where its artifacts live; empty when the variant is regionless.
|
|
79
83
|
regions: tuple[str, ...] = ()
|
|
@@ -128,6 +132,8 @@ class ManagedBuilder:
|
|
|
128
132
|
)
|
|
129
133
|
elif spec.build.source == "dependencyFile":
|
|
130
134
|
findings.extend(self._dependency_file_findings(spec))
|
|
135
|
+
elif spec.build.source == "dockerfile":
|
|
136
|
+
findings.extend(self._dockerfile_findings(spec))
|
|
131
137
|
if spec.packages.python.manager not in self.package_managers:
|
|
132
138
|
findings.append(
|
|
133
139
|
CapabilityFinding(
|
|
@@ -269,6 +275,36 @@ class ManagedBuilder:
|
|
|
269
275
|
|
|
270
276
|
# -- helpers for the subclasses -------------------------------------------
|
|
271
277
|
|
|
278
|
+
def _dockerfile_findings(self, spec: Any) -> list[CapabilityFinding]:
|
|
279
|
+
"""Every instruction in the spec's Dockerfile this variant would not honour.
|
|
280
|
+
|
|
281
|
+
The contract's own refusals are `spec.py`'s to make and apply to every
|
|
282
|
+
variant alike; this is the narrower question of what *this* builder
|
|
283
|
+
does with a Dockerfile it accepts. Each is named with its line, since
|
|
284
|
+
a Dockerfile is somebody's file.
|
|
285
|
+
"""
|
|
286
|
+
if not self.forbidden_instructions:
|
|
287
|
+
return []
|
|
288
|
+
dockerfile = getattr(spec.build, "dockerfile", None)
|
|
289
|
+
content = getattr(dockerfile, "content", "") or ""
|
|
290
|
+
if not content.strip():
|
|
291
|
+
return []
|
|
292
|
+
from ..contract import parse_dockerfile
|
|
293
|
+
|
|
294
|
+
refused = set(self.forbidden_instructions)
|
|
295
|
+
return [
|
|
296
|
+
CapabilityFinding(
|
|
297
|
+
code=CAPABILITY_UNSUPPORTED.code,
|
|
298
|
+
message=(
|
|
299
|
+
f"line {instruction.line}: {self.title} does not implement "
|
|
300
|
+
f"`{instruction.keyword}`"
|
|
301
|
+
),
|
|
302
|
+
field="spec.build.dockerfile.content",
|
|
303
|
+
)
|
|
304
|
+
for instruction in parse_dockerfile(content)
|
|
305
|
+
if instruction.keyword in refused
|
|
306
|
+
]
|
|
307
|
+
|
|
272
308
|
@staticmethod
|
|
273
309
|
def _spec_finding(message: str, field: str) -> CapabilityFinding:
|
|
274
310
|
return CapabilityFinding(code=SPEC_INVALID.code, message=message, field=field)
|
{code_sandboxes-1.9.19 → code_sandboxes-1.9.21}/code_sandboxes/environments/adapters/modal.py
RENAMED
|
@@ -286,7 +286,7 @@ class Builder(ManagedBuilder):
|
|
|
286
286
|
title = "Modal"
|
|
287
287
|
#: A `packages` list and, for conda (E3-02), an `environment.yml`
|
|
288
288
|
#: dependency file installed with `micromamba_install`.
|
|
289
|
-
build_sources = ("packages", "dependencyFile")
|
|
289
|
+
build_sources = ("packages", "dependencyFile", "dockerfile")
|
|
290
290
|
dependency_formats = ("conda",)
|
|
291
291
|
#: Modal runs GPUs, in the owner's workspace (E2-17). This builder does
|
|
292
292
|
#: not build one yet: see `build`'s own guard.
|
|
@@ -675,14 +675,43 @@ def attest_artifact(
|
|
|
675
675
|
answers the mapping the workflow stores: the scan's decision, the
|
|
676
676
|
signature, the SBOM and provenance references, and the size.
|
|
677
677
|
"""
|
|
678
|
+
variant = str(getattr(artifact, "variant", "") or "")
|
|
678
679
|
reference = str(getattr(artifact, "immutable_reference", "") or "")
|
|
680
|
+
if variant and variant != "datalayer":
|
|
681
|
+
# D-11 is about the Datalayer artifact: it lives in this platform's
|
|
682
|
+
# registry, the scanner reads it there, cosign signs that digest, and
|
|
683
|
+
# the Operator refuses to start what is unsigned. A managed artifact
|
|
684
|
+
# is none of those things — it lives in the owner's own provider
|
|
685
|
+
# account (D-8) and is referenced the way that provider names it: a
|
|
686
|
+
# Daytona snapshot uuid, an E2B build id, a Modal `im-…`. There is no
|
|
687
|
+
# digest in a repository to scan or sign, and no Operator starting it.
|
|
688
|
+
#
|
|
689
|
+
# Attesting one anyway is what the first real Daytona build did, and
|
|
690
|
+
# it failed *after* the snapshot was built — the work done, the
|
|
691
|
+
# artifact live at the provider, and the build recorded as failed
|
|
692
|
+
# (2026-09-17).
|
|
693
|
+
#
|
|
694
|
+
# Publishing is not weakened by this: E2-15's gate requires the
|
|
695
|
+
# **Datalayer** artifact to have passed its scan, and that one is
|
|
696
|
+
# still attested here.
|
|
697
|
+
if log:
|
|
698
|
+
log(f"{variant} artifacts are not attested: {reference} is not a digest in a registry")
|
|
699
|
+
return {
|
|
700
|
+
"scan_summary": {},
|
|
701
|
+
"sbom_ref": "",
|
|
702
|
+
"provenance_ref": "",
|
|
703
|
+
"signature_ref": "",
|
|
704
|
+
"size_bytes": size_bytes,
|
|
705
|
+
"signed_now": False,
|
|
706
|
+
"licenses": [],
|
|
707
|
+
}
|
|
679
708
|
registry, _, rest = reference.partition("/")
|
|
680
709
|
repository, _, digest = rest.partition("@")
|
|
681
710
|
if not (registry and repository and _DIGEST.match(digest)):
|
|
682
711
|
raise EnvironmentsError(
|
|
683
712
|
PROVIDER_ERROR,
|
|
684
713
|
f"`{reference}` is not a digest in a repository, so it cannot be attested",
|
|
685
|
-
detail={"reference": reference},
|
|
714
|
+
detail={"reference": reference, "variant": variant},
|
|
686
715
|
)
|
|
687
716
|
use = attestor or Attestor(
|
|
688
717
|
key=str(getattr(credential, "signing_key", "") or ""),
|
|
@@ -55,10 +55,10 @@ __all__ = [
|
|
|
55
55
|
"BUILD_SOURCES",
|
|
56
56
|
"GPU_SIZE_CLASSES",
|
|
57
57
|
"KIND",
|
|
58
|
+
"PUBLIC_PACKAGE_INDEX_HOSTS",
|
|
58
59
|
"SIZE_CLASSES",
|
|
59
60
|
"SUPPORTED_BUILD_SOURCES",
|
|
60
61
|
"VARIANTS",
|
|
61
|
-
"PUBLIC_PACKAGE_INDEX_HOSTS",
|
|
62
62
|
"Accelerator",
|
|
63
63
|
"ArtifactStatus",
|
|
64
64
|
"Base",
|
|
@@ -174,14 +174,12 @@ class Platform(_Model):
|
|
|
174
174
|
#: reached with a credential the public does not hold. Matched on host, so the
|
|
175
175
|
#: trailing `/simple` or its absence never decides it. `pypi.org` is the index;
|
|
176
176
|
#: `files.pythonhosted.org` is where its wheels are served from.
|
|
177
|
-
PUBLIC_PACKAGE_INDEX_HOSTS = frozenset(
|
|
178
|
-
{"pypi.org", "files.pythonhosted.org"}
|
|
179
|
-
)
|
|
177
|
+
PUBLIC_PACKAGE_INDEX_HOSTS = frozenset({"pypi.org", "files.pythonhosted.org"})
|
|
180
178
|
|
|
181
179
|
|
|
182
180
|
def _package_index_host(url: str) -> str:
|
|
183
181
|
"""The host an index URL names, lower-cased and without its port, or `""`."""
|
|
184
|
-
from urllib.parse import urlsplit
|
|
182
|
+
from urllib.parse import urlsplit
|
|
185
183
|
|
|
186
184
|
try:
|
|
187
185
|
return (urlsplit(url).hostname or "").lower()
|
|
@@ -214,9 +212,7 @@ PUBLIC_CONDA_CHANNELS = frozenset(
|
|
|
214
212
|
"msys2",
|
|
215
213
|
}
|
|
216
214
|
)
|
|
217
|
-
PUBLIC_CONDA_CHANNEL_HOSTS = frozenset(
|
|
218
|
-
{"conda.anaconda.org", "repo.anaconda.com", "anaconda.org"}
|
|
219
|
-
)
|
|
215
|
+
PUBLIC_CONDA_CHANNEL_HOSTS = frozenset({"conda.anaconda.org", "repo.anaconda.com", "anaconda.org"})
|
|
220
216
|
|
|
221
217
|
|
|
222
218
|
def channel_is_public(channel: str) -> bool:
|
|
@@ -344,6 +340,25 @@ class DependencyFileSpec(_Model):
|
|
|
344
340
|
lock_content: str = ""
|
|
345
341
|
|
|
346
342
|
|
|
343
|
+
class DockerfileSpec(_Model):
|
|
344
|
+
"""The Dockerfile a `dockerfile`-sourced version builds from (E3-03).
|
|
345
|
+
|
|
346
|
+
Inline, the way ``DependencyFileSpec`` carries a ``requirements.txt``: a
|
|
347
|
+
file the author brings lives in the spec, so ``check_dockerfile`` and
|
|
348
|
+
every variant's capability report can read it **before** anything is
|
|
349
|
+
queued — which is the whole point of refusing an instruction a variant
|
|
350
|
+
does not implement at ``validate`` rather than halfway through a build.
|
|
351
|
+
|
|
352
|
+
The build *context* — the extra files a ``COPY`` needs — is the separate
|
|
353
|
+
upload E3-03 describes, bounded and checked by ``validate_build_context``.
|
|
354
|
+
A Dockerfile with no context is the common case and needs no upload at
|
|
355
|
+
all.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
#: The Dockerfile text.
|
|
359
|
+
content: str = ""
|
|
360
|
+
|
|
361
|
+
|
|
347
362
|
class ImageSourceSpec(_Model):
|
|
348
363
|
"""An existing OCI image, imported as the build's base (E3-04).
|
|
349
364
|
|
|
@@ -370,6 +385,7 @@ class ImageSourceSpec(_Model):
|
|
|
370
385
|
class BuildSpec(_Model):
|
|
371
386
|
source: Literal["packages", "dependencyFile", "dockerfile", "image"] = "packages"
|
|
372
387
|
dependency_file: DependencyFileSpec | None = None
|
|
388
|
+
dockerfile: DockerfileSpec | None = None
|
|
373
389
|
image: ImageSourceSpec | None = None
|
|
374
390
|
|
|
375
391
|
|
|
@@ -486,6 +502,28 @@ def parse_requirements_txt(text: str) -> list[str]:
|
|
|
486
502
|
return lines
|
|
487
503
|
|
|
488
504
|
|
|
505
|
+
def _dockerfile_findings(dockerfile: DockerfileSpec | None) -> list[SpecFinding]:
|
|
506
|
+
"""What a `dockerfile`-sourced version must carry, and what the contract refuses.
|
|
507
|
+
|
|
508
|
+
The contract's own refusals are reported here, at `validate`, rather than
|
|
509
|
+
at build time: a `FROM` that is not an approved base, a privileged build,
|
|
510
|
+
the host network, a Docker socket mount, a host bind mount, and every
|
|
511
|
+
instruction §6 does not allow. Each is named with its line, because a
|
|
512
|
+
Dockerfile is somebody's file and "it was refused" is not a reason.
|
|
513
|
+
"""
|
|
514
|
+
field = "spec.build.dockerfile"
|
|
515
|
+
if dockerfile is None:
|
|
516
|
+
return [SpecFinding(field, "is required when `spec.build.source` is `dockerfile`")]
|
|
517
|
+
if not dockerfile.content.strip():
|
|
518
|
+
return [SpecFinding(f"{field}.content", "is empty; it is the Dockerfile text")]
|
|
519
|
+
from .contract import validate_dockerfile
|
|
520
|
+
|
|
521
|
+
return [
|
|
522
|
+
SpecFinding(f"{field}.content", f"line {finding.line}: {finding.message}")
|
|
523
|
+
for finding in validate_dockerfile(dockerfile.content)
|
|
524
|
+
]
|
|
525
|
+
|
|
526
|
+
|
|
489
527
|
def _dependency_file_findings(dependency_file: DependencyFileSpec | None) -> list[SpecFinding]:
|
|
490
528
|
field = "spec.build.dependencyFile"
|
|
491
529
|
if dependency_file is None:
|
|
@@ -644,6 +682,8 @@ def spec_findings(
|
|
|
644
682
|
)
|
|
645
683
|
if spec.build.source == "dependencyFile":
|
|
646
684
|
findings.extend(_dependency_file_findings(spec.build.dependency_file))
|
|
685
|
+
elif spec.build.source == "dockerfile":
|
|
686
|
+
findings.extend(_dockerfile_findings(spec.build.dockerfile))
|
|
647
687
|
elif spec.build.source == "image":
|
|
648
688
|
findings.extend(_image_findings(spec.build.image))
|
|
649
689
|
|
|
@@ -951,11 +991,7 @@ def publication_findings(environment: Environment) -> list[SpecFinding]:
|
|
|
951
991
|
PUBLICATION_BLOCKED,
|
|
952
992
|
)
|
|
953
993
|
)
|
|
954
|
-
private = [
|
|
955
|
-
url
|
|
956
|
-
for url in environment.spec.packages.python.indexes
|
|
957
|
-
if not index_is_public(url)
|
|
958
|
-
]
|
|
994
|
+
private = [url for url in environment.spec.packages.python.indexes if not index_is_public(url)]
|
|
959
995
|
if private:
|
|
960
996
|
findings.append(
|
|
961
997
|
SpecFinding(
|
|
@@ -95,6 +95,17 @@
|
|
|
95
95
|
],
|
|
96
96
|
"default": null
|
|
97
97
|
},
|
|
98
|
+
"dockerfile": {
|
|
99
|
+
"anyOf": [
|
|
100
|
+
{
|
|
101
|
+
"$ref": "#/$defs/DockerfileSpec"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "null"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"default": null
|
|
108
|
+
},
|
|
98
109
|
"image": {
|
|
99
110
|
"anyOf": [
|
|
100
111
|
{
|
|
@@ -219,6 +230,19 @@
|
|
|
219
230
|
"title": "DependencyFileSpec",
|
|
220
231
|
"type": "object"
|
|
221
232
|
},
|
|
233
|
+
"DockerfileSpec": {
|
|
234
|
+
"additionalProperties": false,
|
|
235
|
+
"description": "The Dockerfile a `dockerfile`-sourced version builds from (E3-03).\n\nInline, the way ``DependencyFileSpec`` carries a ``requirements.txt``: a\nfile the author brings lives in the spec, so ``check_dockerfile`` and\nevery variant's capability report can read it **before** anything is\nqueued \u2014 which is the whole point of refusing an instruction a variant\ndoes not implement at ``validate`` rather than halfway through a build.\n\nThe build *context* \u2014 the extra files a ``COPY`` needs \u2014 is the separate\nupload E3-03 describes, bounded and checked by ``validate_build_context``.\nA Dockerfile with no context is the common case and needs no upload at\nall.",
|
|
236
|
+
"properties": {
|
|
237
|
+
"content": {
|
|
238
|
+
"default": "",
|
|
239
|
+
"title": "Content",
|
|
240
|
+
"type": "string"
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
"title": "DockerfileSpec",
|
|
244
|
+
"type": "object"
|
|
245
|
+
},
|
|
222
246
|
"EnvironmentSpec": {
|
|
223
247
|
"additionalProperties": false,
|
|
224
248
|
"properties": {
|
|
@@ -19,12 +19,13 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import json
|
|
21
21
|
import subprocess
|
|
22
|
+
from types import SimpleNamespace
|
|
22
23
|
|
|
23
24
|
import pytest
|
|
24
25
|
|
|
25
26
|
from code_sandboxes.environments.attest import Attestor, attest_artifact, licenses_of, signature_tag
|
|
26
27
|
from code_sandboxes.environments.builders import ArtifactReference
|
|
27
|
-
from code_sandboxes.environments.errors import EnvironmentsError
|
|
28
|
+
from code_sandboxes.environments.errors import PROVIDER_ERROR, EnvironmentsError
|
|
28
29
|
from code_sandboxes.environments.policy import (
|
|
29
30
|
DEFAULT_POLICY,
|
|
30
31
|
Finding,
|
|
@@ -613,27 +614,25 @@ class TestAttestingAnArtifact:
|
|
|
613
614
|
assert cosign.argv == [], "a blocked artifact must not be signed"
|
|
614
615
|
|
|
615
616
|
def test_a_reference_that_is_not_a_digest_cannot_be_attested(self) -> None:
|
|
616
|
-
artifact
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
)
|
|
617
|
+
"""A `datalayer` artifact must be a digest in this platform's registry.
|
|
618
|
+
|
|
619
|
+
`attest_artifact` takes `artifact: Any`, so nothing guarantees every
|
|
620
|
+
caller went through the model validator that would have refused this.
|
|
621
|
+
"""
|
|
622
|
+
artifact = SimpleNamespace(variant="datalayer", immutable_reference="im-1234567890")
|
|
622
623
|
with pytest.raises(EnvironmentsError) as raised:
|
|
623
624
|
attest_artifact(artifact=artifact, attestor=an_attestor())
|
|
624
625
|
assert raised.value.code.code == "DL_ENV_PROVIDER_ERROR"
|
|
625
626
|
|
|
626
627
|
def test_a_malformed_digest_cannot_be_attested_either(self) -> None:
|
|
627
628
|
"""`sha256:bad` starts with `sha256:` too: only a whole one is
|
|
628
|
-
accepted (found on PR #27's Copilot review).
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
artifact =
|
|
633
|
-
variant="
|
|
629
|
+
accepted (found on PR #27's Copilot review). Asked of `datalayer`,
|
|
630
|
+
since that is the variant this check is for — `attest_artifact` takes
|
|
631
|
+
`artifact: Any`, so nothing guarantees every caller went through the
|
|
632
|
+
model validator that would have refused it."""
|
|
633
|
+
artifact = SimpleNamespace(
|
|
634
|
+
variant="datalayer",
|
|
634
635
|
immutable_reference=f"{REGISTRY}/{REPOSITORY}@sha256:bad",
|
|
635
|
-
provider_artifact_id="sha256:bad",
|
|
636
|
-
contract_version="sandbox-contract/v1",
|
|
637
636
|
)
|
|
638
637
|
with pytest.raises(EnvironmentsError) as raised:
|
|
639
638
|
attest_artifact(artifact=artifact, attestor=an_attestor())
|
|
@@ -749,3 +748,43 @@ class TestLicencesFromTheSbom:
|
|
|
749
748
|
"""Never a reason to fail a build: a licence list is worth having, not dying for."""
|
|
750
749
|
for document in (None, {}, {"packages": None}, {"components": [1, 2]}, "spdx"):
|
|
751
750
|
assert licenses_of(document) == []
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class TestWhatIsAttestedAndWhatIsNot:
|
|
754
|
+
"""D-11 is about the Datalayer artifact, not every artifact.
|
|
755
|
+
|
|
756
|
+
It lives in this platform's registry: the scanner reads it there, cosign
|
|
757
|
+
signs that digest, and the Operator refuses to start what is unsigned. A
|
|
758
|
+
managed artifact is none of those things — it lives in the owner's own
|
|
759
|
+
provider account (D-8), named the way that provider names it.
|
|
760
|
+
"""
|
|
761
|
+
|
|
762
|
+
def _artifact(self, variant: str, reference: str):
|
|
763
|
+
return SimpleNamespace(variant=variant, immutable_reference=reference)
|
|
764
|
+
|
|
765
|
+
def test_a_daytona_snapshot_is_not_attested(self) -> None:
|
|
766
|
+
"""Its reference is a uuid, and attesting it anyway failed the first
|
|
767
|
+
real Daytona build *after* the snapshot was already built
|
|
768
|
+
(2026-09-17)."""
|
|
769
|
+
answer = attest_artifact(
|
|
770
|
+
artifact=self._artifact("daytona", "51d10ab0-d98d-4117-bdb5-918e98646c92"),
|
|
771
|
+
size_bytes=1234,
|
|
772
|
+
)
|
|
773
|
+
assert answer["scan_summary"] == {}
|
|
774
|
+
assert answer["signature_ref"] == ""
|
|
775
|
+
assert answer["signed_now"] is False
|
|
776
|
+
# What the provider told us is still recorded.
|
|
777
|
+
assert answer["size_bytes"] == 1234
|
|
778
|
+
|
|
779
|
+
def test_an_e2b_build_id_and_a_modal_image_id_are_not_either(self) -> None:
|
|
780
|
+
for variant, reference in (("e2b", "bld-123"), ("modal", "im-abc123")):
|
|
781
|
+
answer = attest_artifact(artifact=self._artifact(variant, reference))
|
|
782
|
+
assert answer["signed_now"] is False, variant
|
|
783
|
+
assert answer["scan_summary"] == {}, variant
|
|
784
|
+
|
|
785
|
+
def test_a_datalayer_artifact_that_is_not_a_digest_still_fails(self) -> None:
|
|
786
|
+
"""The check that matters is kept where it means something."""
|
|
787
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
788
|
+
attest_artifact(artifact=self._artifact("datalayer", "not-a-digest"))
|
|
789
|
+
assert raised.value.code is PROVIDER_ERROR
|
|
790
|
+
assert "cannot be attested" in str(raised.value)
|
|
@@ -568,12 +568,28 @@ class TestBuildingASnapshot:
|
|
|
568
568
|
resources = daytona.client.snapshot.create_calls[0].args[0].resources
|
|
569
569
|
assert (resources.cpu, resources.memory, resources.disk) == (cpu, memory, disk)
|
|
570
570
|
|
|
571
|
-
def
|
|
571
|
+
def test_the_region_sent_is_daytonas_own_not_this_platforms(self) -> None:
|
|
572
|
+
"""`request.region` is Datalayer's — `r1` — and Daytona answered
|
|
573
|
+
"Region not found" for it on the first real build (2026-09-17).
|
|
574
|
+
|
|
575
|
+
The owner names a Daytona region in `compatibility.regions`; that is
|
|
576
|
+
the one that scopes the snapshot.
|
|
577
|
+
"""
|
|
572
578
|
daytona = FakeDaytonaModule()
|
|
573
|
-
a_builder(daytona=daytona).build(
|
|
579
|
+
a_builder(daytona=daytona).build(
|
|
580
|
+
a_request(region="r1", spec={"compatibility": {"regions": ["eu"]}})
|
|
581
|
+
)
|
|
574
582
|
params = daytona.client.snapshot.create_calls[0].args[0]
|
|
575
583
|
assert params.region_id == "eu"
|
|
576
584
|
|
|
585
|
+
def test_with_no_region_named_the_account_default_decides(self) -> None:
|
|
586
|
+
"""The field is left out rather than filled with something Daytona
|
|
587
|
+
does not know — which is what every snapshot in a real account has."""
|
|
588
|
+
daytona = FakeDaytonaModule()
|
|
589
|
+
a_builder(daytona=daytona).build(a_request(region="r1"))
|
|
590
|
+
params = daytona.client.snapshot.create_calls[0].args[0]
|
|
591
|
+
assert params.region_id is None
|
|
592
|
+
|
|
577
593
|
def test_the_snapshot_is_named_after_the_environment_and_version(self) -> None:
|
|
578
594
|
daytona = FakeDaytonaModule()
|
|
579
595
|
a_builder(daytona=daytona).build(a_request())
|
|
@@ -55,11 +55,7 @@ def environment(**spec: Any) -> Environment:
|
|
|
55
55
|
#: A `dependencyFile` conda source: an `environment.yml` a managed variant
|
|
56
56
|
#: builds with `micromamba` (E3-02).
|
|
57
57
|
CONDA_ENVIRONMENT_YML = (
|
|
58
|
-
"name: geo\n"
|
|
59
|
-
"channels: [conda-forge]\n"
|
|
60
|
-
"dependencies:\n"
|
|
61
|
-
" - python=3.13\n"
|
|
62
|
-
" - gdal\n"
|
|
58
|
+
"name: geo\nchannels: [conda-forge]\ndependencies:\n - python=3.13\n - gdal\n"
|
|
63
59
|
)
|
|
64
60
|
|
|
65
61
|
|
|
@@ -102,15 +98,33 @@ class TestTheCapabilitySets:
|
|
|
102
98
|
assert get_builder("modal").capabilities().supports_gpu is True
|
|
103
99
|
assert get_builder("daytona").capabilities().supports_gpu is True
|
|
104
100
|
|
|
105
|
-
def
|
|
106
|
-
"""Modal implements its own Dockerfile builder
|
|
107
|
-
Dockerfile
|
|
101
|
+
def test_each_variant_forbids_what_its_own_builder_will_not_honour(self) -> None:
|
|
102
|
+
"""Modal implements its own Dockerfile builder, and E2B parses a
|
|
103
|
+
Dockerfile into Template SDK calls; Daytona hands the text to a real
|
|
104
|
+
Docker builder, which implements all of them.
|
|
105
|
+
|
|
106
|
+
E2B's list is read from its SDK (E3-03, 2026-09-17):
|
|
107
|
+
`e2b.template.dockerfile_parser` branches on FROM, RUN, COPY, ADD,
|
|
108
|
+
WORKDIR, USER, ENV, ARG, CMD and ENTRYPOINT, and for anything else
|
|
109
|
+
**prints `Unsupported instruction` and carries on** — so a template
|
|
110
|
+
built from a Dockerfile naming one of these comes back without it and
|
|
111
|
+
reports success. That is the case a capability report exists for.
|
|
112
|
+
"""
|
|
108
113
|
assert set(get_builder("modal").capabilities().forbidden_instructions) == {
|
|
109
114
|
"ONBUILD",
|
|
110
115
|
"STOPSIGNAL",
|
|
111
116
|
"VOLUME",
|
|
112
117
|
}
|
|
113
|
-
assert get_builder("e2b").capabilities().forbidden_instructions ==
|
|
118
|
+
assert set(get_builder("e2b").capabilities().forbidden_instructions) == {
|
|
119
|
+
"VOLUME",
|
|
120
|
+
"EXPOSE",
|
|
121
|
+
"HEALTHCHECK",
|
|
122
|
+
"SHELL",
|
|
123
|
+
"ONBUILD",
|
|
124
|
+
"STOPSIGNAL",
|
|
125
|
+
"LABEL",
|
|
126
|
+
"MAINTAINER",
|
|
127
|
+
}
|
|
114
128
|
assert get_builder("daytona").capabilities().forbidden_instructions == ()
|
|
115
129
|
|
|
116
130
|
def test_each_one_bounds_how_long_a_build_may_take(self) -> None:
|
|
@@ -120,10 +134,13 @@ class TestTheCapabilitySets:
|
|
|
120
134
|
seconds = get_builder(variant).capabilities().max_build_seconds
|
|
121
135
|
assert seconds and 0 < seconds <= 60 * 60, variant
|
|
122
136
|
|
|
123
|
-
def
|
|
137
|
+
def test_this_phase_builds_a_package_list_a_conda_file_and_a_dockerfile(self) -> None:
|
|
138
|
+
"""All three take a Dockerfile (E3-03), each through its own door:
|
|
139
|
+
E2B parses one into Template SDK calls, Daytona hands the text to a
|
|
140
|
+
real Docker builder, Modal builds it with its own frontend."""
|
|
124
141
|
for variant in MANAGED:
|
|
125
142
|
sources = get_builder(variant).capabilities().build_sources
|
|
126
|
-
assert sources == ("packages", "dependencyFile"), variant
|
|
143
|
+
assert sources == ("packages", "dependencyFile", "dockerfile"), variant
|
|
127
144
|
|
|
128
145
|
|
|
129
146
|
# -- what each one refuses ------------------------------------------------------
|
|
@@ -230,10 +247,29 @@ class TestWhatIsSaidBeforeAnythingIsQueued:
|
|
|
230
247
|
|
|
231
248
|
def test_a_source_this_phase_does_not_build_is_refused_by_name(self) -> None:
|
|
232
249
|
for variant in MANAGED:
|
|
233
|
-
report = get_builder(variant).validate(environment(build={"source": "
|
|
250
|
+
report = get_builder(variant).validate(environment(build={"source": "image"}))
|
|
234
251
|
assert report.supported is False, variant
|
|
235
|
-
assert "`
|
|
236
|
-
assert "it builds packages, dependencyFile" in messages(report)
|
|
252
|
+
assert "`image` is not built for" in messages(report)
|
|
253
|
+
assert "it builds packages, dependencyFile, dockerfile" in messages(report)
|
|
254
|
+
|
|
255
|
+
def test_an_instruction_a_variant_would_drop_is_refused_with_its_line(self) -> None:
|
|
256
|
+
"""The case this exists for: E2B's parser prints `Unsupported
|
|
257
|
+
instruction` and carries on, so without this the template comes back
|
|
258
|
+
missing what the Dockerfile asked for and the build reports success."""
|
|
259
|
+
dockerfile = "FROM datalayer/python-cpu:2026.09\nRUN true\nVOLUME /data\n"
|
|
260
|
+
report = get_builder("e2b").validate(
|
|
261
|
+
environment(build={"source": "dockerfile", "dockerfile": {"content": dockerfile}})
|
|
262
|
+
)
|
|
263
|
+
assert report.supported is False
|
|
264
|
+
assert "line 3: E2B does not implement `VOLUME`" in messages(report)
|
|
265
|
+
|
|
266
|
+
def test_a_dockerfile_a_variant_can_honour_is_accepted(self) -> None:
|
|
267
|
+
"""Daytona builds the text as it is, so Docker's own grammar is the limit."""
|
|
268
|
+
dockerfile = "FROM datalayer/python-cpu:2026.09\nVOLUME /data\nEXPOSE 8888\n"
|
|
269
|
+
report = get_builder("daytona").validate(
|
|
270
|
+
environment(build={"source": "dockerfile", "dockerfile": {"content": dockerfile}})
|
|
271
|
+
)
|
|
272
|
+
assert report.supported is True, messages(report)
|
|
237
273
|
|
|
238
274
|
def test_a_conda_dependency_file_is_buildable_on_every_managed_variant(self) -> None:
|
|
239
275
|
for variant in MANAGED:
|
|
@@ -415,8 +451,11 @@ def test_every_variant_validates_with_no_provider_sdk_installed() -> None:
|
|
|
415
451
|
assert builder.capabilities().variant == variant
|
|
416
452
|
report = builder.validate(environment())
|
|
417
453
|
assert report.supported is True, f"{variant}: {messages(report)}"
|
|
454
|
+
# A refusal each variant still makes, and makes without an SDK.
|
|
455
|
+
# `dockerfile` stopped being one when E3-03 gave all three a door
|
|
456
|
+
# into it, so a managed variant is asked about `image` instead.
|
|
418
457
|
refused = get_builder(variant).validate(
|
|
419
|
-
environment(build={"source": "
|
|
458
|
+
environment(build={"source": "image"})
|
|
420
459
|
if variant != "datalayer"
|
|
421
460
|
else environment(
|
|
422
461
|
resources={"sizeClass": "gpu-small", "accelerator": {"type": "A10G"}}
|
|
@@ -322,14 +322,46 @@ def test_an_invalid_field_outranks_something_unsupported() -> None:
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
|
|
325
|
-
def
|
|
326
|
-
# E3-03: the base is the `FROM` its uploaded Dockerfile names, so
|
|
327
|
-
# `spec.base` is not checked against the approved table (as for `image`).
|
|
325
|
+
def a_dockerfile_document(content: str) -> dict[str, Any]:
|
|
328
326
|
data = mutated("spec.build.source", "dockerfile")
|
|
327
|
+
data["spec"]["build"]["dockerfile"] = {"content": content}
|
|
328
|
+
return data
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_a_dockerfile_source_is_accepted_and_keeps_its_own_base() -> None:
|
|
332
|
+
# E3-03: the base is the `FROM` the Dockerfile names, so `spec.base` is
|
|
333
|
+
# not checked against the approved table (as for `image`).
|
|
334
|
+
data = a_dockerfile_document("FROM datalayer/python-cpu:2026.09\nRUN true\n")
|
|
329
335
|
data["spec"]["base"]["ref"] = "python"
|
|
330
336
|
assert _codes(data) == {}
|
|
331
337
|
|
|
332
338
|
|
|
339
|
+
def test_a_dockerfile_source_without_a_dockerfile_is_refused() -> None:
|
|
340
|
+
"""`source: dockerfile` with nowhere to read the file from is not a spec."""
|
|
341
|
+
data = mutated("spec.build.source", "dockerfile")
|
|
342
|
+
assert _codes(data) == {"spec.build.dockerfile": "DL_ENV_SPEC_INVALID"}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_an_empty_dockerfile_is_refused() -> None:
|
|
346
|
+
assert _codes(a_dockerfile_document(" \n")) == {
|
|
347
|
+
"spec.build.dockerfile.content": "DL_ENV_SPEC_INVALID"
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_the_contract_refuses_a_dockerfile_at_validate_naming_the_line() -> None:
|
|
352
|
+
"""A Dockerfile is somebody's file: "it was refused" is not a reason.
|
|
353
|
+
|
|
354
|
+
The refusals are the contract's own — an unapproved base, the host
|
|
355
|
+
network, a privileged build, a Docker socket mount — and they are made
|
|
356
|
+
here, before anything is queued, rather than partway through a build.
|
|
357
|
+
"""
|
|
358
|
+
data = a_dockerfile_document("FROM ubuntu:22.04\nRUN --network=host apt-get update\n")
|
|
359
|
+
assert _codes(data) == {"spec.build.dockerfile.content": "DL_ENV_SPEC_INVALID"}
|
|
360
|
+
messages = [finding.message for finding in spec_findings(parse_environment(data))]
|
|
361
|
+
assert any("line 1" in message and "approved" in message for message in messages)
|
|
362
|
+
assert any("line 2" in message and "host network" in message for message in messages)
|
|
363
|
+
|
|
364
|
+
|
|
333
365
|
# -- Dependency files (E3-01) --------------------------------------------------
|
|
334
366
|
|
|
335
367
|
|
|
File without changes
|