code-sandboxes 1.9.3__tar.gz → 1.9.5__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.3 → code_sandboxes-1.9.5}/CHANGELOG.md +56 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/PKG-INFO +1 -1
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/datalayer.py +14 -4
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/modal.py +102 -48
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/attest.py +95 -7
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/bases.py +28 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/image_import.py +89 -39
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/policy.py +22 -1
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/resolve.py +108 -22
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_attest.py +121 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_bases.py +22 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_datalayer_builder.py +11 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_image_import.py +85 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_managed_builders.py +12 -10
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_modal_builder.py +92 -25
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_resolve.py +101 -6
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.gitignore +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/Dockerfile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/LICENSE +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/Makefile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/README.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/build.log +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/daytona.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/spec.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/Makefile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/README.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/package.json +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/README.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/pyproject.toml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/schemas/environment-v1alpha1.json +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_daytona_builder.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_environment_spec.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/tests/test_the_kill_relaunch_drill.py +0 -0
|
@@ -8,6 +8,62 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 1.9.5
|
|
12
|
+
|
|
13
|
+
- **Attest waits for a just-pushed image's scan to exist**
|
|
14
|
+
(`environments.attest`; PLAN_ENV.md E1-08). Enhanced scanning starts after
|
|
15
|
+
the push, so the first answers for a new image are
|
|
16
|
+
`ScanNotFoundException`. Attest read that as an image with no scan and
|
|
17
|
+
failed the build at once (found live on r1, 2026-09-14). It is now waited
|
|
18
|
+
for like a running scan, within the same bound.
|
|
19
|
+
- **apt is pinned at, and installed from, the base channel's Ubuntu snapshot**
|
|
20
|
+
(`environments.bases`, `environments.resolve`,
|
|
21
|
+
`environments.adapters.datalayer`; PLAN_ENV.md E1-04). A channel records
|
|
22
|
+
the `snapshot.ubuntu.com` id its image was upgraded at. The solve runs
|
|
23
|
+
`apt-get` with `--snapshot` at that id, the lock records it
|
|
24
|
+
(`# datalayer-apt-snapshot:`), and the Datalayer builder installs its pins
|
|
25
|
+
from the same snapshot. This replaces a `deb` line that named only `main`
|
|
26
|
+
(`gdal-bin` is in `universe`) and left the live mirror enabled beside it.
|
|
27
|
+
`DATALAYER_APT_SNAPSHOT` must now be a snapshot id.
|
|
28
|
+
- **A private registry's credential is resolved and used** (`environments
|
|
29
|
+
.image_import`, `environments.resolve`; PLAN_ENV.md E3-04's second half).
|
|
30
|
+
`image.credentialSecretId` used to only clear the allowlist check; nothing
|
|
31
|
+
fetched or used the credential it named. It is now fetched through the
|
|
32
|
+
same IAM route a build secret is (E3-05), and sent as HTTP Basic on the
|
|
33
|
+
registry's own token exchange — the way `docker login` authenticates a
|
|
34
|
+
private pull — never to the registry named in the reference, and never
|
|
35
|
+
logged. `resolve_environment` and `resolve_image_base` take `owner_uid`
|
|
36
|
+
and `resolve_secret` to reach it; unused by a public registry or any other
|
|
37
|
+
source.
|
|
38
|
+
- **The Datalayer base channel's own five ESM-locked advisories are allowed**
|
|
39
|
+
(`environments.policy`; PLAN_ENV.md E1-08). Amazon Inspector marks all five
|
|
40
|
+
fixable, but the fix is an Ubuntu ESM package version a plain
|
|
41
|
+
`apt-get upgrade` cannot reach. Reviewed and added to `DEFAULT_POLICY
|
|
42
|
+
.allowed`, 2026-09-14, so they are recorded as allowed rather than missed,
|
|
43
|
+
and any other critical still blocks.
|
|
44
|
+
|
|
45
|
+
## 1.9.4
|
|
46
|
+
|
|
47
|
+
- **A pushed Environment image is decided by its linux/amd64 image's scan**
|
|
48
|
+
(`environments.attest`; PLAN_ENV.md E1-08). The Datalayer builder pushes
|
|
49
|
+
with SBOM and provenance attestations, so the digest it records is an OCI
|
|
50
|
+
image index. Amazon Inspector scans the image inside it and answers
|
|
51
|
+
`UNSUPPORTED_IMAGE` for the index itself, so every real build failed at
|
|
52
|
+
`attest` with `DL_ENV_PROVIDER_ERROR` (found live on r1, 2026-09-14, the
|
|
53
|
+
first build to get past the scan-read permission). The attestor now reads
|
|
54
|
+
the manifest and, for an index, reads the scan of its `linux/amd64` image,
|
|
55
|
+
skipping the attestation manifest. The signature stays on the index, which
|
|
56
|
+
is what a pod pulls.
|
|
57
|
+
- **Modal attaches a build secret to the `postInstall` steps that name it**
|
|
58
|
+
(`environments.adapters.modal`; PLAN_ENV.md E3-05). Modal used to refuse
|
|
59
|
+
every `buildSecrets` spec. Each declared secret is now resolved from IAM
|
|
60
|
+
before Modal is touched, made into a Modal Secret of the build's own,
|
|
61
|
+
passed as `secrets=` to exactly the `run_commands` steps whose command
|
|
62
|
+
names it, and deleted after the build like the base-reader secret. Its
|
|
63
|
+
value is redacted from the log and from a failed build's error. A
|
|
64
|
+
`mountAs: file` secret is refused, because Modal passes secrets as
|
|
65
|
+
environment variables and a file would land in a layer.
|
|
66
|
+
|
|
11
67
|
## 1.9.3
|
|
12
68
|
|
|
13
69
|
- **A build secret is mounted only on the `postInstall` commands that name it**
|
{code_sandboxes-1.9.3 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/datalayer.py
RENAMED
|
@@ -68,7 +68,13 @@ from ..errors import (
|
|
|
68
68
|
EnvironmentsError,
|
|
69
69
|
)
|
|
70
70
|
from ..files import files_step
|
|
71
|
-
from ..resolve import
|
|
71
|
+
from ..resolve import (
|
|
72
|
+
WHEELHOUSE_IMAGE_PATH,
|
|
73
|
+
WHEELHOUSE_PATH,
|
|
74
|
+
apt_pins_in,
|
|
75
|
+
apt_snapshot_in,
|
|
76
|
+
locked_versions,
|
|
77
|
+
)
|
|
72
78
|
from ..spec import BuildSecret, Environment, command_names_secret
|
|
73
79
|
|
|
74
80
|
__all__ = [
|
|
@@ -310,14 +316,18 @@ class Builder:
|
|
|
310
316
|
lines.append(f"ENV {name}={shlex.quote(spec.env[name])}")
|
|
311
317
|
if apt:
|
|
312
318
|
# Pinned, and from the lock: `apt-get install <name>` a month later
|
|
313
|
-
# is a different artifact.
|
|
319
|
+
# is a different artifact. From the snapshot the pins were taken
|
|
320
|
+
# from, when the lock names one: a pinned version can leave the
|
|
321
|
+
# live mirror (D-9).
|
|
314
322
|
pinned = " ".join(f"{name}={apt[name]}" for name in sorted(apt))
|
|
323
|
+
snapshot = apt_snapshot_in(request.lock_text)
|
|
324
|
+
option = f" --snapshot {snapshot}" if snapshot else ""
|
|
315
325
|
lines.extend(
|
|
316
326
|
[
|
|
317
327
|
"USER root",
|
|
318
328
|
"RUN --mount=type=cache,target=/var/cache/apt,sharing=locked "
|
|
319
|
-
"apt-get update -qq \\\n"
|
|
320
|
-
f" && apt-get install -y --no-install-recommends {pinned} \\\n"
|
|
329
|
+
f"apt-get update -qq{option} \\\n"
|
|
330
|
+
f" && apt-get install -y --no-install-recommends{option} {pinned} \\\n"
|
|
321
331
|
" && rm -rf /var/lib/apt/lists/*",
|
|
322
332
|
]
|
|
323
333
|
)
|
|
@@ -104,16 +104,16 @@ builder does not know the launch-time `gpu=` argument to hand a size class
|
|
|
104
104
|
either (D-20 leaves that to a later item; a GPU is Modal's own launch
|
|
105
105
|
option, not an image property, per section 11.4 item 10).
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
`run_commands
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
**A build secret is attached to the `postInstall` steps that name it (E3-05).**
|
|
108
|
+
`run_commands` takes a per-step `secrets=` collection, a mechanism E2B and
|
|
109
|
+
Daytona lack. Each declared secret is resolved from IAM
|
|
110
|
+
(`resolve_build_secret`) before Modal is touched, made into a Modal Secret of
|
|
111
|
+
this build's own, attached to exactly the `run_commands` steps whose command
|
|
112
|
+
names it (`command_names_secret`), and deleted after the build the way the
|
|
113
|
+
base-reader secret is. Its value is redacted from the build's log and from a
|
|
114
|
+
failed build's error. Modal hands a secret to a step only as environment
|
|
115
|
+
variables, so a `mountAs: file` secret is refused: making it a file would
|
|
116
|
+
write it into a layer.
|
|
117
117
|
|
|
118
118
|
@module code_sandboxes.environments.adapters.modal
|
|
119
119
|
"""
|
|
@@ -129,6 +129,7 @@ from pathlib import Path
|
|
|
129
129
|
from typing import Any
|
|
130
130
|
|
|
131
131
|
from ..accounts import provider_account
|
|
132
|
+
from ..build_secrets import resolve_build_secret
|
|
132
133
|
from ..builders import (
|
|
133
134
|
ArtifactMetadata,
|
|
134
135
|
ArtifactReference,
|
|
@@ -144,8 +145,9 @@ from ..errors import (
|
|
|
144
145
|
EnvironmentsError,
|
|
145
146
|
)
|
|
146
147
|
from ..files import files_step
|
|
148
|
+
from ..redact import redact
|
|
147
149
|
from ..resolve import WHEELHOUSE_IMAGE_PATH, apt_pins_in
|
|
148
|
-
from ..spec import GPU_SIZE_CLASSES, Environment
|
|
150
|
+
from ..spec import GPU_SIZE_CLASSES, BuildSecret, Environment, command_names_secret
|
|
149
151
|
from .managed import ManagedBuilder
|
|
150
152
|
|
|
151
153
|
__all__ = ["UNIMPLEMENTED_INSTRUCTIONS", "Builder"]
|
|
@@ -204,6 +206,21 @@ def _modal_internals() -> tuple[Any, Any]:
|
|
|
204
206
|
return synchronizer, api_pb2
|
|
205
207
|
|
|
206
208
|
|
|
209
|
+
def _scrubbed(text: str, values: dict[str, str]) -> str:
|
|
210
|
+
"""The text with this build's secret values redacted, or as it was with none (E3-05)."""
|
|
211
|
+
return redact(text, values.values()) if values else text
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _post_install(
|
|
215
|
+
image: Any, commands: list[str], declared: list[BuildSecret], step_secrets: dict[str, Any]
|
|
216
|
+
) -> Any:
|
|
217
|
+
"""Each `postInstall` command as its own step, with the secrets it names (E3-05)."""
|
|
218
|
+
for command in commands:
|
|
219
|
+
named = [step_secrets[s.id] for s in declared if command_names_secret(command, s)]
|
|
220
|
+
image = image.run_commands(command, secrets=named) if named else image.run_commands(command)
|
|
221
|
+
return image
|
|
222
|
+
|
|
223
|
+
|
|
207
224
|
class Builder(ManagedBuilder):
|
|
208
225
|
"""Modal: the capability half (E2-06) and the build (E2-05)."""
|
|
209
226
|
|
|
@@ -228,8 +245,12 @@ class Builder(ManagedBuilder):
|
|
|
228
245
|
modal_internals: Callable[[], tuple[Any, Any]] | None = None,
|
|
229
246
|
image_builder_version: str | None = None,
|
|
230
247
|
region: str | None = None,
|
|
248
|
+
resolve_secret: Callable[..., str] = resolve_build_secret,
|
|
231
249
|
) -> None:
|
|
232
250
|
super().__init__(log=log, credential=credential)
|
|
251
|
+
#: How one `BuildSecret`'s value is fetched (E3-05): IAM by default,
|
|
252
|
+
#: the same seam the Datalayer builder takes.
|
|
253
|
+
self._resolve_secret = resolve_secret
|
|
233
254
|
self._modal_sdk = modal_sdk or _modal_sdk
|
|
234
255
|
self._modal_internals = modal_internals or _modal_internals
|
|
235
256
|
self._image_builder_version = image_builder_version or _IMAGE_BUILDER_VERSION
|
|
@@ -297,24 +318,19 @@ class Builder(ManagedBuilder):
|
|
|
297
318
|
field=f"spec.commands.postInstall[{index}]",
|
|
298
319
|
)
|
|
299
320
|
)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
"per-step secrets could carry one, and resolve_build_secret (E3-05) can "
|
|
313
|
-
"resolve one, but nothing here connects them"
|
|
314
|
-
),
|
|
315
|
-
field="spec.buildSecrets",
|
|
321
|
+
for index, secret in enumerate(environment.spec.build_secrets):
|
|
322
|
+
if secret.mount_as == "file":
|
|
323
|
+
findings.append(
|
|
324
|
+
CapabilityFinding(
|
|
325
|
+
code="DL_ENV_CAPABILITY_UNSUPPORTED",
|
|
326
|
+
message=(
|
|
327
|
+
f"Modal gives `{secret.name}` to a step as an environment variable "
|
|
328
|
+
"only; a file would be written into a layer. Use `mountAs: env`, or "
|
|
329
|
+
"drop `modal` from the variants"
|
|
330
|
+
),
|
|
331
|
+
field=f"spec.buildSecrets[{index}].mountAs",
|
|
332
|
+
)
|
|
316
333
|
)
|
|
317
|
-
)
|
|
318
334
|
return findings
|
|
319
335
|
|
|
320
336
|
# -- Building -------------------------------------------------------------
|
|
@@ -340,20 +356,9 @@ class Builder(ManagedBuilder):
|
|
|
340
356
|
"needs is E2-17's, not built yet",
|
|
341
357
|
detail={"variant": self.variant, "missing": "E2-17"},
|
|
342
358
|
)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
# because nothing enforces that `build` is only ever called
|
|
347
|
-
# after a passing `validate` (found in review): a caller
|
|
348
|
-
# handing `build()` an already-resolved request could otherwise
|
|
349
|
-
# get a successful image with a required credential silently
|
|
350
|
-
# missing from it.
|
|
351
|
-
raise EnvironmentsError(
|
|
352
|
-
CAPABILITY_UNSUPPORTED,
|
|
353
|
-
"Modal's build secrets are not wired into this builder yet (E3-05 has landed, "
|
|
354
|
-
"but nothing here calls it)",
|
|
355
|
-
detail={"variant": self.variant, "missing": "E3-05"},
|
|
356
|
-
)
|
|
359
|
+
# Resolved before Modal is touched: a secret IAM will not give stops
|
|
360
|
+
# the build with nothing to clean up in the owner's workspace.
|
|
361
|
+
declared, values = self._resolved_secrets(request)
|
|
357
362
|
sdk = self._modal_sdk()
|
|
358
363
|
# Read from the process environment, not a per-call argument (see
|
|
359
364
|
# the module docstring) — set before anything else touches the SDK.
|
|
@@ -362,7 +367,12 @@ class Builder(ManagedBuilder):
|
|
|
362
367
|
name = f"dl-{request.environment.metadata.name}-v{request.version}-{request.build_uid}"
|
|
363
368
|
|
|
364
369
|
ecr_secret = self._ecr_secret(sdk, client)
|
|
370
|
+
step_secrets: dict[str, Any] = {}
|
|
365
371
|
try:
|
|
372
|
+
for secret in declared:
|
|
373
|
+
step_secrets[secret.id] = self._hydrated_secret(
|
|
374
|
+
sdk, client, {secret.name: values[secret.id]}, keep=step_secrets
|
|
375
|
+
)
|
|
366
376
|
app = sdk.App.lookup(
|
|
367
377
|
f"dl-{request.environment.metadata.name}", client=client, create_if_missing=True
|
|
368
378
|
)
|
|
@@ -404,8 +414,7 @@ class Builder(ManagedBuilder):
|
|
|
404
414
|
)
|
|
405
415
|
for command in files_step(request.environment, variant=self.variant):
|
|
406
416
|
image = image.run_commands(command)
|
|
407
|
-
|
|
408
|
-
image = image.run_commands(command)
|
|
417
|
+
image = _post_install(image, spec.commands.post_install, declared, step_secrets)
|
|
409
418
|
image = image.workdir(_CONTENT_DIR).entrypoint([_ENTRYPOINT_PATH])
|
|
410
419
|
|
|
411
420
|
logged: list[str] = []
|
|
@@ -423,13 +432,13 @@ class Builder(ManagedBuilder):
|
|
|
423
432
|
# that raised inside the `with` block above never
|
|
424
433
|
# reached the line that read it, so a failed build's
|
|
425
434
|
# own log was silently dropped from the error detail.
|
|
426
|
-
logged.extend(buffer.getvalue().splitlines())
|
|
435
|
+
logged.extend(_scrubbed(buffer.getvalue(), values).splitlines())
|
|
427
436
|
raise EnvironmentsError(
|
|
428
437
|
BUILD_FAILED,
|
|
429
|
-
f"The Modal build failed: {error}",
|
|
438
|
+
f"The Modal build failed: {_scrubbed(str(error), values)}",
|
|
430
439
|
detail={"variant": self.variant, "name": name, "log": logged[-20:]},
|
|
431
|
-
) from error
|
|
432
|
-
logged.extend(buffer.getvalue().splitlines())
|
|
440
|
+
) from (None if values else error)
|
|
441
|
+
logged.extend(_scrubbed(buffer.getvalue(), values).splitlines())
|
|
433
442
|
for line in logged:
|
|
434
443
|
self._log(line)
|
|
435
444
|
# A name for people and dashboards only (§11.4 item 3): moves
|
|
@@ -439,6 +448,8 @@ class Builder(ManagedBuilder):
|
|
|
439
448
|
# No standing credential is left in an account Datalayer does
|
|
440
449
|
# not control, whether the build above succeeded or not (D-18).
|
|
441
450
|
self._delete_secret(sdk, client, ecr_secret)
|
|
451
|
+
for secret in step_secrets.values():
|
|
452
|
+
self._delete_secret(sdk, client, secret)
|
|
442
453
|
|
|
443
454
|
return ArtifactReference(
|
|
444
455
|
variant=self.variant,
|
|
@@ -450,6 +461,25 @@ class Builder(ManagedBuilder):
|
|
|
450
461
|
contract_version=spec.contract or SANDBOX_CONTRACT_V1.version,
|
|
451
462
|
)
|
|
452
463
|
|
|
464
|
+
def _resolved_secrets(self, request: BuildRequest) -> tuple[list[BuildSecret], dict[str, str]]:
|
|
465
|
+
"""The build secrets this build attaches, and each one's value from IAM (E3-05).
|
|
466
|
+
|
|
467
|
+
`_own_findings` refuses a `mountAs: file` secret at `validate()`. This
|
|
468
|
+
guards it too, the same way `build` guards a GPU class, because
|
|
469
|
+
nothing makes a caller validate first.
|
|
470
|
+
"""
|
|
471
|
+
wanted = set(request.build_secret_ids)
|
|
472
|
+
declared = [s for s in request.environment.spec.build_secrets if s.id in wanted]
|
|
473
|
+
if any(secret.mount_as == "file" for secret in declared):
|
|
474
|
+
raise EnvironmentsError(
|
|
475
|
+
CAPABILITY_UNSUPPORTED,
|
|
476
|
+
"Modal gives a build secret to a step as an environment variable only; a "
|
|
477
|
+
"`mountAs: file` secret would be written into a layer",
|
|
478
|
+
detail={"variant": self.variant, "field": "spec.buildSecrets"},
|
|
479
|
+
)
|
|
480
|
+
values = {s.id: self._resolve_secret(s, owner_uid=request.owner_uid) for s in declared}
|
|
481
|
+
return declared, values
|
|
482
|
+
|
|
453
483
|
def _ecr_secret(self, sdk: Any, client: Any) -> Any | None:
|
|
454
484
|
"""A Modal Secret carrying this build's own base-reader credential (D-17, D-18).
|
|
455
485
|
|
|
@@ -489,6 +519,30 @@ class Builder(ManagedBuilder):
|
|
|
489
519
|
) from error
|
|
490
520
|
return secret
|
|
491
521
|
|
|
522
|
+
def _hydrated_secret(
|
|
523
|
+
self, sdk: Any, client: Any, env: dict[str, str], *, keep: dict[str, Any]
|
|
524
|
+
) -> Any:
|
|
525
|
+
"""A Modal Secret of this build's own for one build secret (E3-05).
|
|
526
|
+
|
|
527
|
+
Created in the owner's workspace like `_ecr_secret`'s, and deleted by
|
|
528
|
+
`build`'s own `finally`. A `hydrate` that fails is cleaned up here,
|
|
529
|
+
since `build` never got it back. The error names no value.
|
|
530
|
+
"""
|
|
531
|
+
secret = sdk.Secret.from_dict(env)
|
|
532
|
+
try:
|
|
533
|
+
secret.hydrate(client=client)
|
|
534
|
+
except Exception as error:
|
|
535
|
+
self._delete_secret(sdk, client, secret)
|
|
536
|
+
# `from None`: Modal's own error is not kept, in case it echoes
|
|
537
|
+
# what it was given. Its type is enough to tell failures apart.
|
|
538
|
+
raise EnvironmentsError(
|
|
539
|
+
PROVIDER_ERROR,
|
|
540
|
+
f"Modal could not be given the build secret `{next(iter(env))}` "
|
|
541
|
+
f"({type(error).__name__})",
|
|
542
|
+
detail={"variant": self.variant, "made": len(keep)},
|
|
543
|
+
) from None
|
|
544
|
+
return secret
|
|
545
|
+
|
|
492
546
|
def _delete_secret(self, sdk: Any, client: Any, secret: Any | None) -> None:
|
|
493
547
|
"""Delete the app-owned copy Modal stores of this build's own secret.
|
|
494
548
|
|
|
@@ -32,6 +32,7 @@ clock — so the whole chain is tested without AWS and without a daemon.
|
|
|
32
32
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
|
+
import json
|
|
35
36
|
import logging
|
|
36
37
|
import os
|
|
37
38
|
import re
|
|
@@ -68,6 +69,20 @@ SCAN_STATUSES = {
|
|
|
68
69
|
"refused": ("FAILED", "UNSUPPORTED_IMAGE", "FINDINGS_UNAVAILABLE"),
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
#: The platform an Environment image is built for: the image of an index
|
|
73
|
+
#: whose scan is read.
|
|
74
|
+
_SCANNED_PLATFORM = ("linux", "amd64")
|
|
75
|
+
|
|
76
|
+
#: What a registry answers for an image index, and for a single image.
|
|
77
|
+
_INDEX_MEDIA_TYPES = (
|
|
78
|
+
"application/vnd.oci.image.index.v1+json",
|
|
79
|
+
"application/vnd.docker.distribution.manifest.list.v2+json",
|
|
80
|
+
)
|
|
81
|
+
_MANIFEST_MEDIA_TYPES = (
|
|
82
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
83
|
+
"application/vnd.docker.distribution.manifest.v2+json",
|
|
84
|
+
)
|
|
85
|
+
|
|
71
86
|
#: How long a scan is waited for, and how often it is asked about.
|
|
72
87
|
DEFAULT_SCAN_TIMEOUT_SECONDS = 15 * 60
|
|
73
88
|
DEFAULT_SCAN_INTERVAL_SECONDS = 10.0
|
|
@@ -173,29 +188,34 @@ class Attestor:
|
|
|
173
188
|
— retryable, because nothing about the version is wrong — and so is a
|
|
174
189
|
scanner that refuses the image. A scan that finished is decided, and a
|
|
175
190
|
blocking decision raises `DL_ENV_SCAN_BLOCKED`.
|
|
191
|
+
|
|
192
|
+
An artifact that is an image index is decided by the scan of its
|
|
193
|
+
linux/amd64 image: see `_scanned_digest`.
|
|
176
194
|
"""
|
|
177
195
|
deadline = self._now() + self._timeout
|
|
196
|
+
scanned = self._scanned_digest(repository=repository, digest=digest)
|
|
197
|
+
where = {"repository": repository, "digest": digest, "scannedDigest": scanned}
|
|
178
198
|
waited = 0
|
|
179
199
|
while True:
|
|
180
|
-
status, described = self._describe(repository=repository, digest=
|
|
200
|
+
status, described = self._describe(repository=repository, digest=scanned)
|
|
181
201
|
if status in SCAN_STATUSES["done"]:
|
|
182
202
|
break
|
|
183
203
|
if status in SCAN_STATUSES["refused"]:
|
|
184
204
|
raise EnvironmentsError(
|
|
185
205
|
PROVIDER_ERROR,
|
|
186
206
|
f"The registry did not scan this artifact: {status}",
|
|
187
|
-
detail={
|
|
207
|
+
detail={**where, "scanStatus": status},
|
|
188
208
|
)
|
|
189
209
|
if self._now() >= deadline:
|
|
190
210
|
raise EnvironmentsError(
|
|
191
211
|
PROVIDER_ERROR,
|
|
192
212
|
f"The scan of this artifact did not finish within {self._timeout:.0f}s "
|
|
193
213
|
f"(last status {status or 'unknown'})",
|
|
194
|
-
detail={
|
|
214
|
+
detail={**where, "scanStatus": status},
|
|
195
215
|
)
|
|
196
216
|
waited += 1
|
|
197
217
|
if waited == 1:
|
|
198
|
-
self._log(f"Waiting for the scan of {
|
|
218
|
+
self._log(f"Waiting for the scan of {scanned}")
|
|
199
219
|
self._sleep(self._interval)
|
|
200
220
|
findings = findings_of(self._findings(described))
|
|
201
221
|
decision = decide(
|
|
@@ -211,6 +231,64 @@ class Attestor:
|
|
|
211
231
|
)
|
|
212
232
|
return decision
|
|
213
233
|
|
|
234
|
+
def _scanned_digest(self, *, repository: str, digest: str) -> str:
|
|
235
|
+
"""The digest whose scan decides this artifact: its own, or its linux/amd64 image's.
|
|
236
|
+
|
|
237
|
+
The Datalayer builder pushes with SBOM and provenance attestations, so
|
|
238
|
+
what it records is an OCI image index: the image, and an attestation
|
|
239
|
+
manifest beside it. Enhanced scanning scans the image and answers
|
|
240
|
+
`UNSUPPORTED_IMAGE` for the index (found live on r1, 2026-09-14), so the
|
|
241
|
+
scan read is that of the image the index names for linux/amd64. The
|
|
242
|
+
signature stays on the index, which is what a pod pulls.
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
answer = self._client().batch_get_image(
|
|
246
|
+
repositoryName=repository,
|
|
247
|
+
imageIds=[{"imageDigest": digest}],
|
|
248
|
+
acceptedMediaTypes=[*_INDEX_MEDIA_TYPES, *_MANIFEST_MEDIA_TYPES],
|
|
249
|
+
)
|
|
250
|
+
except Exception as error:
|
|
251
|
+
raise EnvironmentsError(
|
|
252
|
+
PROVIDER_ERROR,
|
|
253
|
+
f"The manifest of {digest} could not be read: {error}",
|
|
254
|
+
detail={"repository": repository, "digest": digest},
|
|
255
|
+
) from error
|
|
256
|
+
images = answer.get("images") or []
|
|
257
|
+
if not images:
|
|
258
|
+
raise EnvironmentsError(
|
|
259
|
+
PROVIDER_ERROR,
|
|
260
|
+
f"The registry has no image {digest}",
|
|
261
|
+
detail={"repository": repository, "digest": digest},
|
|
262
|
+
)
|
|
263
|
+
try:
|
|
264
|
+
manifest = json.loads(images[0].get("imageManifest") or "{}")
|
|
265
|
+
except ValueError:
|
|
266
|
+
manifest = {}
|
|
267
|
+
media_type = str(images[0].get("imageManifestMediaType") or manifest.get("mediaType") or "")
|
|
268
|
+
if media_type not in _INDEX_MEDIA_TYPES and "manifests" not in manifest:
|
|
269
|
+
return digest
|
|
270
|
+
os_name, architecture = _SCANNED_PLATFORM
|
|
271
|
+
for entry in manifest.get("manifests") or []:
|
|
272
|
+
annotations = entry.get("annotations") or {}
|
|
273
|
+
if annotations.get("vnd.docker.reference.type") == "attestation-manifest":
|
|
274
|
+
continue
|
|
275
|
+
platform = entry.get("platform") or {}
|
|
276
|
+
child = str(entry.get("digest") or "")
|
|
277
|
+
if (
|
|
278
|
+
platform.get("os") == os_name
|
|
279
|
+
and platform.get("architecture") == architecture
|
|
280
|
+
and _DIGEST.match(child)
|
|
281
|
+
):
|
|
282
|
+
self._log(
|
|
283
|
+
f"Reading the scan of {child}, the {os_name}/{architecture} image of {digest}"
|
|
284
|
+
)
|
|
285
|
+
return child
|
|
286
|
+
raise EnvironmentsError(
|
|
287
|
+
PROVIDER_ERROR,
|
|
288
|
+
f"The image index {digest} holds no {os_name}/{architecture} image to scan",
|
|
289
|
+
detail={"repository": repository, "digest": digest},
|
|
290
|
+
)
|
|
291
|
+
|
|
214
292
|
def _describe(self, *, repository: str, digest: str) -> tuple[str, dict[str, Any]]:
|
|
215
293
|
"""One `DescribeImageScanFindings`, as a status and what it answered."""
|
|
216
294
|
try:
|
|
@@ -452,10 +530,20 @@ def _is_missing(error: BaseException) -> bool:
|
|
|
452
530
|
return _error_code(error) in {
|
|
453
531
|
"ImageNotFoundException",
|
|
454
532
|
"RepositoryNotFoundException",
|
|
455
|
-
"ScanNotFoundException",
|
|
456
533
|
}
|
|
457
534
|
|
|
458
535
|
|
|
459
536
|
def _is_in_progress(error: BaseException) -> bool:
|
|
460
|
-
"""ECR answers a scan that has not finished as an error, not a status.
|
|
461
|
-
|
|
537
|
+
"""ECR answers a scan that has not finished as an error, not a status.
|
|
538
|
+
|
|
539
|
+
`ScanNotFoundException` is one of those answers: enhanced scanning starts
|
|
540
|
+
after the push, so an image pushed a moment ago has no scan yet (found
|
|
541
|
+
live on r1, 2026-09-14, the first build to read its image's scan rather
|
|
542
|
+
than its index's). It is waited for like a running scan, within the same
|
|
543
|
+
bound.
|
|
544
|
+
"""
|
|
545
|
+
return _error_code(error) in {
|
|
546
|
+
"ScanInProgressException",
|
|
547
|
+
"ScanNotFoundException",
|
|
548
|
+
"LimitExceededException",
|
|
549
|
+
}
|
|
@@ -36,6 +36,7 @@ __all__ = [
|
|
|
36
36
|
"BaseChannelUnpublishedError",
|
|
37
37
|
"approved_base",
|
|
38
38
|
"approved_repositories",
|
|
39
|
+
"channel_snapshot",
|
|
39
40
|
"is_approved_repository",
|
|
40
41
|
"resolve_base",
|
|
41
42
|
]
|
|
@@ -44,6 +45,8 @@ __all__ = [
|
|
|
44
45
|
ECR_BASE_PREFIX = "environments/base/"
|
|
45
46
|
|
|
46
47
|
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}")
|
|
48
|
+
#: A snapshot.ubuntu.com id: the moment, to the second, in UTC.
|
|
49
|
+
_SNAPSHOT = re.compile(r"\d{8}T\d{6}Z")
|
|
47
50
|
|
|
48
51
|
|
|
49
52
|
class ApprovedBase(BaseModel):
|
|
@@ -59,6 +62,11 @@ class ApprovedBase(BaseModel):
|
|
|
59
62
|
#: Channel, then variant, to the digest resolution pins. A channel with no
|
|
60
63
|
#: variant is approved and not yet published.
|
|
61
64
|
channels: dict[str, dict[str, str]] = Field(default_factory=dict)
|
|
65
|
+
#: Channel to the Ubuntu snapshot its apt versions are pinned against
|
|
66
|
+
#: (D-9): a ``snapshot.ubuntu.com`` id, taken just after the channel's
|
|
67
|
+
#: image upgraded its packages, so no pin is older than what the image
|
|
68
|
+
#: already carries. A channel with none pins against the base's mirror.
|
|
69
|
+
snapshots: dict[str, str] = Field(default_factory=dict)
|
|
62
70
|
|
|
63
71
|
@field_validator("channels")
|
|
64
72
|
@classmethod
|
|
@@ -75,6 +83,17 @@ class ApprovedBase(BaseModel):
|
|
|
75
83
|
for channel, digests in channels.items()
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
@field_validator("snapshots")
|
|
87
|
+
@classmethod
|
|
88
|
+
def _snapshot_ids_only(cls, snapshots: dict[str, str]) -> dict[str, str]:
|
|
89
|
+
for channel, snapshot in snapshots.items():
|
|
90
|
+
if not _SNAPSHOT.fullmatch(snapshot):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"channel {channel!r} pins apt to {snapshot!r}, "
|
|
93
|
+
"which is not a snapshot id like 20260914T150000Z"
|
|
94
|
+
)
|
|
95
|
+
return snapshots
|
|
96
|
+
|
|
78
97
|
@property
|
|
79
98
|
def name(self) -> str:
|
|
80
99
|
"""The base's name without its namespace: ``python-cpu``."""
|
|
@@ -103,6 +122,7 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
|
|
|
103
122
|
"sha256:cd09308a0c5e5adeec7fb5d8d29cf7455e78ba86f5c6c095a79068a280e1254f",
|
|
104
123
|
)
|
|
105
124
|
},
|
|
125
|
+
snapshots={"2026.09": "20260914T150000Z"},
|
|
106
126
|
),
|
|
107
127
|
# E2-17: jupyter-python-cuda plus the same layer.
|
|
108
128
|
ApprovedBase(
|
|
@@ -180,6 +200,14 @@ def resolve_base(
|
|
|
180
200
|
return digest
|
|
181
201
|
|
|
182
202
|
|
|
203
|
+
def channel_snapshot(
|
|
204
|
+
ref: str, channel: str, bases: dict[str, ApprovedBase] = APPROVED_BASES
|
|
205
|
+
) -> str:
|
|
206
|
+
"""The Ubuntu snapshot ``ref`` at ``channel`` pins apt against, or ``""`` for none (D-9)."""
|
|
207
|
+
base = bases.get(ref)
|
|
208
|
+
return "" if base is None else base.snapshots.get(channel, "")
|
|
209
|
+
|
|
210
|
+
|
|
183
211
|
def approved_repositories(bases: dict[str, ApprovedBase] = APPROVED_BASES) -> tuple[str, ...]:
|
|
184
212
|
"""Every repository name an approved base is published under."""
|
|
185
213
|
names: list[str] = []
|