code-sandboxes 1.9.4__tar.gz → 1.9.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/CHANGELOG.md +34 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/PKG-INFO +1 -1
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/datalayer.py +14 -4
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/attest.py +13 -3
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/bases.py +28 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/image_import.py +89 -39
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/policy.py +22 -1
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/resolve.py +108 -22
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_attest.py +48 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_bases.py +22 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_datalayer_builder.py +11 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_image_import.py +85 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_resolve.py +101 -6
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.gitignore +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/Dockerfile +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/LICENSE +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/Makefile +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/README.md +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/build.log +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/daytona.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/modal.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/spec.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/Makefile +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/README.md +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/package.json +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/README.md +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/pyproject.toml +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/schemas/environment-v1alpha1.json +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_daytona_builder.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_managed_builders.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_modal_builder.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_environment_spec.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/tests/test_the_kill_relaunch_drill.py +0 -0
|
@@ -8,6 +8,40 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 1.9.5
|
|
12
|
+
|
|
13
|
+
- **Attest waits for a just-pushed image's scan to exist**
|
|
14
|
+
(`environments.attest`; PLAN_ENV.md E1-08). Enhanced scanning starts after
|
|
15
|
+
the push, so the first answers for a new image are
|
|
16
|
+
`ScanNotFoundException`. Attest read that as an image with no scan and
|
|
17
|
+
failed the build at once (found live on r1, 2026-09-14). It is now waited
|
|
18
|
+
for like a running scan, within the same bound.
|
|
19
|
+
- **apt is pinned at, and installed from, the base channel's Ubuntu snapshot**
|
|
20
|
+
(`environments.bases`, `environments.resolve`,
|
|
21
|
+
`environments.adapters.datalayer`; PLAN_ENV.md E1-04). A channel records
|
|
22
|
+
the `snapshot.ubuntu.com` id its image was upgraded at. The solve runs
|
|
23
|
+
`apt-get` with `--snapshot` at that id, the lock records it
|
|
24
|
+
(`# datalayer-apt-snapshot:`), and the Datalayer builder installs its pins
|
|
25
|
+
from the same snapshot. This replaces a `deb` line that named only `main`
|
|
26
|
+
(`gdal-bin` is in `universe`) and left the live mirror enabled beside it.
|
|
27
|
+
`DATALAYER_APT_SNAPSHOT` must now be a snapshot id.
|
|
28
|
+
- **A private registry's credential is resolved and used** (`environments
|
|
29
|
+
.image_import`, `environments.resolve`; PLAN_ENV.md E3-04's second half).
|
|
30
|
+
`image.credentialSecretId` used to only clear the allowlist check; nothing
|
|
31
|
+
fetched or used the credential it named. It is now fetched through the
|
|
32
|
+
same IAM route a build secret is (E3-05), and sent as HTTP Basic on the
|
|
33
|
+
registry's own token exchange — the way `docker login` authenticates a
|
|
34
|
+
private pull — never to the registry named in the reference, and never
|
|
35
|
+
logged. `resolve_environment` and `resolve_image_base` take `owner_uid`
|
|
36
|
+
and `resolve_secret` to reach it; unused by a public registry or any other
|
|
37
|
+
source.
|
|
38
|
+
- **The Datalayer base channel's own five ESM-locked advisories are allowed**
|
|
39
|
+
(`environments.policy`; PLAN_ENV.md E1-08). Amazon Inspector marks all five
|
|
40
|
+
fixable, but the fix is an Ubuntu ESM package version a plain
|
|
41
|
+
`apt-get upgrade` cannot reach. Reviewed and added to `DEFAULT_POLICY
|
|
42
|
+
.allowed`, 2026-09-14, so they are recorded as allowed rather than missed,
|
|
43
|
+
and any other critical still blocks.
|
|
44
|
+
|
|
11
45
|
## 1.9.4
|
|
12
46
|
|
|
13
47
|
- **A pushed Environment image is decided by its linux/amd64 image's scan**
|
{code_sandboxes-1.9.4 → code_sandboxes-1.9.5}/code_sandboxes/environments/adapters/datalayer.py
RENAMED
|
@@ -68,7 +68,13 @@ from ..errors import (
|
|
|
68
68
|
EnvironmentsError,
|
|
69
69
|
)
|
|
70
70
|
from ..files import files_step
|
|
71
|
-
from ..resolve import
|
|
71
|
+
from ..resolve import (
|
|
72
|
+
WHEELHOUSE_IMAGE_PATH,
|
|
73
|
+
WHEELHOUSE_PATH,
|
|
74
|
+
apt_pins_in,
|
|
75
|
+
apt_snapshot_in,
|
|
76
|
+
locked_versions,
|
|
77
|
+
)
|
|
72
78
|
from ..spec import BuildSecret, Environment, command_names_secret
|
|
73
79
|
|
|
74
80
|
__all__ = [
|
|
@@ -310,14 +316,18 @@ class Builder:
|
|
|
310
316
|
lines.append(f"ENV {name}={shlex.quote(spec.env[name])}")
|
|
311
317
|
if apt:
|
|
312
318
|
# Pinned, and from the lock: `apt-get install <name>` a month later
|
|
313
|
-
# is a different artifact.
|
|
319
|
+
# is a different artifact. From the snapshot the pins were taken
|
|
320
|
+
# from, when the lock names one: a pinned version can leave the
|
|
321
|
+
# live mirror (D-9).
|
|
314
322
|
pinned = " ".join(f"{name}={apt[name]}" for name in sorted(apt))
|
|
323
|
+
snapshot = apt_snapshot_in(request.lock_text)
|
|
324
|
+
option = f" --snapshot {snapshot}" if snapshot else ""
|
|
315
325
|
lines.extend(
|
|
316
326
|
[
|
|
317
327
|
"USER root",
|
|
318
328
|
"RUN --mount=type=cache,target=/var/cache/apt,sharing=locked "
|
|
319
|
-
"apt-get update -qq \\\n"
|
|
320
|
-
f" && apt-get install -y --no-install-recommends {pinned} \\\n"
|
|
329
|
+
f"apt-get update -qq{option} \\\n"
|
|
330
|
+
f" && apt-get install -y --no-install-recommends{option} {pinned} \\\n"
|
|
321
331
|
" && rm -rf /var/lib/apt/lists/*",
|
|
322
332
|
]
|
|
323
333
|
)
|
|
@@ -530,10 +530,20 @@ def _is_missing(error: BaseException) -> bool:
|
|
|
530
530
|
return _error_code(error) in {
|
|
531
531
|
"ImageNotFoundException",
|
|
532
532
|
"RepositoryNotFoundException",
|
|
533
|
-
"ScanNotFoundException",
|
|
534
533
|
}
|
|
535
534
|
|
|
536
535
|
|
|
537
536
|
def _is_in_progress(error: BaseException) -> bool:
|
|
538
|
-
"""ECR answers a scan that has not finished as an error, not a status.
|
|
539
|
-
|
|
537
|
+
"""ECR answers a scan that has not finished as an error, not a status.
|
|
538
|
+
|
|
539
|
+
`ScanNotFoundException` is one of those answers: enhanced scanning starts
|
|
540
|
+
after the push, so an image pushed a moment ago has no scan yet (found
|
|
541
|
+
live on r1, 2026-09-14, the first build to read its image's scan rather
|
|
542
|
+
than its index's). It is waited for like a running scan, within the same
|
|
543
|
+
bound.
|
|
544
|
+
"""
|
|
545
|
+
return _error_code(error) in {
|
|
546
|
+
"ScanInProgressException",
|
|
547
|
+
"ScanNotFoundException",
|
|
548
|
+
"LimitExceededException",
|
|
549
|
+
}
|
|
@@ -36,6 +36,7 @@ __all__ = [
|
|
|
36
36
|
"BaseChannelUnpublishedError",
|
|
37
37
|
"approved_base",
|
|
38
38
|
"approved_repositories",
|
|
39
|
+
"channel_snapshot",
|
|
39
40
|
"is_approved_repository",
|
|
40
41
|
"resolve_base",
|
|
41
42
|
]
|
|
@@ -44,6 +45,8 @@ __all__ = [
|
|
|
44
45
|
ECR_BASE_PREFIX = "environments/base/"
|
|
45
46
|
|
|
46
47
|
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}")
|
|
48
|
+
#: A snapshot.ubuntu.com id: the moment, to the second, in UTC.
|
|
49
|
+
_SNAPSHOT = re.compile(r"\d{8}T\d{6}Z")
|
|
47
50
|
|
|
48
51
|
|
|
49
52
|
class ApprovedBase(BaseModel):
|
|
@@ -59,6 +62,11 @@ class ApprovedBase(BaseModel):
|
|
|
59
62
|
#: Channel, then variant, to the digest resolution pins. A channel with no
|
|
60
63
|
#: variant is approved and not yet published.
|
|
61
64
|
channels: dict[str, dict[str, str]] = Field(default_factory=dict)
|
|
65
|
+
#: Channel to the Ubuntu snapshot its apt versions are pinned against
|
|
66
|
+
#: (D-9): a ``snapshot.ubuntu.com`` id, taken just after the channel's
|
|
67
|
+
#: image upgraded its packages, so no pin is older than what the image
|
|
68
|
+
#: already carries. A channel with none pins against the base's mirror.
|
|
69
|
+
snapshots: dict[str, str] = Field(default_factory=dict)
|
|
62
70
|
|
|
63
71
|
@field_validator("channels")
|
|
64
72
|
@classmethod
|
|
@@ -75,6 +83,17 @@ class ApprovedBase(BaseModel):
|
|
|
75
83
|
for channel, digests in channels.items()
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
@field_validator("snapshots")
|
|
87
|
+
@classmethod
|
|
88
|
+
def _snapshot_ids_only(cls, snapshots: dict[str, str]) -> dict[str, str]:
|
|
89
|
+
for channel, snapshot in snapshots.items():
|
|
90
|
+
if not _SNAPSHOT.fullmatch(snapshot):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"channel {channel!r} pins apt to {snapshot!r}, "
|
|
93
|
+
"which is not a snapshot id like 20260914T150000Z"
|
|
94
|
+
)
|
|
95
|
+
return snapshots
|
|
96
|
+
|
|
78
97
|
@property
|
|
79
98
|
def name(self) -> str:
|
|
80
99
|
"""The base's name without its namespace: ``python-cpu``."""
|
|
@@ -103,6 +122,7 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
|
|
|
103
122
|
"sha256:cd09308a0c5e5adeec7fb5d8d29cf7455e78ba86f5c6c095a79068a280e1254f",
|
|
104
123
|
)
|
|
105
124
|
},
|
|
125
|
+
snapshots={"2026.09": "20260914T150000Z"},
|
|
106
126
|
),
|
|
107
127
|
# E2-17: jupyter-python-cuda plus the same layer.
|
|
108
128
|
ApprovedBase(
|
|
@@ -180,6 +200,14 @@ def resolve_base(
|
|
|
180
200
|
return digest
|
|
181
201
|
|
|
182
202
|
|
|
203
|
+
def channel_snapshot(
|
|
204
|
+
ref: str, channel: str, bases: dict[str, ApprovedBase] = APPROVED_BASES
|
|
205
|
+
) -> str:
|
|
206
|
+
"""The Ubuntu snapshot ``ref`` at ``channel`` pins apt against, or ``""`` for none (D-9)."""
|
|
207
|
+
base = bases.get(ref)
|
|
208
|
+
return "" if base is None else base.snapshots.get(channel, "")
|
|
209
|
+
|
|
210
|
+
|
|
183
211
|
def approved_repositories(bases: dict[str, ApprovedBase] = APPROVED_BASES) -> tuple[str, ...]:
|
|
184
212
|
"""Every repository name an approved base is published under."""
|
|
185
213
|
names: list[str] = []
|
|
@@ -13,20 +13,26 @@ path. Nothing downstream needs to know a build started from an import rather
|
|
|
13
13
|
than an approved base; the only difference is where the `FROM` digest comes
|
|
14
14
|
from.
|
|
15
15
|
|
|
16
|
-
**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
**A public registry needs no credential; a private one names its own**
|
|
17
|
+
(E3-04's second half). `credentialSecretId` on the spec (the same `dlsec_...`
|
|
18
|
+
shape a `BuildSecret` uses) is resolved through the same IAM route a build
|
|
19
|
+
secret is (E3-05) — the value is fetched once, at the moment the digest is
|
|
20
|
+
resolved, and handed to the registry's own token exchange as HTTP Basic,
|
|
21
|
+
never logged, never stored, never sent to the registry named in the
|
|
22
|
+
reference. A per-organization allowlist is still E3-06's own organization
|
|
23
|
+
policy document to hold; `DEFAULT_ALLOWED_REGISTRIES` below is a bootstrap
|
|
24
|
+
standing in for it before it exists, not a policy of its own — a registry
|
|
25
|
+
outside this list is refused unless a credential was referenced for it, on
|
|
26
|
+
the read that naming one is a deliberate private registry rather than a
|
|
27
|
+
mistake. When E3-06 lands, an organization's own list replaces this one
|
|
28
|
+
rather than adding to it.
|
|
24
29
|
|
|
25
30
|
@module code_sandboxes.environments.image_import
|
|
26
31
|
"""
|
|
27
32
|
|
|
28
33
|
from __future__ import annotations
|
|
29
34
|
|
|
35
|
+
import base64
|
|
30
36
|
import hashlib
|
|
31
37
|
import re
|
|
32
38
|
from dataclasses import dataclass
|
|
@@ -175,10 +181,11 @@ def refuse_unless_allowed(
|
|
|
175
181
|
A registry named alongside `credentialSecretId` (E3-04's private half) is
|
|
176
182
|
presumed a deliberate private one, its governance E3-06's own allowlist
|
|
177
183
|
to hold once it exists — this bootstrap list is only ever the *public*
|
|
178
|
-
default, never the only door.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
default, never the only door. The credential itself is resolved and used
|
|
185
|
+
at `resolve_image_digest` (E3-05's mechanism); a spec that clears this
|
|
186
|
+
check but names a secret IAM will not give, or that the registry refuses,
|
|
187
|
+
fails honestly there instead, the same as an approved base with no digest
|
|
188
|
+
published.
|
|
182
189
|
"""
|
|
183
190
|
if has_credential or image_registry_allowed(image, allowlist):
|
|
184
191
|
return
|
|
@@ -199,21 +206,68 @@ def _challenge(header: str) -> dict[str, str]:
|
|
|
199
206
|
return dict(_CHALLENGE_FIELD.findall(header or ""))
|
|
200
207
|
|
|
201
208
|
|
|
209
|
+
def _token_for_challenge(client: Any, header: str, *, registry: str, credential: str | None) -> str:
|
|
210
|
+
"""The pull token a `WWW-Authenticate: Bearer ...` challenge answers with.
|
|
211
|
+
|
|
212
|
+
With a `credential` (`username:password`), sent as HTTP Basic on this
|
|
213
|
+
request alone — exactly what `docker login` sends here — so the realm
|
|
214
|
+
mints a token scoped to what that credential can pull; with none, the
|
|
215
|
+
same request answers with a public, anonymous-scope token. Either way,
|
|
216
|
+
the credential itself never reaches the registry named in the reference,
|
|
217
|
+
only this one realm, and only as this one request's header.
|
|
218
|
+
"""
|
|
219
|
+
challenge = _challenge(header)
|
|
220
|
+
realm = challenge.get("realm")
|
|
221
|
+
if not realm:
|
|
222
|
+
raise EnvironmentsError(
|
|
223
|
+
PROVIDER_ERROR,
|
|
224
|
+
f"{registry} refused a manifest request with no way to ask for a token",
|
|
225
|
+
detail={"registry": registry, "status": 401},
|
|
226
|
+
)
|
|
227
|
+
headers = {}
|
|
228
|
+
if credential:
|
|
229
|
+
encoded = base64.b64encode(credential.encode("utf-8")).decode("ascii")
|
|
230
|
+
headers["Authorization"] = f"Basic {encoded}"
|
|
231
|
+
response = client.get(
|
|
232
|
+
realm,
|
|
233
|
+
params={
|
|
234
|
+
key: value
|
|
235
|
+
for key, value in (
|
|
236
|
+
("service", challenge.get("service")),
|
|
237
|
+
("scope", challenge.get("scope")),
|
|
238
|
+
)
|
|
239
|
+
if value
|
|
240
|
+
},
|
|
241
|
+
headers=headers,
|
|
242
|
+
)
|
|
243
|
+
response.raise_for_status()
|
|
244
|
+
return str(response.json().get("token") or "")
|
|
245
|
+
|
|
246
|
+
|
|
202
247
|
def resolve_image_digest(
|
|
203
248
|
image: ImageReference,
|
|
204
249
|
*,
|
|
205
250
|
transport: Any = None,
|
|
206
251
|
timeout: float = 10.0,
|
|
252
|
+
credential: str | None = None,
|
|
207
253
|
) -> str:
|
|
208
|
-
"""The reference's digest, over the registry's own v2 API
|
|
209
|
-
since only a public registry reaches this (§ this module's own docstring).
|
|
254
|
+
"""The reference's digest, over the registry's own v2 API.
|
|
210
255
|
|
|
211
256
|
Already pinned by digest, this is free: an operator who wrote `@sha256:…`
|
|
212
257
|
meant exactly that image, and nothing is asked of the registry. A tag is
|
|
213
|
-
resolved with one
|
|
214
|
-
`WWW-Authenticate` challenge for the
|
|
215
|
-
|
|
216
|
-
|
|
258
|
+
resolved with one manifest request, following the registry's own
|
|
259
|
+
`WWW-Authenticate` challenge for the pull token every registry implementing
|
|
260
|
+
the distribution spec answers with (D-9's base digests are pinned the same
|
|
261
|
+
way, just from a table Datalayer keeps instead of asked live).
|
|
262
|
+
|
|
263
|
+
With no `credential`, the challenge is answered anonymously — the only
|
|
264
|
+
path a public registry needs. With one, `username:password` (E3-04's
|
|
265
|
+
private half), it is sent as HTTP Basic on the token request the challenge
|
|
266
|
+
names, exactly what `docker login` sends there: the registry mints a
|
|
267
|
+
token scoped to what that credential can pull, private repositories
|
|
268
|
+
included, rather than the public-only anonymous one. The credential itself
|
|
269
|
+
is never sent to the registry named in the reference, and never appears in
|
|
270
|
+
a log line or an exception message — only the token that answers it.
|
|
217
271
|
"""
|
|
218
272
|
if image.pinned:
|
|
219
273
|
return image.digest
|
|
@@ -234,28 +288,12 @@ def resolve_image_digest(
|
|
|
234
288
|
try:
|
|
235
289
|
response = client.get(url, headers=headers)
|
|
236
290
|
if response.status_code == 401:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
f"{image.registry} refused an anonymous manifest request with no "
|
|
243
|
-
"way to ask for a token",
|
|
244
|
-
detail={"registry": image.registry, "status": 401},
|
|
245
|
-
)
|
|
246
|
-
token_response = client.get(
|
|
247
|
-
realm,
|
|
248
|
-
params={
|
|
249
|
-
key: value
|
|
250
|
-
for key, value in (
|
|
251
|
-
("service", challenge.get("service")),
|
|
252
|
-
("scope", challenge.get("scope")),
|
|
253
|
-
)
|
|
254
|
-
if value
|
|
255
|
-
},
|
|
291
|
+
token = _token_for_challenge(
|
|
292
|
+
client,
|
|
293
|
+
response.headers.get("www-authenticate", ""),
|
|
294
|
+
registry=image.registry,
|
|
295
|
+
credential=credential,
|
|
256
296
|
)
|
|
257
|
-
token_response.raise_for_status()
|
|
258
|
-
token = str(token_response.json().get("token") or "")
|
|
259
297
|
response = client.get(url, headers={**headers, "Authorization": f"Bearer {token}"})
|
|
260
298
|
except httpx.HTTPError as error:
|
|
261
299
|
raise EnvironmentsError(
|
|
@@ -263,6 +301,18 @@ def resolve_image_digest(
|
|
|
263
301
|
f"{image.registry} could not be reached to resolve {image}: {error}",
|
|
264
302
|
detail={"registry": image.registry, "reference": str(image)},
|
|
265
303
|
) from error
|
|
304
|
+
if response.status_code in (401, 403):
|
|
305
|
+
raise EnvironmentsError(
|
|
306
|
+
PROVIDER_ERROR,
|
|
307
|
+
f"{image.registry} refused the credential for {image}"
|
|
308
|
+
if credential
|
|
309
|
+
else f"{image} needs a credential to resolve at {image.registry}",
|
|
310
|
+
detail={
|
|
311
|
+
"registry": image.registry,
|
|
312
|
+
"reference": str(image),
|
|
313
|
+
"status": response.status_code,
|
|
314
|
+
},
|
|
315
|
+
)
|
|
266
316
|
if response.status_code == 404:
|
|
267
317
|
raise EnvironmentsError(
|
|
268
318
|
PROVIDER_ERROR,
|
|
@@ -129,8 +129,29 @@ class ScanPolicy:
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
#: Advisories allowed on the Datalayer base channel itself (D-9), reviewed and
|
|
133
|
+
#: accepted 2026-09-14 rather than silently missed: all five are in `ffmpeg`
|
|
134
|
+
#: (`CVE-2024-35366`, `CVE-2024-35367`, `CVE-2024-35368`, `CVE-2026-40962`) and
|
|
135
|
+
#: `libcjson1` (`CVE-2025-57052`, pulled in only by `librist4`, itself only
|
|
136
|
+
#: needed by `ffmpeg`), on `datalayer/python-cpu:2026.09`. Amazon Inspector
|
|
137
|
+
#: reports each as fixable, but the fix is an Ubuntu ESM (Extended Security
|
|
138
|
+
#: Maintenance) package version — `7:6.1.1-3ubuntu5+esm5` and the like — not
|
|
139
|
+
#: reachable by a plain `apt-get upgrade`, only by a Pro subscription token
|
|
140
|
+
#: this deployment does not hold. `ffmpeg` has no other installed package
|
|
141
|
+
#: depending on it (`apt-cache rdepends --installed`), so it is kept for what
|
|
142
|
+
#: it is: an image-wide convenience (matplotlib's `FFMpegWriter` and the like),
|
|
143
|
+
#: not something the sandbox contract's own doctor checks for. Revisit once
|
|
144
|
+
#: Ubuntu ships a non-ESM fix, or the channel drops `ffmpeg`.
|
|
145
|
+
_BASE_CHANNEL_ALLOWED: tuple[str, ...] = (
|
|
146
|
+
"CVE-2024-35366",
|
|
147
|
+
"CVE-2024-35367",
|
|
148
|
+
"CVE-2024-35368",
|
|
149
|
+
"CVE-2026-40962",
|
|
150
|
+
"CVE-2025-57052",
|
|
151
|
+
)
|
|
152
|
+
|
|
132
153
|
#: What every owner is scanned against until an organization says otherwise.
|
|
133
|
-
DEFAULT_POLICY = ScanPolicy()
|
|
154
|
+
DEFAULT_POLICY = ScanPolicy(allowed=_BASE_CHANNEL_ALLOWED)
|
|
134
155
|
|
|
135
156
|
|
|
136
157
|
@dataclass(frozen=True)
|
|
@@ -51,7 +51,8 @@ from functools import lru_cache
|
|
|
51
51
|
from pathlib import Path
|
|
52
52
|
from typing import Any, Callable, Protocol
|
|
53
53
|
|
|
54
|
-
from .bases import APPROVED_BASES, ApprovedBase, resolve_base
|
|
54
|
+
from .bases import APPROVED_BASES, ApprovedBase, channel_snapshot, resolve_base
|
|
55
|
+
from .build_secrets import resolve_build_secret
|
|
55
56
|
from .errors import (
|
|
56
57
|
CAPABILITY_UNSUPPORTED,
|
|
57
58
|
PACKAGE_NOT_FOUND,
|
|
@@ -67,10 +68,17 @@ from .image_import import (
|
|
|
67
68
|
refuse_unless_allowed,
|
|
68
69
|
resolve_image_digest,
|
|
69
70
|
)
|
|
70
|
-
from .spec import
|
|
71
|
+
from .spec import (
|
|
72
|
+
BuildSecret,
|
|
73
|
+
DependencyFileSpec,
|
|
74
|
+
Environment,
|
|
75
|
+
parse_environment,
|
|
76
|
+
parse_requirements_txt,
|
|
77
|
+
)
|
|
71
78
|
|
|
72
79
|
__all__ = [
|
|
73
80
|
"APT_PIN_PREFIX",
|
|
81
|
+
"APT_SNAPSHOT_PREFIX",
|
|
74
82
|
"CONSTRAINTS_PATH",
|
|
75
83
|
"LOCK_FORMAT",
|
|
76
84
|
"PROTECTED_PIN_PREFIX",
|
|
@@ -83,6 +91,7 @@ __all__ = [
|
|
|
83
91
|
"ResolveRunner",
|
|
84
92
|
"apt_pins",
|
|
85
93
|
"apt_pins_in",
|
|
94
|
+
"apt_snapshot_in",
|
|
86
95
|
"lock_document",
|
|
87
96
|
"locked_versions",
|
|
88
97
|
"merge_requirements",
|
|
@@ -117,6 +126,11 @@ WHEELHOUSE_PATH = Path(__file__).parent / "constraints" / "wheelhouse"
|
|
|
117
126
|
#: How an apt pin is written in the lock. A comment, so every reader of a
|
|
118
127
|
#: ``pip`` requirements file — the CLI's diff included — ignores it.
|
|
119
128
|
APT_PIN_PREFIX = "# datalayer-apt: "
|
|
129
|
+
#: The Ubuntu snapshot the apt pins were taken from, as the lock records it.
|
|
130
|
+
#: The builder installs the pins from the same snapshot, since a pinned
|
|
131
|
+
#: version can leave the live mirror (D-9).
|
|
132
|
+
APT_SNAPSHOT_PREFIX = "# datalayer-apt-snapshot: "
|
|
133
|
+
_SNAPSHOT_ID = re.compile(r"^\d{8}T\d{6}Z$")
|
|
120
134
|
|
|
121
135
|
#: How a protected pin is recorded in the lock.
|
|
122
136
|
PROTECTED_PIN_PREFIX = "# datalayer-protected: "
|
|
@@ -315,6 +329,11 @@ class ResolveRequest:
|
|
|
315
329
|
already has it baked in (E1-05); an imported image is somebody else's,
|
|
316
330
|
and cannot be assumed to."""
|
|
317
331
|
|
|
332
|
+
apt_snapshot: str = ""
|
|
333
|
+
"""The Ubuntu snapshot apt versions are pinned against, as
|
|
334
|
+
``20260914T150000Z``: the base channel's (D-9). Empty pins against the
|
|
335
|
+
mirror the base names."""
|
|
336
|
+
|
|
318
337
|
|
|
319
338
|
@dataclass
|
|
320
339
|
class ResolveOutcome:
|
|
@@ -325,6 +344,9 @@ class ResolveOutcome:
|
|
|
325
344
|
apt_source: str = ""
|
|
326
345
|
"""The mirror the apt versions were pinned against, for the lock's header."""
|
|
327
346
|
|
|
347
|
+
apt_snapshot: str = ""
|
|
348
|
+
"""The snapshot id they were pinned against, which the builder installs from."""
|
|
349
|
+
|
|
328
350
|
|
|
329
351
|
class ResolveRunner(Protocol):
|
|
330
352
|
"""Where a solve runs."""
|
|
@@ -599,6 +621,12 @@ class BuildkitResolveRunner:
|
|
|
599
621
|
self._tlskey = tlskey or ""
|
|
600
622
|
self._tlscacert = tlscacert or ""
|
|
601
623
|
self._address = address or ""
|
|
624
|
+
if apt_snapshot and not _SNAPSHOT_ID.match(apt_snapshot):
|
|
625
|
+
raise ValueError(
|
|
626
|
+
f"apt_snapshot must be a snapshot.ubuntu.com id like 20260914T150000Z, "
|
|
627
|
+
f"not {apt_snapshot!r}"
|
|
628
|
+
)
|
|
629
|
+
#: The deployment's override of the base channel's snapshot.
|
|
602
630
|
self._apt_snapshot = apt_snapshot
|
|
603
631
|
self._timeout = timeout
|
|
604
632
|
|
|
@@ -648,18 +676,15 @@ class BuildkitResolveRunner:
|
|
|
648
676
|
if request.apt:
|
|
649
677
|
# `--simulate` names every package apt would install, transitive
|
|
650
678
|
# ones included, each with the exact version the mirror serves at
|
|
651
|
-
# the channel's date (D-9).
|
|
652
|
-
snapshot
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
"> /etc/apt/sources.list.d/datalayer-snapshot.list"
|
|
658
|
-
)
|
|
679
|
+
# the channel's date (D-9). `--snapshot` points every suite and
|
|
680
|
+
# component the base's sources name at snapshot.ubuntu.com at that
|
|
681
|
+
# moment. The `deb` line this used to add named one component,
|
|
682
|
+
# `main`, which `gdal-bin` is not in, and left the live mirror
|
|
683
|
+
# enabled beside it (found 2026-09-14).
|
|
684
|
+
option = self._snapshot_option(request)
|
|
659
685
|
lines.append(
|
|
660
|
-
"RUN apt-get update -qq && apt-get install --simulate
|
|
661
|
-
+ " ".join(request.apt)
|
|
662
|
-
+ " > /solve/apt.txt"
|
|
686
|
+
f"RUN apt-get update -qq{option} && apt-get install --simulate "
|
|
687
|
+
f"--no-install-recommends{option} " + " ".join(request.apt) + " > /solve/apt.txt"
|
|
663
688
|
)
|
|
664
689
|
lines.extend(
|
|
665
690
|
[
|
|
@@ -671,6 +696,14 @@ class BuildkitResolveRunner:
|
|
|
671
696
|
lines.append("COPY --from=solve /solve/apt.txt /apt.txt")
|
|
672
697
|
return "\n".join(lines) + "\n"
|
|
673
698
|
|
|
699
|
+
def _snapshot(self, request: ResolveRequest) -> str:
|
|
700
|
+
"""The deployment's override, else the base channel's snapshot."""
|
|
701
|
+
return self._apt_snapshot or request.apt_snapshot
|
|
702
|
+
|
|
703
|
+
def _snapshot_option(self, request: ResolveRequest) -> str:
|
|
704
|
+
snapshot = self._snapshot(request)
|
|
705
|
+
return f" --snapshot {snapshot}" if snapshot else ""
|
|
706
|
+
|
|
674
707
|
def solve(
|
|
675
708
|
self, request: ResolveRequest, log: Callable[[str], None] | None = None
|
|
676
709
|
) -> ResolveOutcome:
|
|
@@ -737,12 +770,19 @@ class BuildkitResolveRunner:
|
|
|
737
770
|
if finished.returncode != 0:
|
|
738
771
|
raise parse_resolver_failure(finished.stderr or finished.stdout or "")
|
|
739
772
|
lock = (out / "lock.txt").read_text(encoding="utf-8")
|
|
740
|
-
pins, source = {}, ""
|
|
773
|
+
pins, source, snapshot = {}, "", ""
|
|
741
774
|
apt_file = out / "apt.txt"
|
|
742
775
|
if request.apt and apt_file.exists():
|
|
743
776
|
pins = apt_pins(apt_file.read_text(encoding="utf-8"))
|
|
744
|
-
|
|
745
|
-
|
|
777
|
+
snapshot = self._snapshot(request)
|
|
778
|
+
source = (
|
|
779
|
+
f"https://snapshot.ubuntu.com/ubuntu/{snapshot}"
|
|
780
|
+
if snapshot
|
|
781
|
+
else "the base channel's mirror"
|
|
782
|
+
)
|
|
783
|
+
return ResolveOutcome(
|
|
784
|
+
lock_text=lock, apt_pins=pins, apt_source=source, apt_snapshot=snapshot
|
|
785
|
+
)
|
|
746
786
|
|
|
747
787
|
def _environment(self, request: ResolveRequest) -> dict[str, str] | None:
|
|
748
788
|
"""The registry credential, as client-side auth: `buildkitd` holds none (D-17)."""
|
|
@@ -830,6 +870,16 @@ def apt_pins_in(lock_text: str) -> dict[str, str]:
|
|
|
830
870
|
return pins
|
|
831
871
|
|
|
832
872
|
|
|
873
|
+
def apt_snapshot_in(lock_text: str) -> str:
|
|
874
|
+
"""The Ubuntu snapshot a lock's apt pins were taken from, or ``""`` (D-9)."""
|
|
875
|
+
for raw in lock_text.splitlines():
|
|
876
|
+
line = raw.strip()
|
|
877
|
+
if line.startswith(APT_SNAPSHOT_PREFIX.strip()):
|
|
878
|
+
snapshot = line[len(APT_SNAPSHOT_PREFIX.strip()) :].strip()
|
|
879
|
+
return snapshot if _SNAPSHOT_ID.match(snapshot) else ""
|
|
880
|
+
return ""
|
|
881
|
+
|
|
882
|
+
|
|
833
883
|
def lock_document(
|
|
834
884
|
outcome: ResolveOutcome,
|
|
835
885
|
*,
|
|
@@ -856,6 +906,8 @@ def lock_document(
|
|
|
856
906
|
header.append(f"{APT_PIN_PREFIX}{pin[0]}={pin[1]}")
|
|
857
907
|
if outcome.apt_pins and outcome.apt_source:
|
|
858
908
|
header.append(f"# datalayer-apt-source: {outcome.apt_source}")
|
|
909
|
+
if outcome.apt_pins and outcome.apt_snapshot:
|
|
910
|
+
header.append(f"{APT_SNAPSHOT_PREFIX}{outcome.apt_snapshot}")
|
|
859
911
|
for constraint in merged.constraints:
|
|
860
912
|
header.append(f"{PROTECTED_PIN_PREFIX}{constraint}")
|
|
861
913
|
text = "\n".join(header) + "\n" + outcome.lock_text.lstrip("\n")
|
|
@@ -904,6 +956,8 @@ def resolve_image_base(
|
|
|
904
956
|
*,
|
|
905
957
|
allowlist: tuple[str, ...] = DEFAULT_ALLOWED_REGISTRIES,
|
|
906
958
|
transport: Any = None,
|
|
959
|
+
owner_uid: str = "",
|
|
960
|
+
resolve_secret: Callable[..., str] = resolve_build_secret,
|
|
907
961
|
) -> dict[str, str]:
|
|
908
962
|
"""Every wanted variant's base, from an imported image rather than an approved one (E3-04).
|
|
909
963
|
|
|
@@ -911,10 +965,18 @@ def resolve_image_base(
|
|
|
911
965
|
never a per-variant table the way an approved base's channel is, and
|
|
912
966
|
every variant this phase builds for is `linux/amd64` regardless (D-9's
|
|
913
967
|
own bases are single-arch too). Only a registry in ``allowlist`` is ever
|
|
914
|
-
resolved
|
|
915
|
-
|
|
916
|
-
without validating first, so nothing that
|
|
917
|
-
checked.
|
|
968
|
+
resolved, unless the spec names a private one's credential — the same
|
|
969
|
+
rule `spec_findings` already refuses the rest by, kept here too for
|
|
970
|
+
whatever resolves directly without validating first, so nothing that
|
|
971
|
+
reaches the network was never checked.
|
|
972
|
+
|
|
973
|
+
A private registry (``image.credential_secret_id`` set, E3-04's second
|
|
974
|
+
half) has its credential fetched from IAM through `resolve_secret` — the
|
|
975
|
+
same seam a build secret is resolved through (E3-05), since this is the
|
|
976
|
+
same mechanism: an id a spec names, a value fetched only at the moment
|
|
977
|
+
it is used, and never returned, logged, or stored anywhere past this call.
|
|
978
|
+
``owner_uid`` is required to fetch one; resolving an uncredentialed image
|
|
979
|
+
needs neither.
|
|
918
980
|
"""
|
|
919
981
|
image = environment.spec.build.image
|
|
920
982
|
if image is None or not image.reference.strip():
|
|
@@ -925,7 +987,11 @@ def resolve_image_base(
|
|
|
925
987
|
)
|
|
926
988
|
parsed = parse_image_reference(image.reference)
|
|
927
989
|
refuse_unless_allowed(parsed, allowlist, has_credential=bool(image.credential_secret_id))
|
|
928
|
-
|
|
990
|
+
credential = None
|
|
991
|
+
if image.credential_secret_id:
|
|
992
|
+
secret = BuildSecret(id=image.credential_secret_id, name="registry-credential")
|
|
993
|
+
credential = resolve_secret(secret, owner_uid=owner_uid)
|
|
994
|
+
digest = resolve_image_digest(parsed, transport=transport, credential=credential)
|
|
929
995
|
reference = f"{parsed.registry}/{parsed.repository}@{digest}"
|
|
930
996
|
return dict.fromkeys(variants, reference)
|
|
931
997
|
|
|
@@ -1097,6 +1163,8 @@ def resolve_environment(
|
|
|
1097
1163
|
uv: str | None = None,
|
|
1098
1164
|
pyproject_run: Callable[..., subprocess.CompletedProcess[str]] | None = None,
|
|
1099
1165
|
image_transport: Any = None,
|
|
1166
|
+
owner_uid: str = "",
|
|
1167
|
+
resolve_secret: Callable[..., str] = resolve_build_secret,
|
|
1100
1168
|
) -> dict[str, Any]:
|
|
1101
1169
|
"""A version's lock, and the base each variant builds from.
|
|
1102
1170
|
|
|
@@ -1132,6 +1200,12 @@ def resolve_environment(
|
|
|
1132
1200
|
the registry request runs over — injected by tests
|
|
1133
1201
|
(`httpx.MockTransport`), the real network otherwise. Unused by every
|
|
1134
1202
|
other source.
|
|
1203
|
+
owner_uid, resolve_secret
|
|
1204
|
+
An `image` source naming a private registry's `credentialSecretId`
|
|
1205
|
+
(E3-04's second half): the owner the credential belongs to, and how
|
|
1206
|
+
its value is fetched — IAM by default, the same seam a build secret
|
|
1207
|
+
(E3-05) is resolved through. Unused by a public registry or any
|
|
1208
|
+
other source.
|
|
1135
1209
|
|
|
1136
1210
|
Returns
|
|
1137
1211
|
-------
|
|
@@ -1166,7 +1240,13 @@ def resolve_environment(
|
|
|
1166
1240
|
)
|
|
1167
1241
|
wanted = sorted({str(variant) for variant in variants} or {"datalayer"})
|
|
1168
1242
|
resolved_bases = (
|
|
1169
|
-
resolve_image_base(
|
|
1243
|
+
resolve_image_base(
|
|
1244
|
+
environment,
|
|
1245
|
+
wanted,
|
|
1246
|
+
transport=image_transport,
|
|
1247
|
+
owner_uid=owner_uid,
|
|
1248
|
+
resolve_secret=resolve_secret,
|
|
1249
|
+
)
|
|
1170
1250
|
if source == "image"
|
|
1171
1251
|
else resolve_bases(environment, wanted, bases, registry=_registry_of(credential))
|
|
1172
1252
|
)
|
|
@@ -1204,6 +1284,12 @@ def resolve_environment(
|
|
|
1204
1284
|
base_reference=solving_in,
|
|
1205
1285
|
registry_auth=_registry_auth(credential),
|
|
1206
1286
|
bootstrap_uv=(source == "image"),
|
|
1287
|
+
# An imported image is not an approved base, and names no channel.
|
|
1288
|
+
apt_snapshot=(
|
|
1289
|
+
""
|
|
1290
|
+
if source == "image"
|
|
1291
|
+
else channel_snapshot(environment.spec.base.ref, environment.spec.base.channel, bases)
|
|
1292
|
+
),
|
|
1207
1293
|
)
|
|
1208
1294
|
outcome = (runner or BuildkitResolveRunner()).solve(request, say)
|
|
1209
1295
|
document = lock_document(
|