code-sandboxes 1.4.5__tar.gz → 1.4.6__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.4.5 → code_sandboxes-1.4.6}/CHANGELOG.md +29 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/PKG-INFO +1 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/base.py +44 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/cloudflare_sandbox.py +2 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/coreweave_sandbox.py +2 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/datalayer_sandbox.py +4 -13
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/daytona_sandbox.py +2 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/docker_sandbox.py +19 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/e2b_sandbox.py +2 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/eval_sandbox.py +2 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/google_colab_sandbox.py +14 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/jupyter_ingress.py +30 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/jupyter_server_sandbox.py +2 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/kaggle_sandbox.py +14 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/modal_sandbox.py +2 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/monty_sandbox.py +21 -1
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +11 -93
- code_sandboxes-1.4.6/tests/test_every_variant_has_a_real_interrupt.py +262 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.gitignore +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.licenserc.yaml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/.vscode/settings.json +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/Dockerfile +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/LICENSE +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/Makefile +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/README.md +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docker/Dockerfile +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/.gitignore +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/LICENSE +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/Makefile +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/README.md +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/babel.config.js +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/modal.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/package.json +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/sidebars.js +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/README.md +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/pyproject.toml +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/__init__.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/conftest.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_builds.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_client.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_console.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_eval.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_factory.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_integration.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_manage.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_models.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_providers.py +0 -0
- {code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_sandboxes.py +0 -0
|
@@ -8,6 +8,35 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
- **Every variant has a real interrupt now, and can be reached to give it.**
|
|
12
|
+
`Sandbox.interrupt` is two gates — `_executing_event` must be set, then
|
|
13
|
+
`_do_interrupt` must answer — and seven variants failed one of them, each in
|
|
14
|
+
a way that looked like success:
|
|
15
|
+
|
|
16
|
+
- `docker`, `google_colab`, `kaggle` and `monty` had no `_do_interrupt`, so
|
|
17
|
+
the base default ran: set a flag, return `True`, stop nothing. `True` means
|
|
18
|
+
"the interrupt was delivered", and nothing had been. `google_colab` and
|
|
19
|
+
`kaggle` did read the flag, but only *after* the run, to label a finished
|
|
20
|
+
result as `interrupted` — labelling a run is not stopping one.
|
|
21
|
+
- `cloudflare`, `coreweave`, `daytona`, `e2b` and `modal` answered honestly
|
|
22
|
+
(`False`: this provider takes no interrupt) but never marked their execution
|
|
23
|
+
window, so `interrupt()` returned at the first gate and their answer was
|
|
24
|
+
never reached — and `is_executing` was always False, which every status
|
|
25
|
+
above them believed.
|
|
26
|
+
|
|
27
|
+
Docker, Colab and Kaggle now interrupt for real, delegating to the
|
|
28
|
+
`JupyterKernelClient` that already knows how to authenticate to their server
|
|
29
|
+
rather than rebuilding the request. Monty answers `False` with its reason:
|
|
30
|
+
`feed_run` executes a snippet in one blocking call with no limits and no
|
|
31
|
+
callable hook, so there is no point at which a flag could be read.
|
|
32
|
+
|
|
33
|
+
The execution window is `@marks_execution` on `run_code` rather than a line
|
|
34
|
+
in each body, because it has to close on *every* path out — including the
|
|
35
|
+
early `return ExecutionResult(...)` each adapter uses for an infrastructure
|
|
36
|
+
failure — and a `finally` in the decorator cannot be forgotten in one branch
|
|
37
|
+
of one variant. Both invariants are asserted across the package with no
|
|
38
|
+
pinned exceptions left.
|
|
39
|
+
|
|
11
40
|
- A Datalayer sandbox says when it is running code, so an interrupt can reach
|
|
12
41
|
it. `Sandbox.interrupt` refuses before it delegates — `if not
|
|
13
42
|
self._executing_event.is_set(): return False` — and `run_code` never set that
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import functools
|
|
9
10
|
import threading
|
|
10
11
|
import uuid
|
|
11
12
|
from abc import ABC, abstractmethod
|
|
@@ -82,6 +83,49 @@ def generate_sandbox_name() -> str:
|
|
|
82
83
|
DEFAULT_ENVIRONMENT = "ai-agents-env"
|
|
83
84
|
|
|
84
85
|
|
|
86
|
+
def marks_execution(run):
|
|
87
|
+
"""Say, for the duration of the call, that this sandbox is running code.
|
|
88
|
+
|
|
89
|
+
`Sandbox.interrupt` refuses before it delegates::
|
|
90
|
+
|
|
91
|
+
if not self._executing_event.is_set():
|
|
92
|
+
return False
|
|
93
|
+
self._interrupt_requested.set()
|
|
94
|
+
return self._do_interrupt()
|
|
95
|
+
|
|
96
|
+
So a variant that never sets the event has an interrupt nothing can
|
|
97
|
+
reach, and an `is_executing` that is always False — every status and
|
|
98
|
+
every cancel decision above it silently wrong. `DatalayerSandbox` was in
|
|
99
|
+
exactly that state, and implementing `_do_interrupt` for it changed the
|
|
100
|
+
measurement on prod1 from 60.8 seconds to 60.5: a fix that reads as
|
|
101
|
+
complete and measures as nothing.
|
|
102
|
+
|
|
103
|
+
A decorator rather than a line in each body, because the window has to
|
|
104
|
+
close on *every* path out — including the several early `return
|
|
105
|
+
ExecutionResult(...)`s that each adapter uses for an infrastructure
|
|
106
|
+
failure — and a `finally` here cannot be forgotten in one branch of one
|
|
107
|
+
variant. It also gives the invariant something to test that is not a
|
|
108
|
+
grep for a line of source.
|
|
109
|
+
|
|
110
|
+
`_interrupt_requested` is cleared on the way in, so a stale request from
|
|
111
|
+
a previous call cannot stop this one, and deliberately *not* on the way
|
|
112
|
+
out: the cooperative variants read it after the run to report that the
|
|
113
|
+
execution was interrupted.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
@functools.wraps(run)
|
|
117
|
+
def marked(self, *args, **kwargs):
|
|
118
|
+
self._interrupt_requested.clear()
|
|
119
|
+
self._executing_event.set()
|
|
120
|
+
try:
|
|
121
|
+
return run(self, *args, **kwargs)
|
|
122
|
+
finally:
|
|
123
|
+
self._executing_event.clear()
|
|
124
|
+
|
|
125
|
+
marked._marks_execution = True
|
|
126
|
+
return marked
|
|
127
|
+
|
|
128
|
+
|
|
85
129
|
class Sandbox(ABC):
|
|
86
130
|
"""Abstract base class for code execution sandboxes.
|
|
87
131
|
|
|
@@ -43,7 +43,7 @@ from collections.abc import Iterator
|
|
|
43
43
|
from typing import Any
|
|
44
44
|
from urllib.parse import quote
|
|
45
45
|
|
|
46
|
-
from .base import Sandbox
|
|
46
|
+
from .base import Sandbox, marks_execution
|
|
47
47
|
from .exceptions import (
|
|
48
48
|
SandboxConfigurationError,
|
|
49
49
|
SandboxConnectionError,
|
|
@@ -370,6 +370,7 @@ class CloudflareSandbox(Sandbox):
|
|
|
370
370
|
except Exception:
|
|
371
371
|
return False
|
|
372
372
|
|
|
373
|
+
@marks_execution
|
|
373
374
|
def run_code(
|
|
374
375
|
self,
|
|
375
376
|
code: str,
|
|
@@ -31,7 +31,7 @@ import math
|
|
|
31
31
|
import time
|
|
32
32
|
from typing import Any
|
|
33
33
|
|
|
34
|
-
from .base import Sandbox
|
|
34
|
+
from .base import Sandbox, marks_execution
|
|
35
35
|
from .exceptions import (
|
|
36
36
|
SandboxConfigurationError,
|
|
37
37
|
SandboxExecutionError,
|
|
@@ -521,6 +521,7 @@ class CoreWeaveSandbox(Sandbox):
|
|
|
521
521
|
if self._info:
|
|
522
522
|
self._info.status = SandboxStatus.STOPPED
|
|
523
523
|
|
|
524
|
+
@marks_execution
|
|
524
525
|
def run_code(
|
|
525
526
|
self,
|
|
526
527
|
code: str,
|
|
@@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, Any, Optional
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
18
|
from .filesystem import SandboxFileHandle
|
|
19
19
|
|
|
20
|
-
from .base import Sandbox
|
|
20
|
+
from .base import Sandbox, marks_execution
|
|
21
21
|
from .contents import (
|
|
22
22
|
FILESYSTEM_PRIMITIVES,
|
|
23
23
|
LOCAL_BRIDGE_MOUNT,
|
|
@@ -761,6 +761,7 @@ class DatalayerSandbox(Sandbox):
|
|
|
761
761
|
return None
|
|
762
762
|
return 0
|
|
763
763
|
|
|
764
|
+
@marks_execution
|
|
764
765
|
def run_code(
|
|
765
766
|
self,
|
|
766
767
|
code: str,
|
|
@@ -801,17 +802,9 @@ class DatalayerSandbox(Sandbox):
|
|
|
801
802
|
|
|
802
803
|
started_at = time.time()
|
|
803
804
|
|
|
804
|
-
#
|
|
805
|
-
#
|
|
806
|
-
# before reaching `_do_interrupt` when `_executing_event` is clear,
|
|
807
|
-
# and this variant never set it — so `is_executing` was always False,
|
|
808
|
-
# every interrupt was refused at the door, and the refusal was
|
|
809
|
-
# reported as "no code was running" to a caller watching a cell run.
|
|
810
|
-
# Implementing `_do_interrupt` alone did not fix cancellation for
|
|
811
|
-
# exactly this reason: nothing ever called it.
|
|
805
|
+
# The execution window is `@marks_execution`'s, not this body's — see
|
|
806
|
+
# its docstring for why the interrupt is refused without one.
|
|
812
807
|
execution_timeout = timeout or self.config.timeout
|
|
813
|
-
self._interrupt_requested.clear()
|
|
814
|
-
self._executing_event.set()
|
|
815
808
|
try:
|
|
816
809
|
# Set environment variables if provided
|
|
817
810
|
if envs:
|
|
@@ -831,8 +824,6 @@ class DatalayerSandbox(Sandbox):
|
|
|
831
824
|
started_at=started_at,
|
|
832
825
|
completed_at=time.time(),
|
|
833
826
|
)
|
|
834
|
-
finally:
|
|
835
|
-
self._executing_event.clear()
|
|
836
827
|
|
|
837
828
|
# Parse the response
|
|
838
829
|
stdout_messages: list[OutputMessage] = []
|
|
@@ -30,7 +30,7 @@ import textwrap
|
|
|
30
30
|
import time
|
|
31
31
|
from typing import Any
|
|
32
32
|
|
|
33
|
-
from .base import Sandbox
|
|
33
|
+
from .base import Sandbox, marks_execution
|
|
34
34
|
from .contents import (
|
|
35
35
|
FILESYSTEM_PRIMITIVES,
|
|
36
36
|
ContentAttachmentSpec,
|
|
@@ -701,6 +701,7 @@ class DaytonaSandbox(Sandbox):
|
|
|
701
701
|
self._contexts[context.id] = existing
|
|
702
702
|
return existing
|
|
703
703
|
|
|
704
|
+
@marks_execution
|
|
704
705
|
def run_code( # noqa: C901
|
|
705
706
|
self,
|
|
706
707
|
code: str,
|
|
@@ -15,9 +15,12 @@ import tempfile
|
|
|
15
15
|
import time
|
|
16
16
|
import uuid
|
|
17
17
|
|
|
18
|
+
import logging
|
|
19
|
+
|
|
18
20
|
import requests
|
|
19
21
|
|
|
20
|
-
from .base import Sandbox
|
|
22
|
+
from .base import Sandbox, marks_execution
|
|
23
|
+
from .jupyter_ingress import interrupt_kernel_client
|
|
21
24
|
from .exceptions import SandboxConfigurationError, SandboxNotStartedError
|
|
22
25
|
from .interfaces import ISandboxClient
|
|
23
26
|
from .models import (
|
|
@@ -34,6 +37,8 @@ from .models import (
|
|
|
34
37
|
SandboxStatus,
|
|
35
38
|
)
|
|
36
39
|
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
37
42
|
DEFAULT_IMAGE = "code-sandboxes-jupyter:latest"
|
|
38
43
|
DEFAULT_PORT = 8888
|
|
39
44
|
|
|
@@ -194,6 +199,18 @@ class DockerSandbox(Sandbox):
|
|
|
194
199
|
"""The underlying kernel client for this sandbox, if started."""
|
|
195
200
|
return self._client
|
|
196
201
|
|
|
202
|
+
def _do_interrupt(self) -> bool:
|
|
203
|
+
"""Interrupt the container's kernel, through the client that runs it.
|
|
204
|
+
|
|
205
|
+
This variant runs a Jupyter Server in a container and executes
|
|
206
|
+
through `jupyter-kernel-client`, so it can be interrupted — and it
|
|
207
|
+
had no `_do_interrupt` at all, so the base class's default ran: sets
|
|
208
|
+
a flag, returns `True`, stops nothing. A caller that cancelled a long
|
|
209
|
+
cell here was told the interrupt had been delivered while the
|
|
210
|
+
container went on computing.
|
|
211
|
+
"""
|
|
212
|
+
return interrupt_kernel_client(self._client, variant="container's")
|
|
213
|
+
|
|
197
214
|
def stop(self) -> None:
|
|
198
215
|
if not self._started:
|
|
199
216
|
return
|
|
@@ -225,6 +242,7 @@ class DockerSandbox(Sandbox):
|
|
|
225
242
|
if self._info:
|
|
226
243
|
self._info.status = SandboxStatus.STOPPED
|
|
227
244
|
|
|
245
|
+
@marks_execution
|
|
228
246
|
def run_code(
|
|
229
247
|
self,
|
|
230
248
|
code: str,
|
|
@@ -28,7 +28,7 @@ import logging
|
|
|
28
28
|
import time
|
|
29
29
|
from typing import Any
|
|
30
30
|
|
|
31
|
-
from .base import Sandbox
|
|
31
|
+
from .base import Sandbox, marks_execution
|
|
32
32
|
from .contents import (
|
|
33
33
|
FILESYSTEM_PRIMITIVES,
|
|
34
34
|
ContentAttachmentSpec,
|
|
@@ -471,6 +471,7 @@ class E2BSandbox(Sandbox):
|
|
|
471
471
|
self._contexts[context.id] = existing
|
|
472
472
|
return existing
|
|
473
473
|
|
|
474
|
+
@marks_execution
|
|
474
475
|
def run_code(
|
|
475
476
|
self,
|
|
476
477
|
code: str,
|
|
@@ -24,7 +24,7 @@ import uuid
|
|
|
24
24
|
from contextlib import contextmanager, redirect_stderr, redirect_stdout
|
|
25
25
|
from typing import Any, Optional
|
|
26
26
|
|
|
27
|
-
from .base import Sandbox
|
|
27
|
+
from .base import Sandbox, marks_execution
|
|
28
28
|
from .exceptions import SandboxNotStartedError
|
|
29
29
|
from .models import (
|
|
30
30
|
CodeError,
|
|
@@ -139,6 +139,7 @@ class EvalSandbox(Sandbox):
|
|
|
139
139
|
self._execution_count[context.id] = 0
|
|
140
140
|
return context
|
|
141
141
|
|
|
142
|
+
@marks_execution
|
|
142
143
|
def run_code(
|
|
143
144
|
self,
|
|
144
145
|
code: str,
|
|
@@ -19,7 +19,8 @@ import logging
|
|
|
19
19
|
import time
|
|
20
20
|
import uuid
|
|
21
21
|
|
|
22
|
-
from .base import Sandbox
|
|
22
|
+
from .base import Sandbox, marks_execution
|
|
23
|
+
from .jupyter_ingress import interrupt_kernel_client
|
|
23
24
|
from .exceptions import SandboxConfigurationError, SandboxNotStartedError
|
|
24
25
|
from .google_colab import GoogleColabKernelClient, parse_google_colab_channels_url
|
|
25
26
|
from .interfaces import ISandboxClient
|
|
@@ -140,6 +141,17 @@ class GoogleColabSandbox(Sandbox):
|
|
|
140
141
|
"""Keep tool calling on the client side for Colab sandboxes."""
|
|
141
142
|
return
|
|
142
143
|
|
|
144
|
+
def _do_interrupt(self) -> bool:
|
|
145
|
+
"""Interrupt the Colab kernel, through the client that runs it.
|
|
146
|
+
|
|
147
|
+
There was no override here, so the base class's default ran: it sets
|
|
148
|
+
a flag, returns `True`, and stops nothing. This variant *does* read
|
|
149
|
+
that flag — but only after the fact, to label a result as
|
|
150
|
+
`interrupted`. Labelling a run is not stopping one: the notebook kept
|
|
151
|
+
executing, and the caller had been told the interrupt was delivered.
|
|
152
|
+
"""
|
|
153
|
+
return interrupt_kernel_client(self._client, variant="Colab")
|
|
154
|
+
|
|
143
155
|
def stop(self) -> None:
|
|
144
156
|
if not self._started:
|
|
145
157
|
return
|
|
@@ -154,6 +166,7 @@ class GoogleColabSandbox(Sandbox):
|
|
|
154
166
|
if self._info:
|
|
155
167
|
self._info.status = SandboxStatus.STOPPED
|
|
156
168
|
|
|
169
|
+
@marks_execution
|
|
157
170
|
def run_code( # noqa: C901
|
|
158
171
|
self,
|
|
159
172
|
code: str,
|
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import logging
|
|
9
10
|
import secrets
|
|
10
11
|
import shlex
|
|
12
|
+
from typing import Any
|
|
11
13
|
|
|
12
14
|
from .models import JupyterServerOptions
|
|
13
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
def resolved_options(options: JupyterServerOptions | None) -> JupyterServerOptions:
|
|
16
20
|
"""Return options carrying a fresh Jupyter token when none was supplied."""
|
|
@@ -72,3 +76,29 @@ def websocket_url(http_url: str) -> str:
|
|
|
72
76
|
if http_url.startswith("http://"):
|
|
73
77
|
return "ws://" + http_url.removeprefix("http://")
|
|
74
78
|
raise ValueError(f"Provider returned a non-HTTP ingress URL: {http_url!r}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def interrupt_kernel_client(client: Any, *, variant: str) -> bool:
|
|
82
|
+
"""Interrupt the kernel a `JupyterKernelClient` is connected to.
|
|
83
|
+
|
|
84
|
+
Every Jupyter-backed variant reaches its kernel through a client that
|
|
85
|
+
already knows how to authenticate to that server — Colab's proxy token
|
|
86
|
+
header and query parameter, Kaggle's signed proxy URL, Docker's Jupyter
|
|
87
|
+
token. `JupyterKernelClient.interrupt` uses exactly that, so this
|
|
88
|
+
delegates rather than rebuilding the request: a second copy of the auth
|
|
89
|
+
is a second thing to keep in step with the first.
|
|
90
|
+
|
|
91
|
+
The client's `interrupt` answers `None` and raises when it fails, so this
|
|
92
|
+
turns it into the boolean `_do_interrupt` owes its caller. It never
|
|
93
|
+
raises: a cancel that could not reach the kernel is a cancel that failed,
|
|
94
|
+
and the caller decides what that means.
|
|
95
|
+
"""
|
|
96
|
+
if client is None:
|
|
97
|
+
return False
|
|
98
|
+
try:
|
|
99
|
+
client.interrupt()
|
|
100
|
+
except Exception as error: # noqa: BLE001 - a failed interrupt is an answer
|
|
101
|
+
logger.warning("The %s kernel could not be interrupted: %s", variant, error)
|
|
102
|
+
return False
|
|
103
|
+
return True
|
|
104
|
+
|
|
@@ -29,7 +29,7 @@ from urllib.parse import parse_qs, urlparse, urlunparse
|
|
|
29
29
|
|
|
30
30
|
import requests
|
|
31
31
|
|
|
32
|
-
from .base import Sandbox
|
|
32
|
+
from .base import Sandbox, marks_execution
|
|
33
33
|
from .exceptions import SandboxConfigurationError, SandboxNotStartedError
|
|
34
34
|
from .interfaces import ISandboxClient
|
|
35
35
|
from .models import (
|
|
@@ -572,6 +572,7 @@ class JupyterServerSandbox(Sandbox):
|
|
|
572
572
|
logger.warning(f"Failed to interrupt Jupyter kernel: {e}")
|
|
573
573
|
return False
|
|
574
574
|
|
|
575
|
+
@marks_execution
|
|
575
576
|
def run_code( # noqa: C901
|
|
576
577
|
self,
|
|
577
578
|
code: str,
|
|
@@ -33,7 +33,8 @@ from collections.abc import AsyncIterator, Iterator
|
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from typing import Any
|
|
35
35
|
|
|
36
|
-
from .base import Sandbox
|
|
36
|
+
from .base import Sandbox, marks_execution
|
|
37
|
+
from .jupyter_ingress import interrupt_kernel_client
|
|
37
38
|
from .exceptions import SandboxConfigurationError, SandboxNotStartedError
|
|
38
39
|
from .interfaces import ISandboxClient
|
|
39
40
|
from .kaggle import KaggleKernelClient, parse_kaggle_channels_url
|
|
@@ -261,6 +262,17 @@ class KaggleSandbox(Sandbox):
|
|
|
261
262
|
except (OSError, ValueError):
|
|
262
263
|
pass
|
|
263
264
|
|
|
265
|
+
def _do_interrupt(self) -> bool:
|
|
266
|
+
"""Interrupt the Kaggle kernel, through the client that runs it.
|
|
267
|
+
|
|
268
|
+
There was no override here, so the base class's default ran: it sets
|
|
269
|
+
a flag, returns `True`, and stops nothing. This variant *does* read
|
|
270
|
+
that flag — but only after the fact, to label a result as
|
|
271
|
+
`interrupted`. Labelling a run is not stopping one: the notebook kept
|
|
272
|
+
executing, and the caller had been told the interrupt was delivered.
|
|
273
|
+
"""
|
|
274
|
+
return interrupt_kernel_client(self._client, variant="Kaggle")
|
|
275
|
+
|
|
264
276
|
def stop(self) -> None:
|
|
265
277
|
if not self._started:
|
|
266
278
|
return
|
|
@@ -277,6 +289,7 @@ class KaggleSandbox(Sandbox):
|
|
|
277
289
|
if self._info:
|
|
278
290
|
self._info.status = SandboxStatus.STOPPED
|
|
279
291
|
|
|
292
|
+
@marks_execution
|
|
280
293
|
def run_code( # noqa: C901
|
|
281
294
|
self,
|
|
282
295
|
code: str,
|
|
@@ -25,7 +25,7 @@ import time
|
|
|
25
25
|
import uuid
|
|
26
26
|
from typing import Any
|
|
27
27
|
|
|
28
|
-
from .base import Sandbox
|
|
28
|
+
from .base import Sandbox, marks_execution
|
|
29
29
|
from .contents import (
|
|
30
30
|
CREDENTIAL_DELIVERY_UNSUPPORTED,
|
|
31
31
|
FILESYSTEM_PRIMITIVES,
|
|
@@ -472,6 +472,7 @@ class ModalSandbox(Sandbox):
|
|
|
472
472
|
if self._info:
|
|
473
473
|
self._info.status = SandboxStatus.STOPPED
|
|
474
474
|
|
|
475
|
+
@marks_execution
|
|
475
476
|
def run_code( # noqa: C901
|
|
476
477
|
self,
|
|
477
478
|
code: str,
|
|
@@ -25,7 +25,7 @@ import time
|
|
|
25
25
|
import uuid
|
|
26
26
|
from typing import Any
|
|
27
27
|
|
|
28
|
-
from .base import Sandbox
|
|
28
|
+
from .base import Sandbox, marks_execution
|
|
29
29
|
from .exceptions import SandboxConfigurationError, SandboxNotStartedError
|
|
30
30
|
from .models import (
|
|
31
31
|
CodeError,
|
|
@@ -126,6 +126,25 @@ class MontySandbox(Sandbox):
|
|
|
126
126
|
)
|
|
127
127
|
self._started = True
|
|
128
128
|
|
|
129
|
+
def _do_interrupt(self) -> bool:
|
|
130
|
+
"""Monty's REPL takes no interrupt; a timeout is the only stop.
|
|
131
|
+
|
|
132
|
+
`False` is the honest answer, and it is the point of writing this at
|
|
133
|
+
all. Without an override the base class's default ran — set a flag,
|
|
134
|
+
return `True` — so a caller that cancelled a long run here was told
|
|
135
|
+
the interrupt had been delivered while the interpreter went on.
|
|
136
|
+
Answering `False` lets the caller say "this could not be stopped"
|
|
137
|
+
instead of "this was stopped".
|
|
138
|
+
|
|
139
|
+
`MontyRepl.feed_run` executes the whole snippet in one blocking call.
|
|
140
|
+
It takes no limits (only `Monty.run` does) and its `print_callback`
|
|
141
|
+
is a `CollectStreams` object the interpreter writes into rather than
|
|
142
|
+
a callable it invokes, so there is no point at which a flag could be
|
|
143
|
+
read. If a later Monty exposes a step or callback hook, this is where
|
|
144
|
+
a cooperative interrupt goes.
|
|
145
|
+
"""
|
|
146
|
+
return False
|
|
147
|
+
|
|
129
148
|
def stop(self) -> None:
|
|
130
149
|
if not self._started:
|
|
131
150
|
return
|
|
@@ -142,6 +161,7 @@ class MontySandbox(Sandbox):
|
|
|
142
161
|
return value.decode("utf-8", errors="replace")
|
|
143
162
|
return str(value)
|
|
144
163
|
|
|
164
|
+
@marks_execution
|
|
145
165
|
def run_code( # noqa: C901
|
|
146
166
|
self,
|
|
147
167
|
code: str,
|
{code_sandboxes-1.4.5 → code_sandboxes-1.4.6}/tests/test_a_datalayer_sandbox_can_be_interrupted.py
RENAMED
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
"""A sandbox that cannot be interrupted, and said it had been.
|
|
5
5
|
|
|
6
6
|
`Sandbox.interrupt` sets a flag and calls `_do_interrupt`, whose default
|
|
7
|
-
"simply sets the interrupt flag" and returns `True`.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
"simply sets the interrupt flag" and returns `True`. `datalayer_sandbox` had
|
|
8
|
+
no override, so on a Datalayer runtime the interrupt was delivered nowhere
|
|
9
|
+
and reported success, and everything above believed it: `agent_runtimes`'
|
|
10
|
+
`interrupt_kernel` delegates straight to it, and so does the interrupt
|
|
11
|
+
`execute_cell` registers for `tasks/cancel`.
|
|
12
|
+
|
|
13
|
+
This variant, in detail. The invariants that hold for the whole package —
|
|
14
|
+
that no variant inherits the lying default, and that each marks the window
|
|
15
|
+
its interrupt is reachable through — are in
|
|
16
|
+
`test_every_variant_has_a_real_interrupt.py`.
|
|
14
17
|
|
|
15
18
|
Measured on prod1 on 2026-09-07: a cell looping for two minutes, cancelled
|
|
16
19
|
ten seconds in. `tasks/cancel` and `tasks/get` both answered `cancelled`,
|
|
@@ -51,51 +54,12 @@ def _sandbox(runtime):
|
|
|
51
54
|
return made
|
|
52
55
|
|
|
53
56
|
|
|
54
|
-
class
|
|
57
|
+
class TestItHasOneAtAll:
|
|
55
58
|
def test_datalayer_overrides_it_like_the_others(self):
|
|
56
59
|
"""The assertion that would have caught this: it is about the class,
|
|
57
60
|
not about a call, because the default *succeeds* silently."""
|
|
58
61
|
assert "_do_interrupt" in DatalayerSandbox.__dict__
|
|
59
62
|
|
|
60
|
-
def test_no_variant_quietly_acknowledges_an_interrupt(self):
|
|
61
|
-
"""Held across the package, so the next variant added without one
|
|
62
|
-
fails here rather than on a runtime somebody is paying for.
|
|
63
|
-
|
|
64
|
-
A variant may either override `_do_interrupt` or consult the flag the
|
|
65
|
-
base sets; doing neither means `interrupt()` returns True having done
|
|
66
|
-
nothing. `docker` and `monty` do neither today — pinned rather than
|
|
67
|
-
hidden, so the set cannot grow silently and shrinks visibly when one
|
|
68
|
-
is fixed.
|
|
69
|
-
"""
|
|
70
|
-
import importlib
|
|
71
|
-
import inspect
|
|
72
|
-
import pkgutil
|
|
73
|
-
|
|
74
|
-
import code_sandboxes
|
|
75
|
-
from code_sandboxes.base import Sandbox
|
|
76
|
-
|
|
77
|
-
deaf = set()
|
|
78
|
-
for module in pkgutil.iter_modules(code_sandboxes.__path__):
|
|
79
|
-
if not module.name.endswith("_sandbox"):
|
|
80
|
-
continue
|
|
81
|
-
loaded = importlib.import_module(f"code_sandboxes.{module.name}")
|
|
82
|
-
reads_flag = "_interrupt_requested" in inspect.getsource(loaded)
|
|
83
|
-
for name in dir(loaded):
|
|
84
|
-
value = getattr(loaded, name)
|
|
85
|
-
if (
|
|
86
|
-
isinstance(value, type)
|
|
87
|
-
and issubclass(value, Sandbox)
|
|
88
|
-
and value is not Sandbox
|
|
89
|
-
and value.__module__ == loaded.__name__
|
|
90
|
-
and "_do_interrupt" not in value.__dict__
|
|
91
|
-
and not reads_flag
|
|
92
|
-
):
|
|
93
|
-
deaf.add(f"{module.name}.{name}")
|
|
94
|
-
|
|
95
|
-
known = {"docker_sandbox.DockerSandbox", "monty_sandbox.MontySandbox"}
|
|
96
|
-
assert deaf - known == set(), f"new variants cannot be interrupted: {deaf - known}"
|
|
97
|
-
assert known - deaf == set(), f"fixed — drop from the pinned set: {known - deaf}"
|
|
98
|
-
|
|
99
63
|
|
|
100
64
|
class TestWhatItDoes:
|
|
101
65
|
def test_it_asks_the_runtime_s_own_client(self):
|
|
@@ -208,49 +172,3 @@ class TestTheInterruptIsReached:
|
|
|
208
172
|
|
|
209
173
|
assert seen == [True], "the sandbox ran code without saying it was running"
|
|
210
174
|
assert not sandbox._executing_event.is_set(), "the flag outlived the execution"
|
|
211
|
-
|
|
212
|
-
def test_every_variant_that_can_be_interrupted_says_when_it_runs(self):
|
|
213
|
-
"""The pair, held together: implementing `_do_interrupt` while never
|
|
214
|
-
setting the event is a fix that changes nothing, and that is the
|
|
215
|
-
mistake this file exists to stop repeating."""
|
|
216
|
-
import importlib
|
|
217
|
-
import inspect
|
|
218
|
-
import pkgutil
|
|
219
|
-
|
|
220
|
-
import code_sandboxes
|
|
221
|
-
from code_sandboxes.base import Sandbox
|
|
222
|
-
|
|
223
|
-
mute = set()
|
|
224
|
-
for module in pkgutil.iter_modules(code_sandboxes.__path__):
|
|
225
|
-
if not module.name.endswith("_sandbox"):
|
|
226
|
-
continue
|
|
227
|
-
loaded = importlib.import_module(f"code_sandboxes.{module.name}")
|
|
228
|
-
source = inspect.getsource(loaded)
|
|
229
|
-
for name in dir(loaded):
|
|
230
|
-
value = getattr(loaded, name)
|
|
231
|
-
if (
|
|
232
|
-
isinstance(value, type)
|
|
233
|
-
and issubclass(value, Sandbox)
|
|
234
|
-
and value is not Sandbox
|
|
235
|
-
and value.__module__ == loaded.__name__
|
|
236
|
-
and "_do_interrupt" in value.__dict__
|
|
237
|
-
and "_executing_event.set()" not in source
|
|
238
|
-
):
|
|
239
|
-
mute.add(f"{module.name}.{name}")
|
|
240
|
-
|
|
241
|
-
# Not exemptions — the same unreachable interrupt, pinned. The gate in
|
|
242
|
-
# `Sandbox.interrupt` is held for every variant, so these five ask a
|
|
243
|
-
# provider to stop work and are refused before they can: each one has
|
|
244
|
-
# a written interrupt that nothing can call. Left as found rather than
|
|
245
|
-
# fixed blind, because none of them can be measured from here.
|
|
246
|
-
known = {
|
|
247
|
-
"cloudflare_sandbox.CloudflareSandbox",
|
|
248
|
-
"coreweave_sandbox.CoreWeaveSandbox",
|
|
249
|
-
"daytona_sandbox.DaytonaSandbox",
|
|
250
|
-
"e2b_sandbox.E2BSandbox",
|
|
251
|
-
"modal_sandbox.ModalSandbox",
|
|
252
|
-
}
|
|
253
|
-
assert mute - known == set(), (
|
|
254
|
-
f"these implement an interrupt nothing can reach: {mute - known}"
|
|
255
|
-
)
|
|
256
|
-
assert known - mute == set(), f"reachable now — drop from the pinned set: {known - mute}"
|