code-sandboxes 1.9.3__tar.gz → 1.9.4__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.4}/CHANGELOG.md +22 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/PKG-INFO +1 -1
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/modal.py +102 -48
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/attest.py +82 -4
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_attest.py +73 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_managed_builders.py +12 -10
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_modal_builder.py +92 -25
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.gitignore +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/Dockerfile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/LICENSE +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/Makefile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/README.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/build.log +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/datalayer.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/daytona.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/bases.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/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.4}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/resolve.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/environments/spec.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/Makefile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/README.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/package.json +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/README.md +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/pyproject.toml +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/schemas/environment-v1alpha1.json +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_bases.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_datalayer_builder.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_daytona_builder.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_resolve.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_environment_spec.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.3 → code_sandboxes-1.9.4}/tests/test_the_kill_relaunch_drill.py +0 -0
|
@@ -8,6 +8,28 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 1.9.4
|
|
12
|
+
|
|
13
|
+
- **A pushed Environment image is decided by its linux/amd64 image's scan**
|
|
14
|
+
(`environments.attest`; PLAN_ENV.md E1-08). The Datalayer builder pushes
|
|
15
|
+
with SBOM and provenance attestations, so the digest it records is an OCI
|
|
16
|
+
image index. Amazon Inspector scans the image inside it and answers
|
|
17
|
+
`UNSUPPORTED_IMAGE` for the index itself, so every real build failed at
|
|
18
|
+
`attest` with `DL_ENV_PROVIDER_ERROR` (found live on r1, 2026-09-14, the
|
|
19
|
+
first build to get past the scan-read permission). The attestor now reads
|
|
20
|
+
the manifest and, for an index, reads the scan of its `linux/amd64` image,
|
|
21
|
+
skipping the attestation manifest. The signature stays on the index, which
|
|
22
|
+
is what a pod pulls.
|
|
23
|
+
- **Modal attaches a build secret to the `postInstall` steps that name it**
|
|
24
|
+
(`environments.adapters.modal`; PLAN_ENV.md E3-05). Modal used to refuse
|
|
25
|
+
every `buildSecrets` spec. Each declared secret is now resolved from IAM
|
|
26
|
+
before Modal is touched, made into a Modal Secret of the build's own,
|
|
27
|
+
passed as `secrets=` to exactly the `run_commands` steps whose command
|
|
28
|
+
names it, and deleted after the build like the base-reader secret. Its
|
|
29
|
+
value is redacted from the log and from a failed build's error. A
|
|
30
|
+
`mountAs: file` secret is refused, because Modal passes secrets as
|
|
31
|
+
environment variables and a file would land in a layer.
|
|
32
|
+
|
|
11
33
|
## 1.9.3
|
|
12
34
|
|
|
13
35
|
- **A build secret is mounted only on the `postInstall` commands that name it**
|
|
@@ -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:
|
|
@@ -17,6 +17,7 @@ rows amount to, and a paraphrase of them would test the paraphrase.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import json
|
|
20
21
|
import subprocess
|
|
21
22
|
|
|
22
23
|
import pytest
|
|
@@ -88,16 +89,35 @@ class FakeEcr:
|
|
|
88
89
|
findings=(),
|
|
89
90
|
enhanced_findings=True,
|
|
90
91
|
signed: set[str] | None = None,
|
|
92
|
+
manifest: dict | None = None,
|
|
91
93
|
) -> None:
|
|
92
94
|
self.statuses = list(statuses)
|
|
93
95
|
self.findings = list(findings)
|
|
94
96
|
self.enhanced_findings = enhanced_findings
|
|
95
97
|
self.signed = set(signed or set())
|
|
98
|
+
self.manifest = manifest
|
|
96
99
|
self.asked = 0
|
|
100
|
+
self.scanned: list[str] = []
|
|
97
101
|
self.described_tags: list[str] = []
|
|
98
102
|
|
|
103
|
+
def batch_get_image(self, repositoryName, imageIds, acceptedMediaTypes): # noqa: N803 - boto3's spelling
|
|
104
|
+
manifest = self.manifest or {
|
|
105
|
+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
106
|
+
"layers": [],
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
"images": [
|
|
110
|
+
{
|
|
111
|
+
"imageId": imageIds[0],
|
|
112
|
+
"imageManifestMediaType": manifest["mediaType"],
|
|
113
|
+
"imageManifest": json.dumps(manifest),
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
|
|
99
118
|
def describe_image_scan_findings(self, repositoryName, imageId): # noqa: N803 - boto3's spelling
|
|
100
119
|
self.asked += 1
|
|
120
|
+
self.scanned.append(imageId["imageDigest"])
|
|
101
121
|
status = self.statuses[min(self.asked, len(self.statuses)) - 1]
|
|
102
122
|
if status == "RAISES_IN_PROGRESS":
|
|
103
123
|
error = Exception("ScanInProgressException")
|
|
@@ -278,6 +298,59 @@ class TestTheScan:
|
|
|
278
298
|
assert raised.value.code.code == "DL_ENV_PROVIDER_ERROR"
|
|
279
299
|
assert raised.value.detail["digest"] == DIGEST
|
|
280
300
|
|
|
301
|
+
IMAGE = "sha256:" + "bb" * 32
|
|
302
|
+
ATTESTATION = "sha256:" + "cc" * 32
|
|
303
|
+
|
|
304
|
+
def an_index(self, *entries: dict) -> dict:
|
|
305
|
+
return {"mediaType": "application/vnd.oci.image.index.v1+json", "manifests": list(entries)}
|
|
306
|
+
|
|
307
|
+
def attestation_entry(self) -> dict:
|
|
308
|
+
return {
|
|
309
|
+
"digest": self.ATTESTATION,
|
|
310
|
+
"platform": {"os": "unknown", "architecture": "unknown"},
|
|
311
|
+
"annotations": {"vnd.docker.reference.type": "attestation-manifest"},
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
def test_an_index_is_decided_by_the_scan_of_its_linux_amd64_image(self) -> None:
|
|
315
|
+
"""The builder pushes with attestations, so what it records is an
|
|
316
|
+
index, and the scanner answers `UNSUPPORTED_IMAGE` for an index (found
|
|
317
|
+
live on r1, 2026-09-14)."""
|
|
318
|
+
ecr = FakeEcr(
|
|
319
|
+
manifest=self.an_index(
|
|
320
|
+
self.attestation_entry(),
|
|
321
|
+
{"digest": self.IMAGE, "platform": {"os": "linux", "architecture": "amd64"}},
|
|
322
|
+
),
|
|
323
|
+
findings=[enhanced("CVE-2026-1234", "CRITICAL", fixed="2.9.15")],
|
|
324
|
+
)
|
|
325
|
+
decision = an_attestor(ecr=ecr).scan(repository=REPOSITORY, digest=DIGEST)
|
|
326
|
+
assert ecr.scanned == [self.IMAGE]
|
|
327
|
+
assert decision.decision == "blocked"
|
|
328
|
+
|
|
329
|
+
def test_an_index_with_no_linux_amd64_image_is_named(self) -> None:
|
|
330
|
+
ecr = FakeEcr(manifest=self.an_index(self.attestation_entry()))
|
|
331
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
332
|
+
an_attestor(ecr=ecr).scan(repository=REPOSITORY, digest=DIGEST)
|
|
333
|
+
assert raised.value.code.code == "DL_ENV_PROVIDER_ERROR"
|
|
334
|
+
assert "linux/amd64" in raised.value.message
|
|
335
|
+
assert ecr.scanned == []
|
|
336
|
+
|
|
337
|
+
def test_a_single_image_is_scanned_by_its_own_digest(self) -> None:
|
|
338
|
+
ecr = FakeEcr()
|
|
339
|
+
an_attestor(ecr=ecr).scan(repository=REPOSITORY, digest=DIGEST)
|
|
340
|
+
assert ecr.scanned == [DIGEST]
|
|
341
|
+
|
|
342
|
+
def test_a_refused_scan_names_both_the_artifact_and_the_image_read(self) -> None:
|
|
343
|
+
ecr = FakeEcr(
|
|
344
|
+
statuses=("UNSUPPORTED_IMAGE",),
|
|
345
|
+
manifest=self.an_index(
|
|
346
|
+
{"digest": self.IMAGE, "platform": {"os": "linux", "architecture": "amd64"}}
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
350
|
+
an_attestor(ecr=ecr).scan(repository=REPOSITORY, digest=DIGEST)
|
|
351
|
+
assert raised.value.detail["digest"] == DIGEST
|
|
352
|
+
assert raised.value.detail["scannedDigest"] == self.IMAGE
|
|
353
|
+
|
|
281
354
|
|
|
282
355
|
# -- the signature --------------------------------------------------------------
|
|
283
356
|
|
|
@@ -235,17 +235,19 @@ class TestWhatIsSaidBeforeAnythingIsQueued:
|
|
|
235
235
|
report = get_builder("datalayer").validate(environment(**spec))
|
|
236
236
|
assert report.supported is True, messages(report)
|
|
237
237
|
|
|
238
|
-
def
|
|
239
|
-
"""Modal has a
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
238
|
+
def test_modal_accepts_an_env_build_secret_and_refuses_a_file_one(self) -> None:
|
|
239
|
+
"""Modal has a per-step `secrets=` mechanism, unlike E2B and Daytona,
|
|
240
|
+
and attaches a secret to the steps that name it (E3-05). It passes
|
|
241
|
+
environment variables only, so a `mountAs: file` secret, which would
|
|
242
|
+
have to be written into a layer, is refused."""
|
|
243
|
+
secret = {"id": "dlsec_01J9BUILDSECRET0000000000", "name": "TOKEN"}
|
|
244
|
+
report = get_builder("modal").validate(environment(buildSecrets=[secret]))
|
|
245
|
+
assert "spec.buildSecrets" not in " ".join(fields(report)), messages(report)
|
|
246
|
+
report = get_builder("modal").validate(
|
|
247
|
+
environment(buildSecrets=[{**secret, "mountAs": "file"}])
|
|
248
|
+
)
|
|
246
249
|
assert report.supported is False, messages(report)
|
|
247
|
-
assert "
|
|
248
|
-
assert "spec.buildSecrets" in fields(report)
|
|
250
|
+
assert "spec.buildSecrets[0].mountAs" in fields(report)
|
|
249
251
|
|
|
250
252
|
def test_every_finding_carries_the_capability_code(self) -> None:
|
|
251
253
|
report = get_builder("e2b").validate(
|