code-sandboxes 1.9.2__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.2 → code_sandboxes-1.9.4}/CHANGELOG.md +33 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/PKG-INFO +1 -1
- code_sandboxes-1.9.4/build.log +5 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/datalayer.py +13 -10
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/modal.py +102 -48
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/attest.py +82 -4
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/spec.py +24 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_attest.py +73 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_datalayer_builder.py +24 -12
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_managed_builders.py +12 -10
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_modal_builder.py +92 -25
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_spec.py +29 -1
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.gitignore +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/Dockerfile +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/LICENSE +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/Makefile +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/README.md +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/daytona.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/bases.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.2 → 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.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/resolve.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/Makefile +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/README.md +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/package.json +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/README.md +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/pyproject.toml +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/schemas/environment-v1alpha1.json +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_bases.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_daytona_builder.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_environment_resolve.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/tests/test_the_kill_relaunch_drill.py +0 -0
|
@@ -8,6 +8,39 @@
|
|
|
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
|
+
|
|
33
|
+
## 1.9.3
|
|
34
|
+
|
|
35
|
+
- **A build secret is mounted only on the `postInstall` commands that name it**
|
|
36
|
+
(`environments.spec.command_names_secret`, `environments.adapters.datalayer`;
|
|
37
|
+
PLAN_ENV.md E3-05). Every declared secret used to be mounted on every
|
|
38
|
+
`postInstall` `RUN`. A command now gets a secret's mount only when the
|
|
39
|
+
secret's name is in it as a whole word (`$NAME`, `${NAME}`, `--key-env NAME`,
|
|
40
|
+
`/run/secrets/NAME`). A declared secret that no command names is a
|
|
41
|
+
`DL_ENV_SPEC_INVALID` finding on `spec.buildSecrets[i]`, rather than a build
|
|
42
|
+
that runs with it empty.
|
|
43
|
+
|
|
11
44
|
## 1.9.2
|
|
12
45
|
|
|
13
46
|
- **A real ECR repository name is lowercase; a real owner uid is not**
|
{code_sandboxes-1.9.2 → code_sandboxes-1.9.4}/code_sandboxes/environments/adapters/datalayer.py
RENAMED
|
@@ -69,7 +69,7 @@ from ..errors import (
|
|
|
69
69
|
)
|
|
70
70
|
from ..files import files_step
|
|
71
71
|
from ..resolve import WHEELHOUSE_IMAGE_PATH, WHEELHOUSE_PATH, apt_pins_in, locked_versions
|
|
72
|
-
from ..spec import BuildSecret, Environment
|
|
72
|
+
from ..spec import BuildSecret, Environment, command_names_secret
|
|
73
73
|
|
|
74
74
|
__all__ = [
|
|
75
75
|
"ECR_ENVIRONMENT_PREFIX",
|
|
@@ -347,15 +347,14 @@ class Builder:
|
|
|
347
347
|
"/opt/datalayer/lock.txt",
|
|
348
348
|
]
|
|
349
349
|
)
|
|
350
|
-
# A build secret is mounted on the postInstall
|
|
351
|
-
# (§4.1, D-11): never an `ARG` or `ENV`, which bakes a
|
|
352
|
-
# image's history,
|
|
353
|
-
#
|
|
354
|
-
# BuildKit mount: the value is available to
|
|
355
|
-
# never written to a layer.
|
|
350
|
+
# A build secret is mounted on the postInstall commands that name it
|
|
351
|
+
# and nowhere else (§4.1, D-11): never an `ARG` or `ENV`, which bakes a
|
|
352
|
+
# value into the image's history, never the package-install or files
|
|
353
|
+
# steps, and never a postInstall command that does not read it.
|
|
354
|
+
# `--mount=type=secret` is a BuildKit mount: the value is available to
|
|
355
|
+
# the one `RUN`'s shell and never written to a layer.
|
|
356
356
|
wanted = set(request.build_secret_ids)
|
|
357
357
|
secrets = [secret for secret in spec.build_secrets if secret.id in wanted]
|
|
358
|
-
secret_mounts = " ".join(_secret_mount(secret) for secret in secrets)
|
|
359
358
|
baked = files_step(request.environment, variant=self.variant)
|
|
360
359
|
if baked:
|
|
361
360
|
lines.append("USER 1000:100")
|
|
@@ -365,11 +364,15 @@ class Builder:
|
|
|
365
364
|
if spec.commands.post_install:
|
|
366
365
|
lines.append("USER 1000:100")
|
|
367
366
|
lines.append("WORKDIR /home/datalayer/content")
|
|
368
|
-
prefix = "RUN --network=none " + (f"{secret_mounts} " if secret_mounts else "")
|
|
369
367
|
for command in spec.commands.post_install:
|
|
370
368
|
# No network: a command that fetches something makes an
|
|
371
369
|
# artifact whose contents depend on the day it was built.
|
|
372
|
-
|
|
370
|
+
mounts = "".join(
|
|
371
|
+
f"{_secret_mount(secret)} "
|
|
372
|
+
for secret in secrets
|
|
373
|
+
if command_names_secret(command, secret)
|
|
374
|
+
)
|
|
375
|
+
lines.append(f"RUN --network=none {mounts}{command}")
|
|
373
376
|
lines.extend(
|
|
374
377
|
[
|
|
375
378
|
"USER 1000:100",
|
|
@@ -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:
|
|
@@ -83,6 +83,7 @@ __all__ = [
|
|
|
83
83
|
"VariantSet",
|
|
84
84
|
"VersionStatus",
|
|
85
85
|
"assert_publishable",
|
|
86
|
+
"command_names_secret",
|
|
86
87
|
"parse_environment",
|
|
87
88
|
"parse_requirements_txt",
|
|
88
89
|
"publication_findings",
|
|
@@ -200,6 +201,18 @@ class BuildSecret(_Model):
|
|
|
200
201
|
name: str = Field(pattern=r"^[A-Za-z0-9_.-]+$")
|
|
201
202
|
|
|
202
203
|
|
|
204
|
+
def command_names_secret(command: str, secret: BuildSecret) -> bool:
|
|
205
|
+
"""Whether one ``postInstall`` command names ``secret``, so its step gets the mount (E3-05).
|
|
206
|
+
|
|
207
|
+
A command names a secret when the secret's name is in it as a whole word:
|
|
208
|
+
``$PIP_TOKEN``, ``${PIP_TOKEN}``, ``--key-env PIP_TOKEN`` or
|
|
209
|
+
``/run/secrets/netrc``. A secret is mounted on exactly the steps that name
|
|
210
|
+
it (§4.1), so a command that reads none holds none.
|
|
211
|
+
"""
|
|
212
|
+
word = rf"(?<![A-Za-z0-9_.-]){re.escape(secret.name)}(?![A-Za-z0-9_.-])"
|
|
213
|
+
return re.search(word, command) is not None
|
|
214
|
+
|
|
215
|
+
|
|
203
216
|
class Accelerator(_Model):
|
|
204
217
|
type: str
|
|
205
218
|
count: int = Field(default=1, ge=1)
|
|
@@ -640,6 +653,17 @@ def spec_findings(
|
|
|
640
653
|
values = [getattr(secret, attribute) for secret in spec.build_secrets]
|
|
641
654
|
if len(values) != len(set(values)):
|
|
642
655
|
findings.append(SpecFinding("spec.buildSecrets", f"a secret {label} is listed twice"))
|
|
656
|
+
for index, secret in enumerate(spec.build_secrets):
|
|
657
|
+
# A secret no command names would be mounted nowhere: the build would
|
|
658
|
+
# run with it empty and fail far from the cause.
|
|
659
|
+
if not any(command_names_secret(command, secret) for command in spec.commands.post_install):
|
|
660
|
+
findings.append(
|
|
661
|
+
SpecFinding(
|
|
662
|
+
f"spec.buildSecrets[{index}]",
|
|
663
|
+
f"`{secret.name}` is named by no `postInstall` command, and a secret "
|
|
664
|
+
"is mounted only on the commands that name it",
|
|
665
|
+
)
|
|
666
|
+
)
|
|
643
667
|
|
|
644
668
|
resources = spec.resources
|
|
645
669
|
if resources.size_class not in SIZE_CLASSES:
|
|
@@ -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
|
|