code-sandboxes 1.9.18__tar.gz → 1.9.19__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.18 → code_sandboxes-1.9.19}/PKG-INFO +1 -1
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/adapters/modal.py +86 -2
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/attest.py +81 -1
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/build_secrets.py +144 -1
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/builders.py +8 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_attest.py +61 -1
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_build_secrets.py +117 -1
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_managed_builders.py +4 -4
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_modal_builder.py +149 -3
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.gitignore +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/CHANGELOG.md +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/Dockerfile +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/LICENSE +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/Makefile +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/README.md +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/adapters/datalayer.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/adapters/daytona.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/bases.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/resolve.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/resolve_conda.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/spec.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/Makefile +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/README.md +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/package.json +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/README.md +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/pyproject.toml +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/schemas/environment-v1alpha1.json +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_bases.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_datalayer_builder.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_daytona_builder.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_resolve.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_resolve_conda.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_environment_spec.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/tests/test_the_kill_relaunch_drill.py +0 -0
{code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/adapters/modal.py
RENAMED
|
@@ -230,8 +230,7 @@ def _install_packages(image: Any, lock_text: str) -> Any:
|
|
|
230
230
|
# Packages install as root: every Modal build step already runs as
|
|
231
231
|
# root regardless of any `USER` line (see the module docstring), so
|
|
232
232
|
# this is stating what is already true rather than asking for it.
|
|
233
|
-
"uv pip sync --system --require-hashes "
|
|
234
|
-
f"--find-links {WHEELHOUSE_IMAGE_PATH} {_LOCK_PATH}",
|
|
233
|
+
f"uv pip sync --system --require-hashes --find-links {WHEELHOUSE_IMAGE_PATH} {_LOCK_PATH}",
|
|
235
234
|
)
|
|
236
235
|
|
|
237
236
|
|
|
@@ -245,6 +244,40 @@ def _post_install(
|
|
|
245
244
|
return image
|
|
246
245
|
|
|
247
246
|
|
|
247
|
+
def _intermediates_of(built: Any) -> tuple[str, ...]:
|
|
248
|
+
"""Every layer under a built image, by id, deepest first (E2-05, E2-09).
|
|
249
|
+
|
|
250
|
+
Each chained builder call leaves an image of its own, and deleting the
|
|
251
|
+
artifact does not delete them. Modal offers no call that lists an
|
|
252
|
+
account's images, so an intermediate nobody wrote down at build time can
|
|
253
|
+
never be found again — which is why this is recorded rather than
|
|
254
|
+
discovered. Confirmed live on 2026-09-17: a three-call chain built through
|
|
255
|
+
`Image.build` hydrates an `object_id` on every image in `deps()`, not only
|
|
256
|
+
on the last.
|
|
257
|
+
|
|
258
|
+
The built image itself is not an intermediate: it is the artifact.
|
|
259
|
+
"""
|
|
260
|
+
found: list[str] = []
|
|
261
|
+
seen: set[int] = set()
|
|
262
|
+
|
|
263
|
+
def walk(image: Any) -> None:
|
|
264
|
+
if id(image) in seen:
|
|
265
|
+
return
|
|
266
|
+
seen.add(id(image))
|
|
267
|
+
for dependency in getattr(image, "deps", lambda: ())():
|
|
268
|
+
if hasattr(dependency, "deps"):
|
|
269
|
+
walk(dependency)
|
|
270
|
+
object_id = getattr(image, "object_id", None)
|
|
271
|
+
if object_id and image is not built:
|
|
272
|
+
found.append(str(object_id))
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
walk(built)
|
|
276
|
+
except Exception:
|
|
277
|
+
return ()
|
|
278
|
+
return tuple(dict.fromkeys(found))
|
|
279
|
+
|
|
280
|
+
|
|
248
281
|
class Builder(ManagedBuilder):
|
|
249
282
|
"""Modal: the capability half (E2-06) and the build (E2-05)."""
|
|
250
283
|
|
|
@@ -479,6 +512,7 @@ class Builder(ManagedBuilder):
|
|
|
479
512
|
mutable_alias=name,
|
|
480
513
|
provider_account=provider_account(self.variant, self._provider_secrets()) or None,
|
|
481
514
|
contract_version=spec.contract or SANDBOX_CONTRACT_V1.version,
|
|
515
|
+
intermediates=_intermediates_of(built),
|
|
482
516
|
)
|
|
483
517
|
|
|
484
518
|
def _resolved_secrets(self, request: BuildRequest) -> tuple[list[BuildSecret], dict[str, str]]:
|
|
@@ -631,6 +665,56 @@ class Builder(ManagedBuilder):
|
|
|
631
665
|
raise self._provider_error("ask whether the image exists", error) from error
|
|
632
666
|
return True
|
|
633
667
|
|
|
668
|
+
def delete(self, artifact: ArtifactReference) -> None:
|
|
669
|
+
"""Delete the image, and every intermediate layer this build recorded (E2-05, E2-09).
|
|
670
|
+
|
|
671
|
+
Deleting the artifact does not delete the layers under it, and Modal
|
|
672
|
+
offers no call that lists an account's images, so what is removed is
|
|
673
|
+
what `build` wrote down — an intermediate nobody recorded can never be
|
|
674
|
+
found again.
|
|
675
|
+
|
|
676
|
+
Two answers are outcomes rather than failures, both found live on
|
|
677
|
+
2026-09-17:
|
|
678
|
+
|
|
679
|
+
* **Already gone** is success. A replay of a collection must delete the
|
|
680
|
+
same set again with no harm, the same way the Datalayer collector
|
|
681
|
+
treats an artifact that is not there.
|
|
682
|
+
* **Not ours to delete.** The bottom of a chain can be an image the
|
|
683
|
+
workspace does not own — Modal's own `debian_slim` answers
|
|
684
|
+
`PermissionDenied` — and an image somebody else owns was never this
|
|
685
|
+
artifact's to collect. It is logged and stepped over, not raised.
|
|
686
|
+
"""
|
|
687
|
+
sdk = self._modal_sdk()
|
|
688
|
+
client = self._client(sdk)
|
|
689
|
+
synchronizer, api_pb2 = self._modal_internals()
|
|
690
|
+
|
|
691
|
+
async def _delete(image_id: str) -> None:
|
|
692
|
+
await client.stub.ImageDelete(api_pb2.ImageDeleteRequest(image_id=image_id))
|
|
693
|
+
|
|
694
|
+
# The artifact last: an intermediate is only reachable while the
|
|
695
|
+
# record naming it survives, so a half-done collection that has
|
|
696
|
+
# dropped the image would strand them.
|
|
697
|
+
for image_id in (*artifact.intermediates, artifact.provider_artifact_id):
|
|
698
|
+
if not image_id:
|
|
699
|
+
continue
|
|
700
|
+
try:
|
|
701
|
+
# A bare coroutine on Modal's stub silently does nothing, so
|
|
702
|
+
# this runs on the SDK's own loop — see `_delete_secret`.
|
|
703
|
+
synchronizer.wrap(_delete)(image_id)
|
|
704
|
+
except sdk.exception.NotFoundError:
|
|
705
|
+
continue
|
|
706
|
+
except Exception as error:
|
|
707
|
+
if "permission" in str(error).lower():
|
|
708
|
+
self._log(
|
|
709
|
+
f"The Modal image {image_id} is not this account's to delete: {error}"
|
|
710
|
+
)
|
|
711
|
+
continue
|
|
712
|
+
if image_id == artifact.provider_artifact_id:
|
|
713
|
+
raise self._provider_error("delete the image", error) from error
|
|
714
|
+
# One layer's refusal does not strand the rest, nor the
|
|
715
|
+
# artifact this was called to collect.
|
|
716
|
+
self._log(f"The Modal intermediate {image_id} could not be deleted: {error}")
|
|
717
|
+
|
|
634
718
|
def _provider_error(self, what: str, error: BaseException) -> EnvironmentsError:
|
|
635
719
|
return EnvironmentsError(
|
|
636
720
|
PROVIDER_ERROR,
|
|
@@ -106,6 +106,76 @@ def signature_tag(digest: str) -> str:
|
|
|
106
106
|
return text.replace(":", "-", 1) + ".sig"
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
#: Where a CycloneDX component keeps its licence, in the order they are read.
|
|
110
|
+
_CYCLONEDX_LICENSE_KEYS = ("id", "name")
|
|
111
|
+
#: Where an SPDX package keeps its licence. `licenseConcluded` is what the
|
|
112
|
+
#: tool decided; `licenseDeclared` is what the package claimed. BuildKit
|
|
113
|
+
#: writes SPDX, so this is the one that matters in practice.
|
|
114
|
+
_SPDX_LICENSE_KEYS = ("licenseConcluded", "licenseDeclared")
|
|
115
|
+
#: What SPDX writes when it could not tell, which is not a licence.
|
|
116
|
+
_SPDX_UNKNOWN = frozenset({"NOASSERTION", "NONE", ""})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _spdx_licenses(document: Mapping[str, Any]) -> set[str]:
|
|
120
|
+
"""What an SPDX document names, which is what BuildKit's `attest:sbom=` writes.
|
|
121
|
+
|
|
122
|
+
`licenseConcluded` is what the tool decided and `licenseDeclared` what the
|
|
123
|
+
package claimed, so the concluded one is read first and the declared one
|
|
124
|
+
only when it said nothing.
|
|
125
|
+
"""
|
|
126
|
+
found: set[str] = set()
|
|
127
|
+
for package in document.get("packages") or ():
|
|
128
|
+
if not isinstance(package, Mapping):
|
|
129
|
+
continue
|
|
130
|
+
for key in _SPDX_LICENSE_KEYS:
|
|
131
|
+
value = str(package.get(key) or "").strip()
|
|
132
|
+
if value and value.upper() not in _SPDX_UNKNOWN:
|
|
133
|
+
found.add(value)
|
|
134
|
+
break
|
|
135
|
+
return found
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _cyclonedx_licenses(document: Mapping[str, Any]) -> set[str]:
|
|
139
|
+
"""What a CycloneDX document names, by id, by name, or as an expression."""
|
|
140
|
+
found: set[str] = set()
|
|
141
|
+
for component in document.get("components") or ():
|
|
142
|
+
if not isinstance(component, Mapping):
|
|
143
|
+
continue
|
|
144
|
+
for entry in component.get("licenses") or ():
|
|
145
|
+
if not isinstance(entry, Mapping):
|
|
146
|
+
continue
|
|
147
|
+
licence = entry.get("license")
|
|
148
|
+
if isinstance(licence, Mapping):
|
|
149
|
+
for key in _CYCLONEDX_LICENSE_KEYS:
|
|
150
|
+
value = str(licence.get(key) or "").strip()
|
|
151
|
+
if value:
|
|
152
|
+
found.add(value)
|
|
153
|
+
break
|
|
154
|
+
expression = str(entry.get("expression") or "").strip()
|
|
155
|
+
if expression:
|
|
156
|
+
found.add(expression)
|
|
157
|
+
return found
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def licenses_of(document: Any) -> list[str]:
|
|
161
|
+
"""Every licence an SBOM names, deduplicated and sorted.
|
|
162
|
+
|
|
163
|
+
Reads both shapes the ecosystem writes: SPDX, which is what BuildKit's
|
|
164
|
+
`attest:sbom=` produces, and CycloneDX. A document in neither shape, or one
|
|
165
|
+
that names nothing, answers an empty list rather than raising: a
|
|
166
|
+
publication's licence list is worth having and never worth failing a build
|
|
167
|
+
over.
|
|
168
|
+
|
|
169
|
+
This is what a published version's snapshot carries (D-12, E2-16). Until
|
|
170
|
+
it did, `licenses` came from the scan summary — and the registry's scanner
|
|
171
|
+
reports vulnerabilities, not licences, so every publication froze an empty
|
|
172
|
+
list beside an SBOM reference.
|
|
173
|
+
"""
|
|
174
|
+
if not isinstance(document, Mapping):
|
|
175
|
+
return []
|
|
176
|
+
return sorted(_spdx_licenses(document) | _cyclonedx_licenses(document))
|
|
177
|
+
|
|
178
|
+
|
|
109
179
|
@dataclass(frozen=True)
|
|
110
180
|
class AttestationResult:
|
|
111
181
|
"""What the workflow stores about an artifact once both gates have passed."""
|
|
@@ -117,6 +187,8 @@ class AttestationResult:
|
|
|
117
187
|
size_bytes: int | None = None
|
|
118
188
|
signed_now: bool = True
|
|
119
189
|
"""False when a replay found the signature that was already there."""
|
|
190
|
+
licenses: tuple[str, ...] = ()
|
|
191
|
+
"""What the SBOM named, frozen onto the artifact for a publication to carry."""
|
|
120
192
|
|
|
121
193
|
def body(self) -> dict[str, Any]:
|
|
122
194
|
"""The mapping `activities_environments.attest` answers."""
|
|
@@ -127,6 +199,7 @@ class AttestationResult:
|
|
|
127
199
|
"signature_ref": self.signature_ref,
|
|
128
200
|
"size_bytes": self.size_bytes,
|
|
129
201
|
"signed_now": self.signed_now,
|
|
202
|
+
"licenses": list(self.licenses),
|
|
130
203
|
}
|
|
131
204
|
|
|
132
205
|
|
|
@@ -515,8 +588,14 @@ class Attestor:
|
|
|
515
588
|
size_bytes: int | None = None,
|
|
516
589
|
sbom_ref: str = "",
|
|
517
590
|
provenance_ref: str = "",
|
|
591
|
+
sbom: Any = None,
|
|
518
592
|
) -> AttestationResult:
|
|
519
|
-
"""Scan, then sign: the order the Operator's check depends on (D-11).
|
|
593
|
+
"""Scan, then sign: the order the Operator's check depends on (D-11).
|
|
594
|
+
|
|
595
|
+
`sbom`, when the caller has the document, is read for the licences a
|
|
596
|
+
publication carries; the registry's scanner reports vulnerabilities
|
|
597
|
+
and never licences, so there is nowhere else they come from.
|
|
598
|
+
"""
|
|
520
599
|
self.can_sign()
|
|
521
600
|
decision = self.scan(repository=repository, digest=digest)
|
|
522
601
|
if not decision.passed:
|
|
@@ -540,6 +619,7 @@ class Attestor:
|
|
|
540
619
|
if size_bytes is not None
|
|
541
620
|
else self.size_of(repository=repository, digest=digest),
|
|
542
621
|
signed_now=signed_now,
|
|
622
|
+
licenses=tuple(licenses_of(sbom)),
|
|
543
623
|
)
|
|
544
624
|
|
|
545
625
|
def size_of(self, *, repository: str, digest: str) -> int | None:
|
{code_sandboxes-1.9.18 → code_sandboxes-1.9.19}/code_sandboxes/environments/build_secrets.py
RENAMED
|
@@ -28,7 +28,12 @@ from __future__ import annotations
|
|
|
28
28
|
import os
|
|
29
29
|
from typing import Any
|
|
30
30
|
|
|
31
|
-
from .errors import
|
|
31
|
+
from .errors import (
|
|
32
|
+
BUILD_SECRET_UNAVAILABLE,
|
|
33
|
+
CAPABILITY_UNSUPPORTED,
|
|
34
|
+
PROVIDER_ERROR,
|
|
35
|
+
EnvironmentsError,
|
|
36
|
+
)
|
|
32
37
|
from .spec import BuildSecret
|
|
33
38
|
|
|
34
39
|
__all__ = [
|
|
@@ -150,3 +155,141 @@ def resolve_build_secret(
|
|
|
150
155
|
retryable=False,
|
|
151
156
|
)
|
|
152
157
|
return value
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
#: The managed variants an owner keeps a credential for (D-8). `datalayer` is
|
|
161
|
+
#: not one: the platform's own builder uses the platform's own registry.
|
|
162
|
+
PROVIDER_VARIANTS = frozenset({"daytona", "e2b", "modal"})
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resolve_provider_credential(
|
|
166
|
+
variant: str,
|
|
167
|
+
*,
|
|
168
|
+
owner_uid: str,
|
|
169
|
+
iam_url: str | None = None,
|
|
170
|
+
api_key: str | None = None,
|
|
171
|
+
timeout: float = 10.0,
|
|
172
|
+
transport: Any = None,
|
|
173
|
+
) -> dict[str, str]:
|
|
174
|
+
"""The owner's own credential for a managed variant (PLAN_ENVS.md E2-01, D-8).
|
|
175
|
+
|
|
176
|
+
A managed artifact exists only in the account the credential opens, so the
|
|
177
|
+
build runs with the owner's keys and records a non-secret fingerprint of
|
|
178
|
+
that account. The value IAM holds is a JSON object of the environment
|
|
179
|
+
names the provider's own SDK reads — ``DAYTONA_API_KEY``,
|
|
180
|
+
``E2B_API_KEY``, ``MODAL_TOKEN_ID`` and ``MODAL_TOKEN_SECRET``, and
|
|
181
|
+
optionally the account names ``environments/accounts.py`` reads to
|
|
182
|
+
fingerprint it — because that is the shape ``BuildCredential`` carries and
|
|
183
|
+
every adapter already reads.
|
|
184
|
+
|
|
185
|
+
Raises ``DL_ENV_CAPABILITY_UNSUPPORTED`` — not retryable — when the owner
|
|
186
|
+
has no credential for this variant. A build must be told it cannot run in
|
|
187
|
+
an account nobody configured, rather than falling back to whatever keys
|
|
188
|
+
the worker happens to hold: that would put one owner's artifact in
|
|
189
|
+
another's account, which is the one thing D-8 exists to prevent.
|
|
190
|
+
"""
|
|
191
|
+
variant = (variant or "").strip().lower()
|
|
192
|
+
if variant not in PROVIDER_VARIANTS:
|
|
193
|
+
raise EnvironmentsError(
|
|
194
|
+
CAPABILITY_UNSUPPORTED,
|
|
195
|
+
f"`{variant}` is not a managed variant a credential is kept for",
|
|
196
|
+
detail={"variant": variant},
|
|
197
|
+
retryable=False,
|
|
198
|
+
)
|
|
199
|
+
key = api_key if api_key is not None else os.environ.get(IAM_API_KEY_VARIABLE, "")
|
|
200
|
+
if not key:
|
|
201
|
+
raise EnvironmentsError(
|
|
202
|
+
CAPABILITY_UNSUPPORTED,
|
|
203
|
+
f"No {IAM_API_KEY_VARIABLE} to ask IAM for the {variant} credential with",
|
|
204
|
+
detail={"variant": variant, "missing": IAM_API_KEY_VARIABLE},
|
|
205
|
+
retryable=False,
|
|
206
|
+
)
|
|
207
|
+
base = (iam_url if iam_url is not None else os.environ.get(IAM_URL_VARIABLE, "")).rstrip("/")
|
|
208
|
+
if not base:
|
|
209
|
+
raise EnvironmentsError(
|
|
210
|
+
CAPABILITY_UNSUPPORTED,
|
|
211
|
+
f"No {IAM_URL_VARIABLE} to ask for the {variant} credential",
|
|
212
|
+
detail={"variant": variant, "missing": IAM_URL_VARIABLE},
|
|
213
|
+
retryable=False,
|
|
214
|
+
)
|
|
215
|
+
try:
|
|
216
|
+
import httpx
|
|
217
|
+
except ImportError as error:
|
|
218
|
+
raise EnvironmentsError(
|
|
219
|
+
CAPABILITY_UNSUPPORTED,
|
|
220
|
+
"No HTTP client to resolve a provider credential with: install "
|
|
221
|
+
"`code-sandboxes[environments-builder]`",
|
|
222
|
+
detail={"variant": variant, "missing": "httpx"},
|
|
223
|
+
retryable=False,
|
|
224
|
+
) from error
|
|
225
|
+
url = f"{base}/api/iam/v1/secrets/provider/{variant}/value"
|
|
226
|
+
try:
|
|
227
|
+
with httpx.Client(transport=transport, timeout=timeout) as client:
|
|
228
|
+
response = client.get(
|
|
229
|
+
url,
|
|
230
|
+
params={"owner_uid": owner_uid},
|
|
231
|
+
headers={"X-API-Key": key},
|
|
232
|
+
)
|
|
233
|
+
except httpx.HTTPError as error:
|
|
234
|
+
raise EnvironmentsError(
|
|
235
|
+
PROVIDER_ERROR,
|
|
236
|
+
f"IAM could not be reached for the {variant} credential: {error}",
|
|
237
|
+
detail={"variant": variant},
|
|
238
|
+
retryable=True,
|
|
239
|
+
) from error
|
|
240
|
+
if response.status_code == 404:
|
|
241
|
+
raise EnvironmentsError(
|
|
242
|
+
CAPABILITY_UNSUPPORTED,
|
|
243
|
+
f"This owner has no {variant} credential: a managed build runs in "
|
|
244
|
+
f"their own account, so one must be kept before {variant} can build",
|
|
245
|
+
detail={"variant": variant, "status": 404},
|
|
246
|
+
retryable=False,
|
|
247
|
+
)
|
|
248
|
+
if response.status_code != 200:
|
|
249
|
+
raise EnvironmentsError(
|
|
250
|
+
PROVIDER_ERROR,
|
|
251
|
+
f"IAM refused the {variant} credential: {response.status_code}",
|
|
252
|
+
detail={"variant": variant, "status": response.status_code},
|
|
253
|
+
retryable=True,
|
|
254
|
+
)
|
|
255
|
+
try:
|
|
256
|
+
body = response.json()
|
|
257
|
+
except ValueError as error:
|
|
258
|
+
raise EnvironmentsError(
|
|
259
|
+
PROVIDER_ERROR,
|
|
260
|
+
f"IAM answered the {variant} credential with a body that is not valid JSON",
|
|
261
|
+
detail={"variant": variant},
|
|
262
|
+
retryable=True,
|
|
263
|
+
) from error
|
|
264
|
+
return _provider_secrets_of(body.get("value") if isinstance(body, dict) else None, variant)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _provider_secrets_of(value: Any, variant: str) -> dict[str, str]:
|
|
268
|
+
"""The environment names a provider credential carries, as IAM answered.
|
|
269
|
+
|
|
270
|
+
A mapping of names to values: IAM gathers them from the secrets the owner
|
|
271
|
+
already keeps, one per environment variable, and decodes them, so the
|
|
272
|
+
worker is not asked to know how a secret is stored.
|
|
273
|
+
"""
|
|
274
|
+
if not isinstance(value, dict) or not value:
|
|
275
|
+
raise EnvironmentsError(
|
|
276
|
+
CAPABILITY_UNSUPPORTED,
|
|
277
|
+
f"IAM answered the {variant} credential with no usable value; a "
|
|
278
|
+
"provider credential is the environment names that provider's own "
|
|
279
|
+
"SDK reads",
|
|
280
|
+
detail={"variant": variant},
|
|
281
|
+
retryable=False,
|
|
282
|
+
)
|
|
283
|
+
secrets = {
|
|
284
|
+
str(name): str(item)
|
|
285
|
+
for name, item in value.items()
|
|
286
|
+
if str(name).strip() and str(item).strip()
|
|
287
|
+
}
|
|
288
|
+
if not secrets:
|
|
289
|
+
raise EnvironmentsError(
|
|
290
|
+
CAPABILITY_UNSUPPORTED,
|
|
291
|
+
f"The {variant} credential names nothing",
|
|
292
|
+
detail={"variant": variant},
|
|
293
|
+
retryable=False,
|
|
294
|
+
)
|
|
295
|
+
return secrets
|
|
@@ -141,6 +141,14 @@ class ArtifactReference(_Model):
|
|
|
141
141
|
provider_account: str | None = None
|
|
142
142
|
contract_version: str
|
|
143
143
|
architecture: str = "linux/amd64"
|
|
144
|
+
#: The layers this build left behind that deleting the artifact does not
|
|
145
|
+
#: delete (E2-05, E2-09). Modal is the variant that has them: each chained
|
|
146
|
+
#: builder call leaves an image of its own with an id, and Modal offers no
|
|
147
|
+
#: call that lists an account's images — `ImageGetOrCreate`, `ImageFromId`,
|
|
148
|
+
#: `ImageGetByTag`, `ImageListTags`, `ImageTagRevisions`, `ImagePublish`
|
|
149
|
+
#: and `ImageDelete`, and nothing that enumerates — so an intermediate
|
|
150
|
+
#: nobody wrote down is an intermediate nobody can ever find again.
|
|
151
|
+
intermediates: tuple[str, ...] = ()
|
|
144
152
|
|
|
145
153
|
@model_validator(mode="after")
|
|
146
154
|
def _immutable(self) -> ArtifactReference:
|
|
@@ -22,7 +22,7 @@ import subprocess
|
|
|
22
22
|
|
|
23
23
|
import pytest
|
|
24
24
|
|
|
25
|
-
from code_sandboxes.environments.attest import Attestor, attest_artifact, signature_tag
|
|
25
|
+
from code_sandboxes.environments.attest import Attestor, attest_artifact, licenses_of, signature_tag
|
|
26
26
|
from code_sandboxes.environments.builders import ArtifactReference
|
|
27
27
|
from code_sandboxes.environments.errors import EnvironmentsError
|
|
28
28
|
from code_sandboxes.environments.policy import (
|
|
@@ -689,3 +689,63 @@ def test_nothing_reaches_a_registry_when_nothing_could_sign() -> None:
|
|
|
689
689
|
attest_artifact(artifact=artifact, attestor=an_attestor(ecr=ecr, key=""))
|
|
690
690
|
assert raised.value.detail["missing"] == "DATALAYER_ENVIRONMENTS_KMS_KEY"
|
|
691
691
|
assert ecr.asked == 0, "the registry was asked before anything could have been signed"
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
# -- the licences a publication carries ---------------------------------------------------------
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
class TestLicencesFromTheSbom:
|
|
698
|
+
"""What `licenses_of` reads, and what it refuses to guess.
|
|
699
|
+
|
|
700
|
+
A published version's page says it shows the licences its SBOM names
|
|
701
|
+
(D-12, E2-16). They had nowhere to come from: the snapshot read the scan
|
|
702
|
+
summary, and the registry's scanner reports vulnerabilities.
|
|
703
|
+
"""
|
|
704
|
+
|
|
705
|
+
def test_it_reads_spdx_which_is_what_buildkit_writes(self) -> None:
|
|
706
|
+
document = {
|
|
707
|
+
"packages": [
|
|
708
|
+
{"name": "gdal", "licenseConcluded": "MIT"},
|
|
709
|
+
{"name": "numpy", "licenseConcluded": "BSD-3-Clause"},
|
|
710
|
+
{"name": "again", "licenseConcluded": "MIT"},
|
|
711
|
+
]
|
|
712
|
+
}
|
|
713
|
+
assert licenses_of(document) == ["BSD-3-Clause", "MIT"]
|
|
714
|
+
|
|
715
|
+
def test_a_concluded_licence_wins_over_a_declared_one(self) -> None:
|
|
716
|
+
"""`licenseConcluded` is what the tool decided; `licenseDeclared` is the claim."""
|
|
717
|
+
document = {
|
|
718
|
+
"packages": [
|
|
719
|
+
{"licenseConcluded": "Apache-2.0", "licenseDeclared": "MIT"},
|
|
720
|
+
]
|
|
721
|
+
}
|
|
722
|
+
assert licenses_of(document) == ["Apache-2.0"]
|
|
723
|
+
|
|
724
|
+
def test_noassertion_is_not_a_licence_and_falls_through(self) -> None:
|
|
725
|
+
"""SPDX writes NOASSERTION when it could not tell, which must not be shown."""
|
|
726
|
+
document = {
|
|
727
|
+
"packages": [
|
|
728
|
+
{"licenseConcluded": "NOASSERTION", "licenseDeclared": "BSD-3-Clause"},
|
|
729
|
+
{"licenseConcluded": "NONE", "licenseDeclared": ""},
|
|
730
|
+
]
|
|
731
|
+
}
|
|
732
|
+
assert licenses_of(document) == ["BSD-3-Clause"]
|
|
733
|
+
|
|
734
|
+
def test_it_reads_cyclonedx_by_id_by_name_and_by_expression(self) -> None:
|
|
735
|
+
document = {
|
|
736
|
+
"components": [
|
|
737
|
+
{"licenses": [{"license": {"id": "Apache-2.0"}}]},
|
|
738
|
+
{"licenses": [{"license": {"name": "Public Domain"}}]},
|
|
739
|
+
{"licenses": [{"expression": "MIT OR Apache-2.0"}]},
|
|
740
|
+
]
|
|
741
|
+
}
|
|
742
|
+
assert licenses_of(document) == [
|
|
743
|
+
"Apache-2.0",
|
|
744
|
+
"MIT OR Apache-2.0",
|
|
745
|
+
"Public Domain",
|
|
746
|
+
]
|
|
747
|
+
|
|
748
|
+
def test_a_document_it_does_not_understand_names_nothing(self) -> None:
|
|
749
|
+
"""Never a reason to fail a build: a licence list is worth having, not dying for."""
|
|
750
|
+
for document in (None, {}, {"packages": None}, {"components": [1, 2]}, "spdx"):
|
|
751
|
+
assert licenses_of(document) == []
|
|
@@ -18,8 +18,13 @@ from code_sandboxes.environments.build_secrets import (
|
|
|
18
18
|
IAM_API_KEY_VARIABLE,
|
|
19
19
|
IAM_URL_VARIABLE,
|
|
20
20
|
resolve_build_secret,
|
|
21
|
+
resolve_provider_credential,
|
|
22
|
+
)
|
|
23
|
+
from code_sandboxes.environments.errors import (
|
|
24
|
+
BUILD_SECRET_UNAVAILABLE,
|
|
25
|
+
CAPABILITY_UNSUPPORTED,
|
|
26
|
+
EnvironmentsError,
|
|
21
27
|
)
|
|
22
|
-
from code_sandboxes.environments.errors import BUILD_SECRET_UNAVAILABLE, EnvironmentsError
|
|
23
28
|
from code_sandboxes.environments.spec import BuildSecret
|
|
24
29
|
|
|
25
30
|
SECRET = BuildSecret(id="dlsec_01J9BUILDSECRET0000000000", name="PIP_TOKEN")
|
|
@@ -215,3 +220,114 @@ def test_the_environment_variables_are_read_when_no_argument_is_given(monkeypatc
|
|
|
215
220
|
|
|
216
221
|
value = resolve_build_secret(SECRET, owner_uid=OWNER, transport=httpx.MockTransport(handler))
|
|
217
222
|
assert value == "tok-1"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# -- the owner's own provider credential (E2-01, D-8) -------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _credential_transport(handler) -> httpx.MockTransport:
|
|
229
|
+
return httpx.MockTransport(handler)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_the_credential_is_asked_for_by_variant_and_owner() -> None:
|
|
233
|
+
"""A build knows whose it is and which variant it is building, never a secret id."""
|
|
234
|
+
seen: list[httpx.Request] = []
|
|
235
|
+
|
|
236
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
237
|
+
seen.append(request)
|
|
238
|
+
return httpx.Response(
|
|
239
|
+
200,
|
|
240
|
+
json={"provider": "daytona", "value": {"DAYTONA_API_KEY": "k"}},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
secrets = resolve_provider_credential(
|
|
244
|
+
"daytona",
|
|
245
|
+
owner_uid="owner-1",
|
|
246
|
+
iam_url="https://iam.example",
|
|
247
|
+
api_key="worker-key",
|
|
248
|
+
transport=_credential_transport(handler),
|
|
249
|
+
)
|
|
250
|
+
assert secrets == {"DAYTONA_API_KEY": "k"}
|
|
251
|
+
(request,) = seen
|
|
252
|
+
assert request.url.path == "/api/iam/v1/secrets/provider/daytona/value"
|
|
253
|
+
assert request.url.params["owner_uid"] == "owner-1"
|
|
254
|
+
# The worker's own key, never a person's token.
|
|
255
|
+
assert request.headers["X-API-Key"] == "worker-key"
|
|
256
|
+
assert "authorization" not in {name.lower() for name in request.headers}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_the_account_names_come_through_beside_the_key() -> None:
|
|
260
|
+
"""`environments/accounts.py` fingerprints the account from these, so a
|
|
261
|
+
credential is more than the key that opens it."""
|
|
262
|
+
|
|
263
|
+
def handler(_request: httpx.Request) -> httpx.Response:
|
|
264
|
+
return httpx.Response(200, json={"value": {"E2B_API_KEY": "k", "E2B_TEAM_ID": "team"}})
|
|
265
|
+
|
|
266
|
+
assert resolve_provider_credential(
|
|
267
|
+
"e2b",
|
|
268
|
+
owner_uid="owner-1",
|
|
269
|
+
iam_url="https://iam.example",
|
|
270
|
+
api_key="k",
|
|
271
|
+
transport=_credential_transport(handler),
|
|
272
|
+
) == {"E2B_API_KEY": "k", "E2B_TEAM_ID": "team"}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_an_owner_with_no_credential_is_refused_by_name_and_not_retried() -> None:
|
|
276
|
+
"""Never a fallback to whatever keys the worker holds: that is the one
|
|
277
|
+
thing D-8 exists to prevent."""
|
|
278
|
+
|
|
279
|
+
def handler(_request: httpx.Request) -> httpx.Response:
|
|
280
|
+
return httpx.Response(404, json={"detail": "No credential for that provider"})
|
|
281
|
+
|
|
282
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
283
|
+
resolve_provider_credential(
|
|
284
|
+
"modal",
|
|
285
|
+
owner_uid="owner-1",
|
|
286
|
+
iam_url="https://iam.example",
|
|
287
|
+
api_key="k",
|
|
288
|
+
transport=_credential_transport(handler),
|
|
289
|
+
)
|
|
290
|
+
assert raised.value.code is CAPABILITY_UNSUPPORTED
|
|
291
|
+
assert raised.value.retryable is False
|
|
292
|
+
assert "their own account" in str(raised.value)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_iam_being_away_is_retryable() -> None:
|
|
296
|
+
def handler(_request: httpx.Request) -> httpx.Response:
|
|
297
|
+
return httpx.Response(503, json={"detail": "away"})
|
|
298
|
+
|
|
299
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
300
|
+
resolve_provider_credential(
|
|
301
|
+
"daytona",
|
|
302
|
+
owner_uid="owner-1",
|
|
303
|
+
iam_url="https://iam.example",
|
|
304
|
+
api_key="k",
|
|
305
|
+
transport=_credential_transport(handler),
|
|
306
|
+
)
|
|
307
|
+
assert raised.value.retryable is True
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_datalayer_is_not_a_variant_a_credential_is_kept_for() -> None:
|
|
311
|
+
"""The platform's own builder uses the platform's own registry."""
|
|
312
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
313
|
+
resolve_provider_credential(
|
|
314
|
+
"datalayer", owner_uid="owner-1", iam_url="https://iam.example", api_key="k"
|
|
315
|
+
)
|
|
316
|
+
assert raised.value.code is CAPABILITY_UNSUPPORTED
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_a_credential_that_names_nothing_is_refused() -> None:
|
|
320
|
+
for value in ({}, "a string", None, {"": "v"}):
|
|
321
|
+
|
|
322
|
+
def handler(_request: httpx.Request, value=value) -> httpx.Response:
|
|
323
|
+
return httpx.Response(200, json={"value": value})
|
|
324
|
+
|
|
325
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
326
|
+
resolve_provider_credential(
|
|
327
|
+
"daytona",
|
|
328
|
+
owner_uid="owner-1",
|
|
329
|
+
iam_url="https://iam.example",
|
|
330
|
+
api_key="k",
|
|
331
|
+
transport=_credential_transport(handler),
|
|
332
|
+
)
|
|
333
|
+
assert raised.value.code is CAPABILITY_UNSUPPORTED, value
|
|
@@ -313,14 +313,14 @@ class TestTheHalfThatIsNotBuiltYet:
|
|
|
313
313
|
`ManagedBuilder` methods on all three."""
|
|
314
314
|
|
|
315
315
|
def test_modal_still_refuses_what_e2_05_did_not_build(self) -> None:
|
|
316
|
-
"""`build`/`inspect`/`exists` are E2-05's
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
"""`build`/`inspect`/`exists` are E2-05's, and `delete` is now too — it
|
|
317
|
+
collects the intermediate layers a build recorded (E2-05, E2-09).
|
|
318
|
+
`smoke_test` and `resolve` are still not built — see
|
|
319
|
+
test_environment_modal_builder.py for what is."""
|
|
319
320
|
builder = get_builder("modal")
|
|
320
321
|
calls = {
|
|
321
322
|
"smoke_test": lambda: builder.smoke_test(None), # type: ignore[arg-type]
|
|
322
323
|
"resolve": lambda: builder.resolve("geo@1"),
|
|
323
|
-
"delete": lambda: builder.delete(None), # type: ignore[arg-type]
|
|
324
324
|
}
|
|
325
325
|
for operation, call in calls.items():
|
|
326
326
|
with pytest.raises(EnvironmentsError) as raised:
|