code-sandboxes 1.9.14__tar.gz → 1.9.18__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.14 → code_sandboxes-1.9.18}/CHANGELOG.md +62 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/PKG-INFO +1 -1
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/client.py +15 -1
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/attest.py +26 -1
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/bases.py +10 -1
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/files.py +18 -6
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/resolve.py +10 -8
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/resolve_conda.py +22 -23
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/spec.py +16 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/jupyter_server_sandbox.py +52 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/schemas/environment-v1alpha1.json +46 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_client.py +56 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_attest.py +41 -1
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_bases.py +16 -5
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_builders.py +52 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_resolve.py +15 -5
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_resolve_conda.py +9 -15
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_jupyter_server.py +74 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.gitignore +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/Dockerfile +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/LICENSE +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/Makefile +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/README.md +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/datalayer.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/daytona.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/adapters/modal.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/Makefile +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/README.md +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/package.json +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/README.md +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/pyproject.toml +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_datalayer_builder.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_daytona_builder.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_managed_builders.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_modal_builder.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_environment_spec.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/tests/test_the_kill_relaunch_drill.py +0 -0
|
@@ -8,6 +8,68 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 1.9.18
|
|
12
|
+
|
|
13
|
+
- **A lock no longer carries a wall clock, so the build cache can hit**
|
|
14
|
+
(`environments/resolve.py`, `resolve_conda.py`; PLAN_ENVS.md E1-26, D-12).
|
|
15
|
+
The lock's header carried a `# resolved-at:` line, and its digest is over
|
|
16
|
+
the whole text — so two resolves of the same spec, in the same base,
|
|
17
|
+
pinning the same 320 packages, produced two different digests. Found by
|
|
18
|
+
resolving one environment twice on r1 on 2026-09-16: the texts differed in
|
|
19
|
+
exactly that one line out of 5,388. Section 5's cache key is over the lock
|
|
20
|
+
digest, so D-12's build cache could never hit, and it never had:
|
|
21
|
+
`environments.cache.lookups` read `hit=false` twelve times out of twelve.
|
|
22
|
+
When a lock was resolved is on the lock document Runtimes stores, in its
|
|
23
|
+
`created_at`, which is where it belongs. `resolved_at` is gone from
|
|
24
|
+
`lock_document`, `conda_lock_document`, `resolve_environment` and
|
|
25
|
+
`resolve_conda_environment`; the two tests that asserted determinism by
|
|
26
|
+
freezing the clock now assert it without one.
|
|
27
|
+
|
|
28
|
+
## 1.9.17
|
|
29
|
+
|
|
30
|
+
- **An artifact's size is read from the registry** (`environments/attest.py`;
|
|
31
|
+
PLAN_ENVS.md E1-25). `attest_artifact` took `size_bytes` from its caller and
|
|
32
|
+
nobody ever passed one — the builder answers a reference, not a weight — so
|
|
33
|
+
every artefact was recorded with `sizeBytes: null` and
|
|
34
|
+
`environments.artifact.bytes`, the series section 14 tracks the artifact
|
|
35
|
+
size in, had no point in it although artifacts had been recorded (seen on r1,
|
|
36
|
+
2026-09-16, through the OTEL query API). `Attestor.size_of()` asks the
|
|
37
|
+
registry, with the client the scan is already read from, and a size that
|
|
38
|
+
cannot be read is logged rather than raised: a missing number on a dashboard
|
|
39
|
+
is not a reason to refuse an artifact that is otherwise signed. 3 new tests.
|
|
40
|
+
|
|
41
|
+
## 1.9.15
|
|
42
|
+
|
|
43
|
+
- **A restart restarts the kernel, not just this client's socket**
|
|
44
|
+
(`jupyter_server_sandbox`, `client`; PLAN_ENV.md E0-09, Appendix B check
|
|
45
|
+
7). `CodeSandboxClient.restart()` was `stop()` then `start()`, which is
|
|
46
|
+
right for a sandbox this process owns — it is destroyed and recreated, and
|
|
47
|
+
nothing survives — and wrong for one *attached* to a Jupyter server
|
|
48
|
+
somebody else runs, which is every Datalayer runtime pod: stopping drops
|
|
49
|
+
the websocket while the kernel process keeps running, so the reconnect
|
|
50
|
+
lands in the same interpreter with every global still set. Check 7 is
|
|
51
|
+
"nothing is assumed to persist across restarts", and it read `state survived the restart ('True')` for exactly this reason — found live on r1,
|
|
52
|
+
2026-09-16, the first drill whose smoke test reached the check.
|
|
53
|
+
`JupyterServerSandbox.restart_kernel()` now asks the server's own
|
|
54
|
+
`POST /api/kernels/{id}/restart` (the way `_do_interrupt` already uses the
|
|
55
|
+
API rather than the client's lifecycle) and reconnects onto the new
|
|
56
|
+
kernel; `restart()` prefers it and falls back to the lifecycle for every
|
|
57
|
+
variant that draws no such distinction. 7 new tests.
|
|
58
|
+
- **`datalayer/python-cpu:2026.09` repinned** to
|
|
59
|
+
`sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148`:
|
|
60
|
+
the contract layer now sets `MappingKernelManager.root_dir`, so kernels
|
|
61
|
+
start in `/home/datalayer/content`. The image already declared `WORKDIR`
|
|
62
|
+
there and `sandbox-contract/v1`'s User row already required it, but a
|
|
63
|
+
kernel's cwd is the Jupyter server's to choose and jupyter-python's config
|
|
64
|
+
roots it at `$HOME` — so every environment's kernel ran in
|
|
65
|
+
`/home/datalayer` and Appendix B check 2 read `cwd is '/home/datalayer', not '/home/datalayer/content'`. The file browser stays rooted at `$HOME`,
|
|
66
|
+
where a person expects to see everything they have; only the kernel moves.
|
|
67
|
+
- Two assertions that had rotted through three base releases are pinned in
|
|
68
|
+
one place again: the channel's digest and its apt snapshot were duplicated
|
|
69
|
+
across `test_environment_bases.py` and `test_environment_resolve.py`, and
|
|
70
|
+
2026-09-15's and 2026-09-16's releases left both red rather than catching
|
|
71
|
+
anything.
|
|
72
|
+
|
|
11
73
|
## 1.9.14
|
|
12
74
|
|
|
13
75
|
- **`datalayer/python-cpu:2026.09` base channel repinned** to the rebuilt
|
|
@@ -508,7 +508,21 @@ class CodeSandboxClient:
|
|
|
508
508
|
return self.is_started
|
|
509
509
|
|
|
510
510
|
def restart(self) -> None:
|
|
511
|
-
"""Restart the wrapped sandbox
|
|
511
|
+
"""Restart the wrapped sandbox, clearing what it was holding.
|
|
512
|
+
|
|
513
|
+
A sandbox this process owns is restarted by its own lifecycle: stop
|
|
514
|
+
and start destroy and recreate it, and nothing survives. One that is
|
|
515
|
+
merely *attached* to a server somebody else runs — a Jupyter server
|
|
516
|
+
in a Datalayer runtime pod — is not: stopping drops this client's
|
|
517
|
+
websocket while the kernel process goes on running, and starting
|
|
518
|
+
reconnects to the same interpreter with every global still set. A
|
|
519
|
+
sandbox that knows how to restart what it is attached to says so with
|
|
520
|
+
`restart_kernel`, and that is used in preference; the lifecycle is
|
|
521
|
+
the fallback for every variant that has no such distinction.
|
|
522
|
+
"""
|
|
523
|
+
restart_kernel = getattr(self._sandbox, "restart_kernel", None)
|
|
524
|
+
if callable(restart_kernel) and restart_kernel():
|
|
525
|
+
return
|
|
512
526
|
self._sandbox.stop()
|
|
513
527
|
self._sandbox.start()
|
|
514
528
|
|
|
@@ -536,10 +536,35 @@ class Attestor:
|
|
|
536
536
|
signature_ref=signature,
|
|
537
537
|
sbom_ref=sbom_ref or f"{registry}/{repository}@{digest}.sbom",
|
|
538
538
|
provenance_ref=provenance_ref or f"{registry}/{repository}@{digest}.att",
|
|
539
|
-
size_bytes=size_bytes
|
|
539
|
+
size_bytes=size_bytes
|
|
540
|
+
if size_bytes is not None
|
|
541
|
+
else self.size_of(repository=repository, digest=digest),
|
|
540
542
|
signed_now=signed_now,
|
|
541
543
|
)
|
|
542
544
|
|
|
545
|
+
def size_of(self, *, repository: str, digest: str) -> int | None:
|
|
546
|
+
"""What the registry says the artifact weighs, or None.
|
|
547
|
+
|
|
548
|
+
Nobody hands the size down: the builder answers a reference, not a
|
|
549
|
+
weight, so until this asked the registry `size_bytes` was always None
|
|
550
|
+
— and with it `environments.artifact.bytes`, the series section 14
|
|
551
|
+
tracks the artifact size in, which had no point in it on 2026-09-16
|
|
552
|
+
although artifacts had been recorded. The registry has known all
|
|
553
|
+
along; the scan is read from the same client.
|
|
554
|
+
|
|
555
|
+
Never a reason to fail an attestation: a size that could not be read
|
|
556
|
+
is a missing number on a dashboard, and the artifact is still signed.
|
|
557
|
+
"""
|
|
558
|
+
try:
|
|
559
|
+
images = self._client().describe_images(
|
|
560
|
+
repositoryName=repository, imageIds=[{"imageDigest": digest}]
|
|
561
|
+
)["imageDetails"]
|
|
562
|
+
except Exception as error:
|
|
563
|
+
self._log(f"The artifact's size could not be read: {error}")
|
|
564
|
+
return None
|
|
565
|
+
size = (images[0] or {}).get("imageSizeInBytes") if images else None
|
|
566
|
+
return int(size) if size else None
|
|
567
|
+
|
|
543
568
|
def _client(self) -> Any:
|
|
544
569
|
if self._ecr is None:
|
|
545
570
|
try:
|
|
@@ -126,7 +126,16 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
|
|
|
126
126
|
# `.spec.VARIANTS`, spelled out: `spec` imports from this
|
|
127
127
|
# module, so importing it back here would be circular.
|
|
128
128
|
("datalayer", "e2b", "daytona", "modal"),
|
|
129
|
-
|
|
129
|
+
# 2026-09-16: kernels now start in the contract's own
|
|
130
|
+
# working directory. The image already declared `WORKDIR
|
|
131
|
+
# /home/datalayer/content`, but a kernel's cwd is the
|
|
132
|
+
# Jupyter server's to choose, and jupyter-python's config
|
|
133
|
+
# roots it at `$HOME` — so every environment's kernel ran
|
|
134
|
+
# in `/home/datalayer` and Appendix B check 2 read "cwd is
|
|
135
|
+
# '/home/datalayer', not '/home/datalayer/content'". The
|
|
136
|
+
# contract layer now sets `MappingKernelManager.root_dir`,
|
|
137
|
+
# which moves the kernel without moving the file browser.
|
|
138
|
+
"sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148",
|
|
130
139
|
)
|
|
131
140
|
},
|
|
132
141
|
snapshots={"2026.09": "20260916T120000Z"},
|
|
@@ -25,12 +25,15 @@ __all__ = ["build_entries", "files_step"]
|
|
|
25
25
|
def build_entries(
|
|
26
26
|
environment: Environment, *, source_of: Callable[[FileEntry], str] | None = None
|
|
27
27
|
) -> list[BuildEntry]:
|
|
28
|
-
"""The spec's files as build entries
|
|
28
|
+
"""The spec's baked files as build entries: ``files`` then ``contentsBuild``.
|
|
29
29
|
|
|
30
|
-
``
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
``files`` a user uploaded (referenced by ``contentRef``) and
|
|
31
|
+
``contentsBuild`` fetched from an external ``source`` are both baked the
|
|
32
|
+
same way, so they are the same kind of entry here. ``source_of`` turns a
|
|
33
|
+
``contentRef`` into a URL the build can fetch — a presigned URL for a
|
|
34
|
+
``blob://`` reference — and defaults to the reference itself; a
|
|
35
|
+
``contentsBuild`` source is already a URL and is used as is. A file without
|
|
36
|
+
its sha256 is refused: the build verifies every byte it bakes.
|
|
34
37
|
"""
|
|
35
38
|
entries: list[BuildEntry] = []
|
|
36
39
|
for index, entry in enumerate(environment.spec.files):
|
|
@@ -48,6 +51,15 @@ def build_entries(
|
|
|
48
51
|
size_bytes=entry.size_bytes,
|
|
49
52
|
)
|
|
50
53
|
)
|
|
54
|
+
for built in environment.spec.contents_build:
|
|
55
|
+
entries.append(
|
|
56
|
+
BuildEntry(
|
|
57
|
+
source_uri=built.source,
|
|
58
|
+
destination_path=built.path,
|
|
59
|
+
sha256=built.sha256,
|
|
60
|
+
size_bytes=built.size_bytes,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
51
63
|
return entries
|
|
52
64
|
|
|
53
65
|
|
|
@@ -63,7 +75,7 @@ def files_step(
|
|
|
63
75
|
"""
|
|
64
76
|
if variant not in VARIANTS:
|
|
65
77
|
raise ValueError(f"{variant!r} is not a variant")
|
|
66
|
-
if not environment.spec.files:
|
|
78
|
+
if not environment.spec.files and not environment.spec.contents_build:
|
|
67
79
|
return []
|
|
68
80
|
build = EnvironmentBuild(
|
|
69
81
|
environment=environment.metadata.name,
|
|
@@ -889,7 +889,6 @@ def lock_document(
|
|
|
889
889
|
python_version: str,
|
|
890
890
|
base_reference: str,
|
|
891
891
|
merged: MergedRequirements,
|
|
892
|
-
resolved_at: datetime | None = None,
|
|
893
892
|
) -> dict[str, Any]:
|
|
894
893
|
"""The stored lock: its text, its digest, and what a reader needs from it.
|
|
895
894
|
|
|
@@ -897,11 +896,19 @@ def lock_document(
|
|
|
897
896
|
output, so the document is still a requirements file — ``pip install -r``
|
|
898
897
|
reads it, and so does every tool that only knows that format — while
|
|
899
898
|
saying everything the build installs.
|
|
899
|
+
|
|
900
|
+
**Nothing here is a wall clock.** The header carried a ``# resolved-at:``
|
|
901
|
+
line until 2026-09-16, and since the digest is over the whole text, two
|
|
902
|
+
resolves of the same spec, in the same base, pinning the same 320 packages
|
|
903
|
+
produced two different digests — the texts differed in that one line out
|
|
904
|
+
of 5,388, found by resolving the same environment twice on r1. The cache
|
|
905
|
+
key of section 5 is over the lock digest, so D-12's build cache could
|
|
906
|
+
never hit: `environments.cache.lookups` read `hit=false` 12 times out of
|
|
907
|
+
12. When the lock was resolved is on the lock document Runtimes stores, in
|
|
908
|
+
its ``created_at``, where it belongs.
|
|
900
909
|
"""
|
|
901
|
-
when = (resolved_at or _utcnow()).replace(microsecond=0).isoformat()
|
|
902
910
|
header = [
|
|
903
911
|
"# Resolved by Datalayer (PLAN_ENV.md D-9). Do not edit: a change makes a new version.",
|
|
904
|
-
f"# resolved-at: {when}",
|
|
905
912
|
f"# python: {python_version}",
|
|
906
913
|
f"# base: {base_reference}",
|
|
907
914
|
]
|
|
@@ -1163,7 +1170,6 @@ def resolve_environment(
|
|
|
1163
1170
|
runner: ResolveRunner | None = None,
|
|
1164
1171
|
conda_runner: CondaResolveRunner | None = None,
|
|
1165
1172
|
bases: dict[str, ApprovedBase] = APPROVED_BASES,
|
|
1166
|
-
resolved_at: datetime | None = None,
|
|
1167
1173
|
uv: str | None = None,
|
|
1168
1174
|
pyproject_run: Callable[..., subprocess.CompletedProcess[str]] | None = None,
|
|
1169
1175
|
image_transport: Any = None,
|
|
@@ -1198,8 +1204,6 @@ def resolve_environment(
|
|
|
1198
1204
|
bases
|
|
1199
1205
|
The approved bases, injected by tests and by a plane whose channel is
|
|
1200
1206
|
published somewhere else.
|
|
1201
|
-
resolved_at
|
|
1202
|
-
The moment the lock records. Now by default.
|
|
1203
1207
|
uv, pyproject_run
|
|
1204
1208
|
A `pyproject` `dependencyFile` source's own verification (E3-01):
|
|
1205
1209
|
the `uv` to check and export with, and how it is run — injected by
|
|
@@ -1274,7 +1278,6 @@ def resolve_environment(
|
|
|
1274
1278
|
credential=credential,
|
|
1275
1279
|
log=say,
|
|
1276
1280
|
runner=conda_runner,
|
|
1277
|
-
resolved_at=resolved_at,
|
|
1278
1281
|
)
|
|
1279
1282
|
if dependency_file.source_format == "pyproject":
|
|
1280
1283
|
# Verified, not re-resolved (E3-01): the author's own uv.lock is
|
|
@@ -1321,7 +1324,6 @@ def resolve_environment(
|
|
|
1321
1324
|
python_version=environment.spec.language.version,
|
|
1322
1325
|
base_reference=solving_in,
|
|
1323
1326
|
merged=merged,
|
|
1324
|
-
resolved_at=resolved_at,
|
|
1325
1327
|
)
|
|
1326
1328
|
say(
|
|
1327
1329
|
f"Locked {document['package_count']} packages"
|
{code_sandboxes-1.9.14 → code_sandboxes-1.9.18}/code_sandboxes/environments/resolve_conda.py
RENAMED
|
@@ -644,24 +644,28 @@ class BuildkitCondaResolveRunner:
|
|
|
644
644
|
carries the whole of what the solve resolved, not the conda layer alone.
|
|
645
645
|
"""
|
|
646
646
|
micromamba = shlex.quote(MICROMAMBA_BINARY)
|
|
647
|
-
return
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
647
|
+
return (
|
|
648
|
+
"\n".join(
|
|
649
|
+
[
|
|
650
|
+
f"FROM {request.base_reference} AS solve",
|
|
651
|
+
"USER root",
|
|
652
|
+
"WORKDIR /solve",
|
|
653
|
+
micromamba_bootstrap_dockerfile_line(),
|
|
654
|
+
"COPY environment.yml ./environment.yml",
|
|
655
|
+
"ENV PIP_FIND_LINKS=" + shlex.quote(WHEELHOUSE_IMAGE_PATH),
|
|
656
|
+
"RUN --mount=type=cache,target=/opt/conda/pkgs "
|
|
657
|
+
f"{micromamba} create --yes --prefix /solve/prefix "
|
|
658
|
+
f"--platform {shlex.quote(request.platform)} --file environment.yml",
|
|
659
|
+
f"RUN {micromamba} env export --explicit "
|
|
660
|
+
"--prefix /solve/prefix > /solve/lock.txt",
|
|
661
|
+
f"RUN {micromamba} env export --prefix /solve/prefix > /solve/pip-env.yml",
|
|
662
|
+
"FROM scratch",
|
|
663
|
+
"COPY --from=solve /solve/lock.txt /lock.txt",
|
|
664
|
+
"COPY --from=solve /solve/pip-env.yml /pip-env.yml",
|
|
665
|
+
]
|
|
666
|
+
)
|
|
667
|
+
+ "\n"
|
|
668
|
+
)
|
|
665
669
|
|
|
666
670
|
def solve(
|
|
667
671
|
self, request: CondaResolveRequest, log: Callable[[str], None] | None = None
|
|
@@ -800,7 +804,6 @@ def conda_lock_document(
|
|
|
800
804
|
base_reference: str,
|
|
801
805
|
merged: MergedRequirements,
|
|
802
806
|
platform: str = CONDA_PLATFORM,
|
|
803
|
-
resolved_at: datetime | None = None,
|
|
804
807
|
) -> dict[str, Any]:
|
|
805
808
|
"""The stored conda lock: its text, its digest, and what a reader needs.
|
|
806
809
|
|
|
@@ -814,10 +817,8 @@ def conda_lock_document(
|
|
|
814
817
|
could read the prefix back, and the merged requirements otherwise, so it is
|
|
815
818
|
always complete rather than the protected pins alone.
|
|
816
819
|
"""
|
|
817
|
-
when = (resolved_at or _utcnow()).replace(microsecond=0).isoformat()
|
|
818
820
|
header = [
|
|
819
821
|
"# Resolved by Datalayer (PLAN_ENV.md D-9). Do not edit: a change makes a new version.",
|
|
820
|
-
f"# resolved-at: {when}",
|
|
821
822
|
f"# python: {python_version}",
|
|
822
823
|
f"# platform: {platform}",
|
|
823
824
|
f"# base: {base_reference}",
|
|
@@ -854,7 +855,6 @@ def resolve_conda_environment(
|
|
|
854
855
|
credential: Any = None,
|
|
855
856
|
log: Callable[[str], None] | None = None,
|
|
856
857
|
runner: CondaResolveRunner | None = None,
|
|
857
|
-
resolved_at: datetime | None = None,
|
|
858
858
|
) -> dict[str, Any]:
|
|
859
859
|
"""A conda version's lock, from its ``environment.yml`` (E3-02).
|
|
860
860
|
|
|
@@ -892,7 +892,6 @@ def resolve_conda_environment(
|
|
|
892
892
|
base_reference=solving_in,
|
|
893
893
|
merged=merged,
|
|
894
894
|
platform=platform,
|
|
895
|
-
resolved_at=resolved_at,
|
|
896
895
|
)
|
|
897
896
|
say(f"Locked {document['package_count']} conda packages as {document['digest']}")
|
|
898
897
|
return {**document, "resolved_bases": dict(resolved_bases)}
|
|
@@ -259,6 +259,21 @@ class FileEntry(_Model):
|
|
|
259
259
|
sha256: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
class ContentsBuildEntry(_Model):
|
|
263
|
+
"""One immutable file the Environment bakes from an external source.
|
|
264
|
+
|
|
265
|
+
Unlike ``files`` — which a user uploads, referenced by ``contentRef`` — a
|
|
266
|
+
``contentsBuild`` entry names an external ``source`` URL fetched at build
|
|
267
|
+
time and verified against ``sha256`` (required: the build verifies every
|
|
268
|
+
byte it bakes). Both are baked the same way, into every provider artifact.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
source: str
|
|
272
|
+
path: str
|
|
273
|
+
sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
274
|
+
size_bytes: int | None = Field(default=None, ge=0)
|
|
275
|
+
|
|
276
|
+
|
|
262
277
|
class Commands(_Model):
|
|
263
278
|
post_install: list[str] = Field(default_factory=list)
|
|
264
279
|
|
|
@@ -365,6 +380,7 @@ class EnvironmentSpec(_Model):
|
|
|
365
380
|
platform: Platform = Field(default_factory=Platform)
|
|
366
381
|
packages: Packages = Field(default_factory=Packages)
|
|
367
382
|
files: list[FileEntry] = Field(default_factory=list)
|
|
383
|
+
contents_build: list[ContentsBuildEntry] = Field(default_factory=list)
|
|
368
384
|
env: dict[str, str] = Field(default_factory=dict)
|
|
369
385
|
commands: Commands = Field(default_factory=Commands)
|
|
370
386
|
build_secrets: list[BuildSecret] = Field(default_factory=list)
|
|
@@ -607,6 +607,58 @@ class JupyterServerSandbox(Sandbox):
|
|
|
607
607
|
logger.warning(f"Failed to interrupt Jupyter kernel: {e}")
|
|
608
608
|
return False
|
|
609
609
|
|
|
610
|
+
def restart_kernel(self) -> bool:
|
|
611
|
+
"""Restart the kernel itself, through the server's own REST API.
|
|
612
|
+
|
|
613
|
+
Not `stop()` then `start()`: those are this *client's* lifecycle, and
|
|
614
|
+
when the server is somebody else's — a Datalayer runtime pod, which
|
|
615
|
+
is every attached sandbox — stopping the client only drops the
|
|
616
|
+
websocket. The kernel is a process on the server and keeps running,
|
|
617
|
+
so a reconnect lands back in the same interpreter with every global
|
|
618
|
+
still set.
|
|
619
|
+
|
|
620
|
+
That is what Appendix B check 7 ("nothing is assumed to persist
|
|
621
|
+
across restarts") measures, and it read `state survived the restart`
|
|
622
|
+
for exactly this reason — found live on r1, 2026-09-16, the first
|
|
623
|
+
drill whose smoke test reached the check. `POST
|
|
624
|
+
/api/kernels/{id}/restart` is the one that restarts the kernel, the
|
|
625
|
+
same way `_do_interrupt` already uses the API rather than the
|
|
626
|
+
client's own lifecycle.
|
|
627
|
+
|
|
628
|
+
Answers whether the server accepted it; never raises, so a caller
|
|
629
|
+
that cannot restart reports a failed check rather than an error.
|
|
630
|
+
"""
|
|
631
|
+
if not self._server_url or not self._client:
|
|
632
|
+
return False
|
|
633
|
+
kernel_id = getattr(self._client, "id", None)
|
|
634
|
+
if not kernel_id:
|
|
635
|
+
return False
|
|
636
|
+
try:
|
|
637
|
+
response = requests.post(
|
|
638
|
+
f"{self._server_url}/api/kernels/{kernel_id}/restart",
|
|
639
|
+
params={"token": self._token},
|
|
640
|
+
headers=self._headers or None,
|
|
641
|
+
timeout=30,
|
|
642
|
+
)
|
|
643
|
+
except Exception as error:
|
|
644
|
+
# A restart that cannot even be asked for is a failed check, not
|
|
645
|
+
# an error to raise at the caller, exactly as `_do_interrupt` is.
|
|
646
|
+
logger.warning(f"Failed to restart Jupyter kernel: {error}")
|
|
647
|
+
return False
|
|
648
|
+
if not response.ok:
|
|
649
|
+
logger.warning(
|
|
650
|
+
"Failed to restart Jupyter kernel: the server answered %s", response.status_code
|
|
651
|
+
)
|
|
652
|
+
return False
|
|
653
|
+
# The websocket the client holds is to the kernel that has just been
|
|
654
|
+
# replaced; reconnecting is what makes the next execution land in the
|
|
655
|
+
# new interpreter rather than on a channel nobody is reading.
|
|
656
|
+
with contextlib.suppress(Exception):
|
|
657
|
+
self._client.stop()
|
|
658
|
+
with contextlib.suppress(Exception):
|
|
659
|
+
self._client.start()
|
|
660
|
+
return True
|
|
661
|
+
|
|
610
662
|
@marks_execution
|
|
611
663
|
def run_code( # noqa: C901
|
|
612
664
|
self,
|
|
@@ -152,6 +152,45 @@
|
|
|
152
152
|
"title": "Compatibility",
|
|
153
153
|
"type": "object"
|
|
154
154
|
},
|
|
155
|
+
"ContentsBuildEntry": {
|
|
156
|
+
"additionalProperties": false,
|
|
157
|
+
"description": "One immutable file the Environment bakes from an external source.\n\nUnlike ``files`` \u2014 which a user uploads, referenced by ``contentRef`` \u2014 a\n``contentsBuild`` entry names an external ``source`` URL fetched at build\ntime and verified against ``sha256`` (required: the build verifies every\nbyte it bakes). Both are baked the same way, into every provider artifact.",
|
|
158
|
+
"properties": {
|
|
159
|
+
"path": {
|
|
160
|
+
"title": "Path",
|
|
161
|
+
"type": "string"
|
|
162
|
+
},
|
|
163
|
+
"sha256": {
|
|
164
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
165
|
+
"title": "Sha256",
|
|
166
|
+
"type": "string"
|
|
167
|
+
},
|
|
168
|
+
"sizeBytes": {
|
|
169
|
+
"anyOf": [
|
|
170
|
+
{
|
|
171
|
+
"minimum": 0,
|
|
172
|
+
"type": "integer"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"type": "null"
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
"default": null,
|
|
179
|
+
"title": "Sizebytes"
|
|
180
|
+
},
|
|
181
|
+
"source": {
|
|
182
|
+
"title": "Source",
|
|
183
|
+
"type": "string"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"required": [
|
|
187
|
+
"source",
|
|
188
|
+
"path",
|
|
189
|
+
"sha256"
|
|
190
|
+
],
|
|
191
|
+
"title": "ContentsBuildEntry",
|
|
192
|
+
"type": "object"
|
|
193
|
+
},
|
|
155
194
|
"DependencyFileSpec": {
|
|
156
195
|
"additionalProperties": false,
|
|
157
196
|
"description": "A `requirements.txt`, a `pyproject.toml` with its `uv.lock`, or a conda\n`environment.yml` (E3-01, E3-02).\n\n``requirements`` resolves the way ``packages`` does \u2014 the protected\nconstraints merged in, the same solve. ``pyproject`` does not resolve at\nall: its own ``uv.lock`` is verified against the current\n``pyproject.toml`` and exported, never re-solved, because a lock the\nauthor already made is the whole point of bringing one. ``conda`` resolves\nthe ``environment.yml`` in its own ``micromamba`` solve into an explicit\nlock, with the protected constraints merged over its ``pip:`` layer.",
|
|
@@ -202,6 +241,13 @@
|
|
|
202
241
|
"compatibility": {
|
|
203
242
|
"$ref": "#/$defs/Compatibility"
|
|
204
243
|
},
|
|
244
|
+
"contentsBuild": {
|
|
245
|
+
"items": {
|
|
246
|
+
"$ref": "#/$defs/ContentsBuildEntry"
|
|
247
|
+
},
|
|
248
|
+
"title": "Contentsbuild",
|
|
249
|
+
"type": "array"
|
|
250
|
+
},
|
|
205
251
|
"contract": {
|
|
206
252
|
"default": "sandbox-contract/v1",
|
|
207
253
|
"title": "Contract",
|
|
@@ -157,6 +157,62 @@ def test_variables_interrupt_and_restart_delegate_to_sandbox():
|
|
|
157
157
|
assert client.is_alive() is True
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
class TestRestartingWhatIsActuallyHoldingTheState:
|
|
161
|
+
"""A restart has to clear the interpreter, not just this client's socket.
|
|
162
|
+
|
|
163
|
+
Appendix B check 7 is "nothing is assumed to persist across restarts". A
|
|
164
|
+
sandbox this process owns is cleared by its own lifecycle; one attached
|
|
165
|
+
to somebody else's Jupyter server is not, because the kernel outlives
|
|
166
|
+
the websocket. Found live on r1, 2026-09-16: `state survived the
|
|
167
|
+
restart`.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def test_a_sandbox_that_can_restart_its_kernel_is_asked_to(self):
|
|
171
|
+
class _AttachedSandbox(_FakeSandbox):
|
|
172
|
+
def __init__(self):
|
|
173
|
+
super().__init__()
|
|
174
|
+
self.kernel_restarts = 0
|
|
175
|
+
|
|
176
|
+
def restart_kernel(self):
|
|
177
|
+
self.kernel_restarts += 1
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
sandbox = _AttachedSandbox()
|
|
181
|
+
sandbox.start()
|
|
182
|
+
CodeSandboxClient(sandbox).restart()
|
|
183
|
+
|
|
184
|
+
assert sandbox.kernel_restarts == 1
|
|
185
|
+
# And the lifecycle was left alone: stopping it would have dropped a
|
|
186
|
+
# connection the restarted kernel is still reachable on.
|
|
187
|
+
assert sandbox.is_started is True
|
|
188
|
+
|
|
189
|
+
def test_a_kernel_restart_that_fails_falls_back_to_the_lifecycle(self):
|
|
190
|
+
class _RefusingSandbox(_FakeSandbox):
|
|
191
|
+
def __init__(self):
|
|
192
|
+
super().__init__()
|
|
193
|
+
self.stops = 0
|
|
194
|
+
|
|
195
|
+
def restart_kernel(self):
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def stop(self):
|
|
199
|
+
self.stops += 1
|
|
200
|
+
super().stop()
|
|
201
|
+
|
|
202
|
+
sandbox = _RefusingSandbox()
|
|
203
|
+
sandbox.start()
|
|
204
|
+
CodeSandboxClient(sandbox).restart()
|
|
205
|
+
|
|
206
|
+
assert sandbox.stops == 1, "a refused kernel restart still restarts the sandbox"
|
|
207
|
+
assert sandbox.is_started is True
|
|
208
|
+
|
|
209
|
+
def test_a_sandbox_with_no_kernel_of_its_own_restarts_as_it_always_did(self):
|
|
210
|
+
sandbox = _FakeSandbox()
|
|
211
|
+
sandbox.start()
|
|
212
|
+
CodeSandboxClient(sandbox).restart()
|
|
213
|
+
assert sandbox.is_started is True
|
|
214
|
+
|
|
215
|
+
|
|
160
216
|
def test_a_sandbox_that_knows_it_is_gone_is_believed():
|
|
161
217
|
"""`is_alive` reports what the sandbox can find out, not the local start
|
|
162
218
|
flag, so a backend that died under us is not reported as ready."""
|
|
@@ -80,7 +80,7 @@ def basic(identifier: str, severity: str, *, package: str = "openssl") -> dict:
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
class FakeEcr:
|
|
83
|
-
"""The
|
|
83
|
+
"""The three ECR calls the attestor makes, and nothing else."""
|
|
84
84
|
|
|
85
85
|
def __init__(
|
|
86
86
|
self,
|
|
@@ -90,6 +90,7 @@ class FakeEcr:
|
|
|
90
90
|
enhanced_findings=True,
|
|
91
91
|
manifest: dict | None = None,
|
|
92
92
|
pages: list[list[dict]] | None = None,
|
|
93
|
+
size_bytes: int | None = 2_147_483_648,
|
|
93
94
|
) -> None:
|
|
94
95
|
self.statuses = list(statuses)
|
|
95
96
|
self.findings = list(findings)
|
|
@@ -99,8 +100,12 @@ class FakeEcr:
|
|
|
99
100
|
self.pages = pages
|
|
100
101
|
self.enhanced_findings = enhanced_findings
|
|
101
102
|
self.manifest = manifest
|
|
103
|
+
#: What `describe_images` answers for the digest; None makes it raise
|
|
104
|
+
#: `ImageNotFoundException`, the way a registry that lost it would.
|
|
105
|
+
self.size_bytes = size_bytes
|
|
102
106
|
self.asked = 0
|
|
103
107
|
self.scanned: list[str] = []
|
|
108
|
+
self.sized: list[str] = []
|
|
104
109
|
|
|
105
110
|
def batch_get_image(self, repositoryName, imageIds, acceptedMediaTypes): # noqa: N803 - boto3's spelling
|
|
106
111
|
manifest = self.manifest or {
|
|
@@ -117,6 +122,14 @@ class FakeEcr:
|
|
|
117
122
|
]
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
def describe_images(self, repositoryName, imageIds): # noqa: N803 - boto3's spelling
|
|
126
|
+
self.sized.append(imageIds[0]["imageDigest"])
|
|
127
|
+
if self.size_bytes is None:
|
|
128
|
+
error = Exception("ImageNotFoundException")
|
|
129
|
+
error.response = {"Error": {"Code": "ImageNotFoundException"}}
|
|
130
|
+
raise error
|
|
131
|
+
return {"imageDetails": [{"imageSizeInBytes": self.size_bytes}]}
|
|
132
|
+
|
|
120
133
|
def describe_image_scan_findings(self, repositoryName, imageId, nextToken=None): # noqa: N803 - boto3's spelling
|
|
121
134
|
self.asked += 1
|
|
122
135
|
self.scanned.append(imageId["imageDigest"])
|
|
@@ -630,6 +643,33 @@ class TestAttestingAnArtifact:
|
|
|
630
643
|
answer = attest_artifact(artifact=self.an_artifact(), attestor=an_attestor())
|
|
631
644
|
assert answer["scan_summary"]["policy"] == DEFAULT_POLICY.body()
|
|
632
645
|
|
|
646
|
+
def test_the_size_is_read_from_the_registry_when_nobody_hands_one_down(self) -> None:
|
|
647
|
+
"""Which is every real call: the builder answers a reference, not a
|
|
648
|
+
weight, so `environments.artifact.bytes` — section 14's artifact size
|
|
649
|
+
— had no point in it although artifacts had been recorded (E1-25)."""
|
|
650
|
+
ecr = FakeEcr(size_bytes=2_147_483_648)
|
|
651
|
+
answer = attest_artifact(artifact=self.an_artifact(), attestor=an_attestor(ecr=ecr))
|
|
652
|
+
assert answer["size_bytes"] == 2_147_483_648
|
|
653
|
+
assert ecr.sized == [DIGEST]
|
|
654
|
+
|
|
655
|
+
def test_a_size_handed_down_is_kept_and_the_registry_is_not_asked(self) -> None:
|
|
656
|
+
ecr = FakeEcr()
|
|
657
|
+
answer = attest_artifact(
|
|
658
|
+
artifact=self.an_artifact(), size_bytes=116_183_040, attestor=an_attestor(ecr=ecr)
|
|
659
|
+
)
|
|
660
|
+
assert answer["size_bytes"] == 116_183_040
|
|
661
|
+
assert ecr.sized == []
|
|
662
|
+
|
|
663
|
+
def test_a_size_that_cannot_be_read_is_not_a_reason_to_refuse(self) -> None:
|
|
664
|
+
"""A missing number on a dashboard, against an artifact nothing can
|
|
665
|
+
launch: the artifact is signed and the attestation stands."""
|
|
666
|
+
said: list[str] = []
|
|
667
|
+
attestor = an_attestor(ecr=FakeEcr(size_bytes=None), log=said.append)
|
|
668
|
+
answer = attest_artifact(artifact=self.an_artifact(), attestor=attestor)
|
|
669
|
+
assert answer["size_bytes"] is None
|
|
670
|
+
assert answer["signature_ref"] == f"{REGISTRY}/{REPOSITORY}@{DIGEST}"
|
|
671
|
+
assert any("size could not be read" in line for line in said)
|
|
672
|
+
|
|
633
673
|
|
|
634
674
|
def test_nothing_reaches_a_registry_when_nothing_could_sign() -> None:
|
|
635
675
|
"""The order that keeps a refusal cheap and honest (E1-08, E1-09).
|