code-sandboxes 1.9.28__tar.gz → 1.9.30__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.28 → code_sandboxes-1.9.30}/PKG-INFO +1 -1
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/adapters/datalayer.py +45 -11
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/adapters/daytona.py +34 -6
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/contract.py +190 -8
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/resolve.py +41 -18
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/spec.py +15 -3
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_datalayer_builder.py +1 -6
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_daytona_builder.py +5 -0
- code_sandboxes-1.9.30/tests/test_environment_dockerfile_source.py +229 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_resolve.py +0 -11
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.gitignore +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/CHANGELOG.md +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/Dockerfile +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/LICENSE +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/Makefile +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/README.md +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/adapters/modal.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/attest.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/bases.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/resolve_conda.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/Makefile +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/README.md +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/package.json +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/README.md +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/pyproject.toml +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/schemas/environment-v1alpha1.json +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_attest.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_bases.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_managed_builders.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_modal_builder.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_organization_policy.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_resolve_conda.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_spec.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_environment_spec_organization_policy.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/tests/test_the_kill_relaunch_drill.py +0 -0
{code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/adapters/datalayer.py
RENAMED
|
@@ -61,7 +61,7 @@ from ..builders import (
|
|
|
61
61
|
owner_cache_repository,
|
|
62
62
|
owner_repository,
|
|
63
63
|
)
|
|
64
|
-
from ..contract import SANDBOX_CONTRACT_V1
|
|
64
|
+
from ..contract import SANDBOX_CONTRACT_V1, pin_dockerfile_base
|
|
65
65
|
from ..errors import (
|
|
66
66
|
ARTIFACT_MISSING,
|
|
67
67
|
BUILD_FAILED,
|
|
@@ -145,6 +145,41 @@ def _secret_mount(secret: BuildSecret) -> str:
|
|
|
145
145
|
return f"--mount=type=secret,id={secret.id},env={secret.name}"
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
def _head(request: BuildRequest, authored: Any) -> list[str]:
|
|
149
|
+
"""The Dockerfile's first lines: the frontend, what built it, and its base.
|
|
150
|
+
|
|
151
|
+
For a `dockerfile` source (E3-03) the base is the author's own
|
|
152
|
+
Dockerfile, its `FROM` pinned to the digest the resolver chose; the
|
|
153
|
+
contract's lines follow it, whatever came before them.
|
|
154
|
+
"""
|
|
155
|
+
lines = [
|
|
156
|
+
f"# syntax={DOCKERFILE_FRONTEND}",
|
|
157
|
+
f"# Generated by Datalayer for {request.environment.metadata.name} "
|
|
158
|
+
f"v{request.version}, build {request.build_uid}.",
|
|
159
|
+
f"# Lock: {request.lock_digest}",
|
|
160
|
+
]
|
|
161
|
+
if authored is not None:
|
|
162
|
+
lines.append(pin_dockerfile_base(authored.content, request.resolved_base).rstrip("\n"))
|
|
163
|
+
else:
|
|
164
|
+
lines.append(f"FROM {request.resolved_base}")
|
|
165
|
+
return lines
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _pip_lock_command(find_links: str, *, authored: bool) -> str:
|
|
169
|
+
"""Install the pip lock: `sync` to it, or `install` it over an authored Dockerfile.
|
|
170
|
+
|
|
171
|
+
`sync` removes whatever the lock does not name, and a `dockerfile`
|
|
172
|
+
source's lock does not name what the Dockerfile itself installed
|
|
173
|
+
(`DOCKERFILE_COVERAGE`). The lock's pins, the protected ones included,
|
|
174
|
+
win either way.
|
|
175
|
+
"""
|
|
176
|
+
verb, target = ("install", "-r ") if authored else ("sync", "")
|
|
177
|
+
return (
|
|
178
|
+
f"uv pip {verb} --system --require-hashes --find-links {find_links} "
|
|
179
|
+
f"{target}/opt/datalayer/lock.txt"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
148
183
|
class Builder:
|
|
149
184
|
"""The Datalayer variant: BuildKit into the owner's ECR repository.
|
|
150
185
|
|
|
@@ -224,7 +259,7 @@ class Builder:
|
|
|
224
259
|
# `image` resolves from an imported reference instead of an
|
|
225
260
|
# approved base (E3-04). Neither changes how this builder itself
|
|
226
261
|
# builds, once the resolver has done its part.
|
|
227
|
-
build_sources=("packages", "dependencyFile", "image"),
|
|
262
|
+
build_sources=("packages", "dependencyFile", "image", "dockerfile"),
|
|
228
263
|
package_managers=("uv", "pip"),
|
|
229
264
|
# Nothing is forbidden by the builder itself: BuildKit is the
|
|
230
265
|
# reference implementation of a Dockerfile, and what the contract
|
|
@@ -242,7 +277,10 @@ class Builder:
|
|
|
242
277
|
"""Whether this variant can build this spec, before anything is queued."""
|
|
243
278
|
findings: list[CapabilityFinding] = []
|
|
244
279
|
source = environment.spec.build.source
|
|
245
|
-
|
|
280
|
+
# The capability set is the one list of what this builder builds: a
|
|
281
|
+
# second copy here refused `dockerfile` on r1 after the capability
|
|
282
|
+
# set had learned it (E3-03, 2026-09-18).
|
|
283
|
+
if source not in self.capabilities().build_sources:
|
|
246
284
|
findings.append(
|
|
247
285
|
CapabilityFinding(
|
|
248
286
|
code=CAPABILITY_UNSUPPORTED.code,
|
|
@@ -301,12 +339,9 @@ class Builder:
|
|
|
301
339
|
"""
|
|
302
340
|
spec = request.environment.spec
|
|
303
341
|
apt = apt_pins_in(request.lock_text)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
f"v{request.version}, build {request.build_uid}.",
|
|
308
|
-
f"# Lock: {request.lock_digest}",
|
|
309
|
-
f"FROM {request.resolved_base}",
|
|
342
|
+
authored = spec.build.dockerfile if spec.build.source == "dockerfile" else None
|
|
343
|
+
lines = _head(request, authored)
|
|
344
|
+
lines += [
|
|
310
345
|
f'LABEL io.datalayer.environment="{request.environment.metadata.name}" \\',
|
|
311
346
|
f' io.datalayer.environment.uid="{request.environment_uid}" \\',
|
|
312
347
|
f' io.datalayer.version="{request.version}" \\',
|
|
@@ -385,8 +420,7 @@ class Builder:
|
|
|
385
420
|
# `--find-links` for what no index has — a protected pin's
|
|
386
421
|
# own wheel, the fork's local version above all (E1-04).
|
|
387
422
|
"RUN --mount=type=cache,target=/root/.cache/uv "
|
|
388
|
-
|
|
389
|
-
"/opt/datalayer/lock.txt",
|
|
423
|
+
+ _pip_lock_command(find_links, authored=authored is not None),
|
|
390
424
|
]
|
|
391
425
|
)
|
|
392
426
|
# A build secret is mounted on the postInstall commands that name it
|
{code_sandboxes-1.9.28 → code_sandboxes-1.9.30}/code_sandboxes/environments/adapters/daytona.py
RENAMED
|
@@ -102,7 +102,7 @@ from ..builders import (
|
|
|
102
102
|
CapabilityFinding,
|
|
103
103
|
ValidationResult,
|
|
104
104
|
)
|
|
105
|
-
from ..contract import SANDBOX_CONTRACT_V1
|
|
105
|
+
from ..contract import SANDBOX_CONTRACT_V1, pin_dockerfile_base
|
|
106
106
|
from ..errors import (
|
|
107
107
|
ARTIFACT_MISSING,
|
|
108
108
|
BUILD_FAILED,
|
|
@@ -148,6 +148,19 @@ _CPU_RESOURCES: dict[str, dict[str, int]] = {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
def _pip_lock_command(*, authored: bool) -> str:
|
|
152
|
+
"""Install the pip lock: `sync` to it, or `install` it over an authored Dockerfile.
|
|
153
|
+
|
|
154
|
+
`sync` would remove what the Dockerfile installed and the lock does not
|
|
155
|
+
name (E3-03, `DOCKERFILE_COVERAGE`).
|
|
156
|
+
"""
|
|
157
|
+
verb, target = ("install", "-r ") if authored else ("sync", "")
|
|
158
|
+
return (
|
|
159
|
+
f"uv pip {verb} --system --require-hashes "
|
|
160
|
+
f"--find-links {WHEELHOUSE_IMAGE_PATH} {target}{_LOCK_PATH}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
151
164
|
def _daytona_sdk() -> Any:
|
|
152
165
|
try:
|
|
153
166
|
import daytona
|
|
@@ -339,7 +352,8 @@ class Builder(ManagedBuilder):
|
|
|
339
352
|
lock_file = Path(scratch) / "lock.txt"
|
|
340
353
|
lock_file.write_text(request.lock_text, encoding="utf-8")
|
|
341
354
|
|
|
342
|
-
|
|
355
|
+
authored = spec.build.dockerfile if spec.build.source == "dockerfile" else None
|
|
356
|
+
image = self._starting_image(sdk, request, authored, Path(scratch))
|
|
343
357
|
# `env` before anything installs, the same order the
|
|
344
358
|
# Datalayer and E2B builders keep: a package that compiles
|
|
345
359
|
# against a library found through an env var behaves
|
|
@@ -394,10 +408,7 @@ class Builder(ManagedBuilder):
|
|
|
394
408
|
# Datalayer and E2B builders give: a user install lands
|
|
395
409
|
# under the content directory's own home, which the
|
|
396
410
|
# runtime mounts over.
|
|
397
|
-
image = image.run_commands(
|
|
398
|
-
"uv pip sync --system --require-hashes "
|
|
399
|
-
f"--find-links {WHEELHOUSE_IMAGE_PATH} {_LOCK_PATH}"
|
|
400
|
-
)
|
|
411
|
+
image = image.run_commands(_pip_lock_command(authored=authored is not None))
|
|
401
412
|
image = image.dockerfile_commands([f"USER 1000:100\nWORKDIR {_CONTENT_DIR}"])
|
|
402
413
|
for command in files_step(request.environment, variant=self.variant):
|
|
403
414
|
image = image.run_commands(command)
|
|
@@ -477,6 +488,23 @@ class Builder(ManagedBuilder):
|
|
|
477
488
|
contract_version=spec.contract or SANDBOX_CONTRACT_V1.version,
|
|
478
489
|
)
|
|
479
490
|
|
|
491
|
+
@staticmethod
|
|
492
|
+
def _starting_image(sdk: Any, request: BuildRequest, authored: Any, scratch: Path) -> Any:
|
|
493
|
+
"""The image the chain starts from: the base, or the author's Dockerfile on it.
|
|
494
|
+
|
|
495
|
+
A Dockerfile source (E3-03) starts from the author's own Dockerfile,
|
|
496
|
+
its base pinned to the digest the resolver chose — which is also what
|
|
497
|
+
the build's registry entry lets Daytona pull. The contract's steps are
|
|
498
|
+
chained after it either way.
|
|
499
|
+
"""
|
|
500
|
+
if authored is None:
|
|
501
|
+
return sdk.Image.base(request.resolved_base)
|
|
502
|
+
dockerfile = scratch / "Dockerfile"
|
|
503
|
+
dockerfile.write_text(
|
|
504
|
+
pin_dockerfile_base(authored.content, request.resolved_base), encoding="utf-8"
|
|
505
|
+
)
|
|
506
|
+
return sdk.Image.from_dockerfile(str(dockerfile))
|
|
507
|
+
|
|
480
508
|
def _resources(self, sdk: Any, size_class: str) -> Any:
|
|
481
509
|
"""The CPU resources a size class bakes into the snapshot (§11.3 item 4)."""
|
|
482
510
|
shape = _CPU_RESOURCES.get(size_class, _CPU_RESOURCES["small"])
|
|
@@ -24,13 +24,13 @@ import argparse
|
|
|
24
24
|
import re
|
|
25
25
|
import shlex
|
|
26
26
|
import sys
|
|
27
|
-
from collections.abc import Sequence
|
|
27
|
+
from collections.abc import Mapping, Sequence
|
|
28
28
|
from dataclasses import dataclass
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
|
|
31
31
|
from pydantic import BaseModel, ConfigDict
|
|
32
32
|
|
|
33
|
-
from .bases import is_approved_repository
|
|
33
|
+
from .bases import APPROVED_BASES, ApprovedBase, is_approved_repository
|
|
34
34
|
from .errors import CAPABILITY_UNSUPPORTED, SPEC_INVALID, EnvironmentsError
|
|
35
35
|
|
|
36
36
|
__all__ = [
|
|
@@ -43,14 +43,18 @@ __all__ = [
|
|
|
43
43
|
"BuildContextEntry",
|
|
44
44
|
"BuildContextFinding",
|
|
45
45
|
"ContractRow",
|
|
46
|
+
"DockerfileBase",
|
|
46
47
|
"DockerfileFinding",
|
|
47
48
|
"DockerfileInstruction",
|
|
48
49
|
"SandboxContract",
|
|
49
50
|
"check_build_context",
|
|
50
51
|
"check_dockerfile",
|
|
51
52
|
"contract_markdown",
|
|
53
|
+
"dockerfile_base",
|
|
54
|
+
"dockerfile_findings_for_build",
|
|
52
55
|
"get_contract",
|
|
53
56
|
"parse_dockerfile",
|
|
57
|
+
"pin_dockerfile_base",
|
|
54
58
|
"validate_build_context",
|
|
55
59
|
"validate_dockerfile",
|
|
56
60
|
]
|
|
@@ -461,6 +465,188 @@ def check_dockerfile(text: str, *, contract: SandboxContract = SANDBOX_CONTRACT_
|
|
|
461
465
|
)
|
|
462
466
|
|
|
463
467
|
|
|
468
|
+
# --- A Dockerfile source's base, and how it is built (E3-03) -----------------------------
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@dataclass(frozen=True)
|
|
472
|
+
class DockerfileBase:
|
|
473
|
+
"""The approved base a Dockerfile builds on: its channel, and the `FROM` lines naming it."""
|
|
474
|
+
|
|
475
|
+
ref: str
|
|
476
|
+
channel: str
|
|
477
|
+
lines: tuple[int, ...]
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@dataclass(frozen=True)
|
|
481
|
+
class _ApprovedFrom:
|
|
482
|
+
line: int
|
|
483
|
+
image: str
|
|
484
|
+
ref: str
|
|
485
|
+
tag: str
|
|
486
|
+
digest: str
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _from_image(instruction: DockerfileInstruction) -> tuple[str, str | None]:
|
|
490
|
+
"""A `FROM`'s image and its stage name, flags skipped."""
|
|
491
|
+
tokens = _tokens(instruction.arguments)
|
|
492
|
+
for index, argument in enumerate(tokens):
|
|
493
|
+
if argument.startswith("--"):
|
|
494
|
+
continue
|
|
495
|
+
rest = tokens[index + 1 :]
|
|
496
|
+
alias = rest[1].lower() if len(rest) >= 2 and rest[0].upper() == "AS" else None
|
|
497
|
+
return argument, alias
|
|
498
|
+
return "", None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _approved_froms(text: str, bases: Mapping[str, ApprovedBase]) -> list[_ApprovedFrom]:
|
|
502
|
+
"""Every `FROM` naming an approved base, in order; a stage name is not a base."""
|
|
503
|
+
stages: set[str] = set()
|
|
504
|
+
found: list[_ApprovedFrom] = []
|
|
505
|
+
for instruction in parse_dockerfile(text):
|
|
506
|
+
if instruction.keyword != "FROM":
|
|
507
|
+
continue
|
|
508
|
+
image, alias = _from_image(instruction)
|
|
509
|
+
if image and image.lower() not in stages and "$" not in image:
|
|
510
|
+
repository, _, digest = image.partition("@")
|
|
511
|
+
last = repository.rsplit("/", 1)[-1]
|
|
512
|
+
name, _, tag = last.partition(":")
|
|
513
|
+
repository = repository[: len(repository) - len(last)] + name
|
|
514
|
+
for base in bases.values():
|
|
515
|
+
if any(
|
|
516
|
+
repository == known or repository.endswith("/" + known)
|
|
517
|
+
for known in (base.ref, base.repository)
|
|
518
|
+
):
|
|
519
|
+
found.append(_ApprovedFrom(instruction.line, image, base.ref, tag, digest))
|
|
520
|
+
break
|
|
521
|
+
if alias:
|
|
522
|
+
stages.add(alias)
|
|
523
|
+
return found
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def dockerfile_findings_for_build(
|
|
527
|
+
text: str, bases: Mapping[str, ApprovedBase] = APPROVED_BASES
|
|
528
|
+
) -> list[DockerfileFinding]:
|
|
529
|
+
"""What a Dockerfile source must also be to be built, beyond what the contract allows.
|
|
530
|
+
|
|
531
|
+
- **One base, named by its channel.** Every `FROM` of an approved base
|
|
532
|
+
names the same base, and its channel as the tag
|
|
533
|
+
(`datalayer/python-cpu:2026.09`): the build pins that channel to its
|
|
534
|
+
digest, the way a `packages` source's base is pinned (D-9), and the
|
|
535
|
+
lock is solved in it. A digest is refused rather than trusted — which
|
|
536
|
+
channel it belongs to is what the build must know.
|
|
537
|
+
- **No build context yet.** A `COPY` or `ADD` from the context needs the
|
|
538
|
+
upload this item also describes, which is not taken yet; without it
|
|
539
|
+
the file is absent and the build fails halfway. From another stage
|
|
540
|
+
(`--from=`), from a heredoc, and `ADD` of a URL need none.
|
|
541
|
+
- **The default escape character.** The build appends its own lines,
|
|
542
|
+
continued with a backslash.
|
|
543
|
+
"""
|
|
544
|
+
findings: list[DockerfileFinding] = []
|
|
545
|
+
for raw in text.splitlines():
|
|
546
|
+
directive = _DIRECTIVE.match(raw.strip())
|
|
547
|
+
if not directive:
|
|
548
|
+
break
|
|
549
|
+
if directive.group(1).lower() == "escape" and directive.group(2) != "\\":
|
|
550
|
+
findings.append(
|
|
551
|
+
DockerfileFinding(
|
|
552
|
+
1, "escape", "keep the default escape character: the build appends lines to it"
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
named: list[_ApprovedFrom] = []
|
|
556
|
+
for found in _approved_froms(text, bases):
|
|
557
|
+
channels = bases[found.ref].channels
|
|
558
|
+
if found.digest:
|
|
559
|
+
message = f"name `{found.ref}` by its channel, not a digest: the build pins the channel"
|
|
560
|
+
elif not found.tag:
|
|
561
|
+
example = next(iter(channels), "2026.09")
|
|
562
|
+
message = f"name `{found.ref}`'s channel as its tag, such as `{found.ref}:{example}`"
|
|
563
|
+
elif found.tag not in channels:
|
|
564
|
+
message = f"`{found.ref}` has no channel `{found.tag}`; channels: " + (
|
|
565
|
+
", ".join(channels) or "none"
|
|
566
|
+
)
|
|
567
|
+
else:
|
|
568
|
+
named.append(found)
|
|
569
|
+
continue
|
|
570
|
+
findings.append(DockerfileFinding(found.line, "FROM", message))
|
|
571
|
+
if len({(found.ref, found.tag) for found in named}) > 1:
|
|
572
|
+
findings.append(
|
|
573
|
+
DockerfileFinding(
|
|
574
|
+
named[1].line,
|
|
575
|
+
"FROM",
|
|
576
|
+
"every approved base is the same base and channel: the build pins one",
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
for instruction in parse_dockerfile(text):
|
|
580
|
+
if instruction.keyword not in ("COPY", "ADD"):
|
|
581
|
+
continue
|
|
582
|
+
tokens = _tokens(instruction.arguments)
|
|
583
|
+
if "<<" in instruction.arguments or any(t.startswith("--from=") for t in tokens):
|
|
584
|
+
continue
|
|
585
|
+
sources = [t for t in tokens if not t.startswith("--")][:-1]
|
|
586
|
+
local = [s for s in sources if not re.match(r"^(https?|git)://|^git@", s, re.IGNORECASE)]
|
|
587
|
+
if local:
|
|
588
|
+
findings.append(
|
|
589
|
+
DockerfileFinding(
|
|
590
|
+
instruction.line,
|
|
591
|
+
instruction.keyword,
|
|
592
|
+
f"copies `{local[0]}` from the build context, which this deployment does not "
|
|
593
|
+
"take yet: bake it with a `RUN` or a heredoc, or copy it from another stage",
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
return sorted(findings, key=lambda finding: finding.line)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def dockerfile_base(
|
|
600
|
+
text: str, bases: Mapping[str, ApprovedBase] = APPROVED_BASES
|
|
601
|
+
) -> DockerfileBase:
|
|
602
|
+
"""The approved base a Dockerfile source is built on, or a refusal naming the line."""
|
|
603
|
+
refused = [
|
|
604
|
+
finding
|
|
605
|
+
for finding in dockerfile_findings_for_build(text, bases)
|
|
606
|
+
if finding.instruction in ("FROM", "escape")
|
|
607
|
+
]
|
|
608
|
+
if refused:
|
|
609
|
+
raise EnvironmentsError(
|
|
610
|
+
SPEC_INVALID,
|
|
611
|
+
f"line {refused[0].line}: {refused[0].message}",
|
|
612
|
+
detail={"field": "spec.build.dockerfile", "line": refused[0].line},
|
|
613
|
+
)
|
|
614
|
+
found = _approved_froms(text, bases)
|
|
615
|
+
if not found:
|
|
616
|
+
raise EnvironmentsError(
|
|
617
|
+
SPEC_INVALID,
|
|
618
|
+
"the Dockerfile builds on no approved base",
|
|
619
|
+
detail={"field": "spec.build.dockerfile"},
|
|
620
|
+
)
|
|
621
|
+
return DockerfileBase(
|
|
622
|
+
ref=found[0].ref,
|
|
623
|
+
channel=found[0].tag,
|
|
624
|
+
lines=tuple(item.line for item in found),
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def pin_dockerfile_base(
|
|
629
|
+
text: str, reference: str, bases: Mapping[str, ApprovedBase] = APPROVED_BASES
|
|
630
|
+
) -> str:
|
|
631
|
+
"""The Dockerfile as it is built: each `FROM` of its base pinned to `reference`.
|
|
632
|
+
|
|
633
|
+
Only the image of those lines changes; comments, the author's own
|
|
634
|
+
continuations and the stage names stay as written. The parser directives
|
|
635
|
+
go, because the build states its own frontend first (`# syntax=`), and a
|
|
636
|
+
directive anywhere but the very top is only a comment.
|
|
637
|
+
"""
|
|
638
|
+
images = {found.line: found.image for found in _approved_froms(text, bases)}
|
|
639
|
+
rewritten: list[str] = []
|
|
640
|
+
in_directives = True
|
|
641
|
+
for number, raw in enumerate(text.splitlines(), start=1):
|
|
642
|
+
if in_directives and _DIRECTIVE.match(raw.strip()):
|
|
643
|
+
continue
|
|
644
|
+
in_directives = False
|
|
645
|
+
image = images.get(number)
|
|
646
|
+
rewritten.append(raw.replace(image, reference, 1) if image else raw)
|
|
647
|
+
return "\n".join(rewritten) + "\n"
|
|
648
|
+
|
|
649
|
+
|
|
464
650
|
# --- The build context (E3-03) -------------------------------------------------------
|
|
465
651
|
|
|
466
652
|
#: A `dockerfile` source uploads a build context to object storage. These bound
|
|
@@ -515,9 +701,7 @@ def validate_build_context(
|
|
|
515
701
|
elif ".." in components:
|
|
516
702
|
findings.append(BuildContextFinding(path, "escapes the context with `..`"))
|
|
517
703
|
if entry.is_symlink:
|
|
518
|
-
findings.append(
|
|
519
|
-
BuildContextFinding(path, "is a symlink, which could read a host file")
|
|
520
|
-
)
|
|
704
|
+
findings.append(BuildContextFinding(path, "is a symlink, which could read a host file"))
|
|
521
705
|
if entry.size_bytes > MAX_CONTEXT_FILE_BYTES:
|
|
522
706
|
findings.append(
|
|
523
707
|
BuildContextFinding(
|
|
@@ -526,9 +710,7 @@ def validate_build_context(
|
|
|
526
710
|
)
|
|
527
711
|
total += entry.size_bytes
|
|
528
712
|
if len(entries) > MAX_CONTEXT_FILES:
|
|
529
|
-
findings.append(
|
|
530
|
-
BuildContextFinding("", f"has more than {MAX_CONTEXT_FILES} files")
|
|
531
|
-
)
|
|
713
|
+
findings.append(BuildContextFinding("", f"has more than {MAX_CONTEXT_FILES} files"))
|
|
532
714
|
if total > MAX_CONTEXT_TOTAL_BYTES:
|
|
533
715
|
findings.append(
|
|
534
716
|
BuildContextFinding("", f"is over the {MAX_CONTEXT_TOTAL_BYTES}-byte total limit")
|
|
@@ -83,6 +83,8 @@ __all__ = [
|
|
|
83
83
|
"APT_PIN_PREFIX",
|
|
84
84
|
"APT_SNAPSHOT_PREFIX",
|
|
85
85
|
"CONSTRAINTS_PATH",
|
|
86
|
+
"COVERAGE_PREFIX",
|
|
87
|
+
"DOCKERFILE_COVERAGE",
|
|
86
88
|
"LOCK_FORMAT",
|
|
87
89
|
"PROTECTED_PIN_PREFIX",
|
|
88
90
|
"BuildkitResolveRunner",
|
|
@@ -129,6 +131,13 @@ WHEELHOUSE_PATH = Path(__file__).parent / "constraints" / "wheelhouse"
|
|
|
129
131
|
#: How an apt pin is written in the lock. A comment, so every reader of a
|
|
130
132
|
#: ``pip`` requirements file — the CLI's diff included — ignores it.
|
|
131
133
|
APT_PIN_PREFIX = "# datalayer-apt: "
|
|
134
|
+
#: What a lock says it does not cover, as a header line.
|
|
135
|
+
COVERAGE_PREFIX = "# datalayer-coverage: "
|
|
136
|
+
#: A `dockerfile` source's lock (E3-03): what the resolver saw, and no more.
|
|
137
|
+
DOCKERFILE_COVERAGE = (
|
|
138
|
+
"the declared packages and Datalayer's protected pins only; what the Dockerfile's own "
|
|
139
|
+
"instructions install is not locked, so a rebuild may install different versions of it"
|
|
140
|
+
)
|
|
132
141
|
#: The Ubuntu snapshot the apt pins were taken from, as the lock records it.
|
|
133
142
|
#: The builder installs the pins from the same snapshot, since a pinned
|
|
134
143
|
#: version can leave the live mirror (D-9).
|
|
@@ -889,6 +898,7 @@ def lock_document(
|
|
|
889
898
|
python_version: str,
|
|
890
899
|
base_reference: str,
|
|
891
900
|
merged: MergedRequirements,
|
|
901
|
+
coverage: str | None = None,
|
|
892
902
|
) -> dict[str, Any]:
|
|
893
903
|
"""The stored lock: its text, its digest, and what a reader needs from it.
|
|
894
904
|
|
|
@@ -912,6 +922,10 @@ def lock_document(
|
|
|
912
922
|
f"# python: {python_version}",
|
|
913
923
|
f"# base: {base_reference}",
|
|
914
924
|
]
|
|
925
|
+
if coverage:
|
|
926
|
+
# What this lock does not cover, in the lock itself: it travels with
|
|
927
|
+
# the version, and a reader of the lock is who needs to know (E3-03).
|
|
928
|
+
header.append(f"{COVERAGE_PREFIX}{coverage}")
|
|
915
929
|
for pin in outcome.apt_pins.items():
|
|
916
930
|
header.append(f"{APT_PIN_PREFIX}{pin[0]}={pin[1]}")
|
|
917
931
|
if outcome.apt_pins and outcome.apt_source:
|
|
@@ -938,6 +952,9 @@ def resolve_bases(
|
|
|
938
952
|
variants: Sequence[str],
|
|
939
953
|
bases: dict[str, ApprovedBase] = APPROVED_BASES,
|
|
940
954
|
registry: str | None = None,
|
|
955
|
+
*,
|
|
956
|
+
ref: str | None = None,
|
|
957
|
+
channel: str | None = None,
|
|
941
958
|
) -> dict[str, str]:
|
|
942
959
|
"""Each variant's base, pinned by digest (D-9, §4).
|
|
943
960
|
|
|
@@ -947,15 +964,15 @@ def resolve_bases(
|
|
|
947
964
|
Hub. Bare ``<repository>@sha256:…`` otherwise, which is what every test
|
|
948
965
|
and fixture that never passes a credential still gets.
|
|
949
966
|
"""
|
|
950
|
-
|
|
951
|
-
|
|
967
|
+
ref = ref or environment.spec.base.ref
|
|
968
|
+
channel = channel or environment.spec.base.channel
|
|
969
|
+
base = bases.get(ref)
|
|
970
|
+
repository = base.repository if base is not None else ref
|
|
952
971
|
if registry:
|
|
953
972
|
repository = f"{registry}/{repository}"
|
|
954
973
|
resolved: dict[str, str] = {}
|
|
955
974
|
for variant in variants:
|
|
956
|
-
digest = resolve_base(
|
|
957
|
-
environment.spec.base.ref, environment.spec.base.channel, variant, bases
|
|
958
|
-
)
|
|
975
|
+
digest = resolve_base(ref, channel, variant, bases)
|
|
959
976
|
resolved[variant] = f"{repository}@{digest}"
|
|
960
977
|
return resolved
|
|
961
978
|
|
|
@@ -1238,13 +1255,15 @@ def resolve_environment(
|
|
|
1238
1255
|
environment = parse_environment(spec)
|
|
1239
1256
|
python = environment.spec.packages.python
|
|
1240
1257
|
source = environment.spec.build.source
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1258
|
+
# A Dockerfile names its own base in `FROM` (E3-03): that is what is
|
|
1259
|
+
# pinned and solved in, never `spec.base`, which the schema still asks for.
|
|
1260
|
+
dockerfile = environment.spec.build.dockerfile if source == "dockerfile" else None
|
|
1261
|
+
base_ref, base_channel = environment.spec.base.ref, environment.spec.base.channel
|
|
1262
|
+
if dockerfile is not None:
|
|
1263
|
+
from .contract import dockerfile_base
|
|
1264
|
+
|
|
1265
|
+
named = dockerfile_base(dockerfile.content, bases)
|
|
1266
|
+
base_ref, base_channel = named.ref, named.channel
|
|
1248
1267
|
if python.manager == "conda":
|
|
1249
1268
|
raise EnvironmentsError(
|
|
1250
1269
|
CAPABILITY_UNSUPPORTED,
|
|
@@ -1262,7 +1281,14 @@ def resolve_environment(
|
|
|
1262
1281
|
resolve_secret=resolve_secret,
|
|
1263
1282
|
)
|
|
1264
1283
|
if source == "image"
|
|
1265
|
-
else resolve_bases(
|
|
1284
|
+
else resolve_bases(
|
|
1285
|
+
environment,
|
|
1286
|
+
wanted,
|
|
1287
|
+
bases,
|
|
1288
|
+
registry=_registry_of(credential),
|
|
1289
|
+
ref=base_ref,
|
|
1290
|
+
channel=base_channel,
|
|
1291
|
+
)
|
|
1266
1292
|
)
|
|
1267
1293
|
dependency_file = environment.spec.build.dependency_file
|
|
1268
1294
|
if source == "dependencyFile" and dependency_file is not None:
|
|
@@ -1312,11 +1338,7 @@ def resolve_environment(
|
|
|
1312
1338
|
registry_auth=_registry_auth(credential),
|
|
1313
1339
|
bootstrap_uv=(source == "image"),
|
|
1314
1340
|
# An imported image is not an approved base, and names no channel.
|
|
1315
|
-
apt_snapshot=(
|
|
1316
|
-
""
|
|
1317
|
-
if source == "image"
|
|
1318
|
-
else channel_snapshot(environment.spec.base.ref, environment.spec.base.channel, bases)
|
|
1319
|
-
),
|
|
1341
|
+
apt_snapshot=("" if source == "image" else channel_snapshot(base_ref, base_channel, bases)),
|
|
1320
1342
|
)
|
|
1321
1343
|
outcome = (runner or BuildkitResolveRunner()).solve(request, say)
|
|
1322
1344
|
document = lock_document(
|
|
@@ -1324,6 +1346,7 @@ def resolve_environment(
|
|
|
1324
1346
|
python_version=environment.spec.language.version,
|
|
1325
1347
|
base_reference=solving_in,
|
|
1326
1348
|
merged=merged,
|
|
1349
|
+
coverage=DOCKERFILE_COVERAGE if dockerfile is not None else None,
|
|
1327
1350
|
)
|
|
1328
1351
|
say(
|
|
1329
1352
|
f"Locked {document['package_count']} packages"
|
|
@@ -535,11 +535,23 @@ def _dockerfile_findings(dockerfile: DockerfileSpec | None) -> list[SpecFinding]
|
|
|
535
535
|
return [SpecFinding(field, "is required when `spec.build.source` is `dockerfile`")]
|
|
536
536
|
if not dockerfile.content.strip():
|
|
537
537
|
return [SpecFinding(f"{field}.content", "is empty; it is the Dockerfile text")]
|
|
538
|
-
from .contract import validate_dockerfile
|
|
539
|
-
|
|
538
|
+
from .contract import dockerfile_findings_for_build, validate_dockerfile
|
|
539
|
+
|
|
540
|
+
# The contract's refusals, then what building it needs on top (E3-03):
|
|
541
|
+
# one approved base named by its channel, and no build context yet. The
|
|
542
|
+
# second set is not the contract's — a variant could honour both — so it
|
|
543
|
+
# is its own list, and a line the contract already refused is not named
|
|
544
|
+
# twice.
|
|
545
|
+
findings = validate_dockerfile(dockerfile.content)
|
|
546
|
+
refused = {finding.line for finding in findings}
|
|
547
|
+
findings += [
|
|
548
|
+
finding
|
|
549
|
+
for finding in dockerfile_findings_for_build(dockerfile.content)
|
|
550
|
+
if finding.line not in refused
|
|
551
|
+
]
|
|
540
552
|
return [
|
|
541
553
|
SpecFinding(f"{field}.content", f"line {finding.line}: {finding.message}")
|
|
542
|
-
for finding in
|
|
554
|
+
for finding in sorted(findings, key=lambda finding: finding.line)
|
|
543
555
|
]
|
|
544
556
|
|
|
545
557
|
|
|
@@ -429,11 +429,6 @@ class TestWhatItSaysItCannotBuild:
|
|
|
429
429
|
assert any("modal or daytona" in finding.message for finding in report.findings)
|
|
430
430
|
assert all(finding.field == "spec.resources.sizeClass" for finding in report.findings)
|
|
431
431
|
|
|
432
|
-
def test_a_form_that_is_not_built_yet_is_a_finding(self) -> None:
|
|
433
|
-
request = a_request(spec={"build": {"source": "dockerfile"}})
|
|
434
|
-
report = a_builder().validate(request.environment)
|
|
435
|
-
assert [finding.field for finding in report.findings] == ["spec.build.source"]
|
|
436
|
-
|
|
437
432
|
def test_a_dependency_file_source_is_supported(self) -> None:
|
|
438
433
|
"""E3-01 resolves it the same way `packages` does; this builder never
|
|
439
434
|
refused it on its own — until this box, its own `validate` still did."""
|
|
@@ -475,7 +470,7 @@ class TestWhatItSaysItCannotBuild:
|
|
|
475
470
|
def test_this_variant_has_no_gpu_yet(self) -> None:
|
|
476
471
|
capabilities = a_builder().capabilities()
|
|
477
472
|
assert capabilities.supports_gpu is False
|
|
478
|
-
assert capabilities.build_sources == ("packages", "dependencyFile", "image")
|
|
473
|
+
assert capabilities.build_sources == ("packages", "dependencyFile", "image", "dockerfile")
|
|
479
474
|
assert capabilities.package_managers == ("uv", "pip")
|
|
480
475
|
|
|
481
476
|
|
|
@@ -114,6 +114,11 @@ class FakeImage:
|
|
|
114
114
|
def base(cls, ref: str) -> FakeImage:
|
|
115
115
|
return cls([Call("base", (ref,))])
|
|
116
116
|
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_dockerfile(cls, path: str) -> FakeImage:
|
|
119
|
+
"""The text is read now: the builder's scratch file is gone by the time a test looks."""
|
|
120
|
+
return cls([Call("from_dockerfile", (path,), {"content": Path(path).read_text()})])
|
|
121
|
+
|
|
117
122
|
def env(self, env_vars: dict[str, str]) -> FakeImage:
|
|
118
123
|
self.calls.append(Call("env", (env_vars,)))
|
|
119
124
|
return self
|