code-sandboxes 1.9.13__tar.gz → 1.9.17__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.13 → code_sandboxes-1.9.17}/CHANGELOG.md +64 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/PKG-INFO +1 -1
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/client.py +15 -1
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/attest.py +26 -1
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/bases.py +14 -4
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/files.py +18 -6
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/spec.py +16 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/jupyter_server_sandbox.py +52 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/schemas/environment-v1alpha1.json +46 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_client.py +56 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_attest.py +41 -1
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_bases.py +16 -5
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_builders.py +52 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_resolve.py +5 -1
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_jupyter_server.py +74 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.gitignore +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/Dockerfile +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/LICENSE +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/Makefile +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/README.md +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/datalayer.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/daytona.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/adapters/modal.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/resolve.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/resolve_conda.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/Makefile +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/README.md +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/environments/specification.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/package.json +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/README.md +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/pyproject.toml +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_datalayer_builder.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_daytona_builder.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_managed_builders.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_modal_builder.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_resolve_conda.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_environment_spec.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.13 → code_sandboxes-1.9.17}/tests/test_the_kill_relaunch_drill.py +0 -0
|
@@ -8,6 +8,70 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 1.9.17
|
|
12
|
+
|
|
13
|
+
- **An artifact's size is read from the registry** (`environments/attest.py`;
|
|
14
|
+
PLAN_ENVS.md E1-25). `attest_artifact` took `size_bytes` from its caller and
|
|
15
|
+
nobody ever passed one — the builder answers a reference, not a weight — so
|
|
16
|
+
every artefact was recorded with `sizeBytes: null` and
|
|
17
|
+
`environments.artifact.bytes`, the series section 14 tracks the artifact
|
|
18
|
+
size in, had no point in it although artifacts had been recorded (seen on r1,
|
|
19
|
+
2026-09-16, through the OTEL query API). `Attestor.size_of()` asks the
|
|
20
|
+
registry, with the client the scan is already read from, and a size that
|
|
21
|
+
cannot be read is logged rather than raised: a missing number on a dashboard
|
|
22
|
+
is not a reason to refuse an artifact that is otherwise signed. 3 new tests.
|
|
23
|
+
|
|
24
|
+
## 1.9.15
|
|
25
|
+
|
|
26
|
+
- **A restart restarts the kernel, not just this client's socket**
|
|
27
|
+
(`jupyter_server_sandbox`, `client`; PLAN_ENV.md E0-09, Appendix B check
|
|
28
|
+
7). `CodeSandboxClient.restart()` was `stop()` then `start()`, which is
|
|
29
|
+
right for a sandbox this process owns — it is destroyed and recreated, and
|
|
30
|
+
nothing survives — and wrong for one *attached* to a Jupyter server
|
|
31
|
+
somebody else runs, which is every Datalayer runtime pod: stopping drops
|
|
32
|
+
the websocket while the kernel process keeps running, so the reconnect
|
|
33
|
+
lands in the same interpreter with every global still set. Check 7 is
|
|
34
|
+
"nothing is assumed to persist across restarts", and it read `state survived the restart ('True')` for exactly this reason — found live on r1,
|
|
35
|
+
2026-09-16, the first drill whose smoke test reached the check.
|
|
36
|
+
`JupyterServerSandbox.restart_kernel()` now asks the server's own
|
|
37
|
+
`POST /api/kernels/{id}/restart` (the way `_do_interrupt` already uses the
|
|
38
|
+
API rather than the client's lifecycle) and reconnects onto the new
|
|
39
|
+
kernel; `restart()` prefers it and falls back to the lifecycle for every
|
|
40
|
+
variant that draws no such distinction. 7 new tests.
|
|
41
|
+
- **`datalayer/python-cpu:2026.09` repinned** to
|
|
42
|
+
`sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148`:
|
|
43
|
+
the contract layer now sets `MappingKernelManager.root_dir`, so kernels
|
|
44
|
+
start in `/home/datalayer/content`. The image already declared `WORKDIR`
|
|
45
|
+
there and `sandbox-contract/v1`'s User row already required it, but a
|
|
46
|
+
kernel's cwd is the Jupyter server's to choose and jupyter-python's config
|
|
47
|
+
roots it at `$HOME` — so every environment's kernel ran in
|
|
48
|
+
`/home/datalayer` and Appendix B check 2 read `cwd is '/home/datalayer', not '/home/datalayer/content'`. The file browser stays rooted at `$HOME`,
|
|
49
|
+
where a person expects to see everything they have; only the kernel moves.
|
|
50
|
+
- Two assertions that had rotted through three base releases are pinned in
|
|
51
|
+
one place again: the channel's digest and its apt snapshot were duplicated
|
|
52
|
+
across `test_environment_bases.py` and `test_environment_resolve.py`, and
|
|
53
|
+
2026-09-15's and 2026-09-16's releases left both red rather than catching
|
|
54
|
+
anything.
|
|
55
|
+
|
|
56
|
+
## 1.9.14
|
|
57
|
+
|
|
58
|
+
- **`datalayer/python-cpu:2026.09` base channel repinned** to the rebuilt
|
|
59
|
+
`jupyter-python:0.2.2` (now carrying `jupyter-kernels==1.2.23`) plus the
|
|
60
|
+
contract layer, digest
|
|
61
|
+
`sha256:aa5413000bb5b6ecd0a0cf03959b107f0d572f65bf230c08bbdf9a4569775545`,
|
|
62
|
+
released 2026-09-16 to `environments/base/python-cpu`. Every variant pins the
|
|
63
|
+
same digest.
|
|
64
|
+
|
|
65
|
+
## 1.9.13
|
|
66
|
+
|
|
67
|
+
- **`jupyter-kernels==1.2.23` forced into `sandbox-contract/v1`**: it carries
|
|
68
|
+
the pooled kernel manager the runtime's Jupyter config selects
|
|
69
|
+
(`kernel_manager_class = jupyter_kernels.pool.mapping.PooledMappingKernelManager`),
|
|
70
|
+
replacing the deprecated private `datalayer-kernels`. PyPI serves it, so a
|
|
71
|
+
resolve satisfies it from the index and the wheelhouse carries no wheel for
|
|
72
|
+
it; the pin keeps `uv pip sync --require-hashes` from stripping it out of a
|
|
73
|
+
user environment's image.
|
|
74
|
+
|
|
11
75
|
## 1.9.12
|
|
12
76
|
|
|
13
77
|
- **`owner_repository`, `owner_cache_repository` and `ECR_ENVIRONMENT_PREFIX`
|
|
@@ -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:
|
|
@@ -109,8 +109,9 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
|
|
|
109
109
|
base.ref: base
|
|
110
110
|
for base in (
|
|
111
111
|
# E1-05: jupyter-python:0.2.2 (Ubuntu security packages and conda's own
|
|
112
|
-
# OpenSSL upgraded, JupyterLab's staging yarn.lock dropped
|
|
113
|
-
# contract layer, released 2026-09-
|
|
112
|
+
# OpenSSL upgraded, JupyterLab's staging yarn.lock dropped, jupyter-kernels
|
|
113
|
+
# installed) plus the contract layer, released 2026-09-16 to
|
|
114
|
+
# environments/base/python-cpu.
|
|
114
115
|
# One image, so every variant pins the same digest until a variant
|
|
115
116
|
# needs a base of its own. The prior digest, jupyter-python:0.2.1,
|
|
116
117
|
# carried 31 fixable-critical findings the scan (E1-08) blocks every
|
|
@@ -125,10 +126,19 @@ APPROVED_BASES: dict[str, ApprovedBase] = {
|
|
|
125
126
|
# `.spec.VARIANTS`, spelled out: `spec` imports from this
|
|
126
127
|
# module, so importing it back here would be circular.
|
|
127
128
|
("datalayer", "e2b", "daytona", "modal"),
|
|
128
|
-
|
|
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",
|
|
129
139
|
)
|
|
130
140
|
},
|
|
131
|
-
snapshots={"2026.09": "
|
|
141
|
+
snapshots={"2026.09": "20260916T120000Z"},
|
|
132
142
|
),
|
|
133
143
|
# E2-17: jupyter-python-cuda plus the same layer.
|
|
134
144
|
ApprovedBase(
|
|
@@ -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,
|
|
@@ -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).
|
|
@@ -52,10 +52,17 @@ def test_the_2026_09_channel_of_python_cuda_has_no_digest_until_it_is_pushed(
|
|
|
52
52
|
def test_the_2026_09_channel_of_python_cpu_resolves_the_digest_its_release_pushed(
|
|
53
53
|
variant: str,
|
|
54
54
|
) -> None:
|
|
55
|
-
"""PLAN_ENV.md, E1-05:
|
|
56
|
-
|
|
55
|
+
"""PLAN_ENV.md, E1-05: the digest the channel's last release pushed, the
|
|
56
|
+
same one for every variant.
|
|
57
|
+
|
|
58
|
+
**This moves with every base release**, and `bases.py` is where it moves
|
|
59
|
+
first: two releases (2026-09-15's and 2026-09-16's) changed the channel
|
|
60
|
+
and left this assertion on 2026-09-14's digest, so it sat red rather than
|
|
61
|
+
catching anything. Current: released 2026-09-16, the contract layer that
|
|
62
|
+
starts kernels in `/home/datalayer/content` (E1-05, Appendix B check 2).
|
|
63
|
+
"""
|
|
57
64
|
ref = "datalayer/python-cpu"
|
|
58
|
-
digest = "sha256:
|
|
65
|
+
digest = "sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148"
|
|
59
66
|
assert APPROVED_BASES[ref].channels == {"2026.09": dict.fromkeys(VARIANTS, digest)}
|
|
60
67
|
assert resolve_base(ref, "2026.09", variant) == digest
|
|
61
68
|
|
|
@@ -114,8 +121,12 @@ def test_each_base_is_published_under_its_own_repository() -> None:
|
|
|
114
121
|
|
|
115
122
|
|
|
116
123
|
def test_the_2026_09_channel_of_python_cpu_pins_apt_to_its_snapshot() -> None:
|
|
117
|
-
"""D-9: the moment just after the channel's image upgraded its packages.
|
|
118
|
-
|
|
124
|
+
"""D-9: the moment just after the channel's image upgraded its packages.
|
|
125
|
+
|
|
126
|
+
Moves with the channel, like the digest above, and had rotted the same
|
|
127
|
+
way — left on 2026-09-14's id after the channel moved to 2026-09-16's.
|
|
128
|
+
"""
|
|
129
|
+
assert channel_snapshot("datalayer/python-cpu", "2026.09") == "20260916T120000Z"
|
|
119
130
|
assert channel_snapshot("datalayer/python-cuda", "2026.09") == ""
|
|
120
131
|
assert channel_snapshot("datalayer/nothing", "2026.09") == ""
|
|
121
132
|
|
|
@@ -280,6 +280,58 @@ def test_a_file_is_not_baked_without_its_digest() -> None:
|
|
|
280
280
|
files_step(environment(), variant="kaggle")
|
|
281
281
|
|
|
282
282
|
|
|
283
|
+
CONTENTS_SHA = "c" * 64
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_the_files_step_bakes_a_contents_build_manifest_from_an_external_source() -> None:
|
|
287
|
+
env = environment(
|
|
288
|
+
files=[],
|
|
289
|
+
contents_build=[
|
|
290
|
+
{
|
|
291
|
+
"source": "https://data.example/iris.csv",
|
|
292
|
+
"path": "/opt/datalayer/contents/iris.csv",
|
|
293
|
+
"sha256": CONTENTS_SHA,
|
|
294
|
+
}
|
|
295
|
+
],
|
|
296
|
+
)
|
|
297
|
+
commands = files_step(env, variant="modal")
|
|
298
|
+
# The external source is fetched as it is (no contentRef to sign) and verified.
|
|
299
|
+
assert (
|
|
300
|
+
"curl -fsSL https://data.example/iris.csv -o /opt/datalayer/contents/iris.csv"
|
|
301
|
+
in commands[0]
|
|
302
|
+
)
|
|
303
|
+
assert f'echo "{CONTENTS_SHA} /opt/datalayer/contents/iris.csv" | sha256sum -c' in commands[0]
|
|
304
|
+
assert "environment-contents.json" in commands[-1]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_files_and_contents_build_are_baked_together() -> None:
|
|
308
|
+
env = environment(
|
|
309
|
+
contents_build=[
|
|
310
|
+
{
|
|
311
|
+
"source": "https://data.example/iris.csv",
|
|
312
|
+
"path": "/opt/datalayer/contents/iris.csv",
|
|
313
|
+
"sha256": CONTENTS_SHA,
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
)
|
|
317
|
+
entries = build_entries(env, source_of=lambda entry: "https://signed.example/notes.md")
|
|
318
|
+
# The uploaded file first, then the external build entry — both baked, one engine.
|
|
319
|
+
assert [entry.destination_path for entry in entries] == [
|
|
320
|
+
"/home/datalayer/content/notes.md",
|
|
321
|
+
"/opt/datalayer/contents/iris.csv",
|
|
322
|
+
]
|
|
323
|
+
assert entries[0].source_uri == "https://signed.example/notes.md"
|
|
324
|
+
assert entries[1].source_uri == "https://data.example/iris.csv"
|
|
325
|
+
assert entries[1].sha256 == CONTENTS_SHA
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_a_contents_build_entry_without_its_digest_will_not_parse() -> None:
|
|
329
|
+
with pytest.raises(Exception):
|
|
330
|
+
environment(
|
|
331
|
+
contents_build=[{"source": "https://data.example/x", "path": "/opt/x"}]
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
283
335
|
def test_the_neutral_modules_import_no_provider_sdk() -> None:
|
|
284
336
|
"""What a service may import must not drag a provider in."""
|
|
285
337
|
code = (
|
|
@@ -998,8 +998,12 @@ def test_the_solve_is_asked_to_pin_apt_at_the_base_channels_snapshot() -> None:
|
|
|
998
998
|
runner = RecordedRunner(A_LOCK)
|
|
999
999
|
resolve_environment(spec=a_spec(), variants=["datalayer"], runner=runner)
|
|
1000
1000
|
assert runner.request is not None
|
|
1001
|
+
# What this test is about: the solve is pinned at *the channel's* snapshot,
|
|
1002
|
+
# whatever that is. The id itself is pinned once, in
|
|
1003
|
+
# `test_environment_bases.py`, where it moves with the channel — repeating
|
|
1004
|
+
# it here only meant a base release left two tests red instead of one.
|
|
1001
1005
|
assert runner.request.apt_snapshot == channel_snapshot("datalayer/python-cpu", "2026.09")
|
|
1002
|
-
assert runner.request.apt_snapshot
|
|
1006
|
+
assert runner.request.apt_snapshot
|
|
1003
1007
|
|
|
1004
1008
|
|
|
1005
1009
|
class TestTheBuildkitRunner:
|