ipinb 0.2.0__tar.gz → 0.3.0__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.
Files changed (247) hide show
  1. {ipinb-0.2.0 → ipinb-0.3.0}/BREAKING_CHANGES.md +8 -0
  2. {ipinb-0.2.0 → ipinb-0.3.0}/CHANGELOG.md +7 -0
  3. {ipinb-0.2.0 → ipinb-0.3.0}/PKG-INFO +4 -4
  4. {ipinb-0.2.0 → ipinb-0.3.0}/README.md +3 -3
  5. {ipinb-0.2.0 → ipinb-0.3.0}/docs/architecture.md +6 -0
  6. {ipinb-0.2.0/ipi/skills/notebook/references → ipinb-0.3.0/docs}/kernel-environments.md +2 -2
  7. {ipinb-0.2.0/docs → ipinb-0.3.0/ipi/skills/notebook/references}/kernel-environments.md +2 -2
  8. {ipinb-0.2.0 → ipinb-0.3.0}/pyproject.toml +1 -1
  9. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/contract.py +0 -1
  10. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/ssh.py +7 -8
  11. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/supervisor.py +13 -20
  12. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_uv_environment.py +34 -68
  13. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/execution_models.py +0 -1
  14. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/kernel.py +7 -47
  15. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resource_registry.py +5 -2
  16. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resources/references/kernel-environments.md +2 -2
  17. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/environment.py +1 -11
  18. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/execution_server.py +1 -10
  19. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipinb.egg-info/PKG-INFO +4 -4
  20. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_kernel.py +29 -96
  21. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_ssh_transport.py +54 -52
  22. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_transport_contract.py +0 -4
  23. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_uv_environment.py +146 -15
  24. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_environment.py +1 -1
  25. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_plugin_authoring_skill.py +1 -2
  26. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/test_multi_project_plugins.py +4 -5
  27. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/test_plugin_server_integration.py +1 -2
  28. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_execution_registry.py +58 -12
  29. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_execution_transports.py +2 -2
  30. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_resource_registry.py +24 -1
  31. {ipinb-0.2.0 → ipinb-0.3.0}/LICENSE +0 -0
  32. {ipinb-0.2.0 → ipinb-0.3.0}/MANIFEST.in +0 -0
  33. {ipinb-0.2.0 → ipinb-0.3.0}/docs/autopilot/ipi-mcp-redesign.md +0 -0
  34. {ipinb-0.2.0 → ipinb-0.3.0}/docs/autopilot/notebook-service-log.md +0 -0
  35. {ipinb-0.2.0 → ipinb-0.3.0}/docs/autopilot/notebook-service-plan.md +0 -0
  36. {ipinb-0.2.0 → ipinb-0.3.0}/docs/autopilot/performance-log.md +0 -0
  37. {ipinb-0.2.0 → ipinb-0.3.0}/docs/autopilot/performance-plan.md +0 -0
  38. {ipinb-0.2.0 → ipinb-0.3.0}/docs/autopilot/performance-results.md +0 -0
  39. {ipinb-0.2.0 → ipinb-0.3.0}/docs/autopilot/reduce-output-tokens-log.md +0 -0
  40. {ipinb-0.2.0 → ipinb-0.3.0}/docs/autopilot/reduce-output-tokens-plan.md +0 -0
  41. {ipinb-0.2.0 → ipinb-0.3.0}/docs/debugging.md +0 -0
  42. {ipinb-0.2.0 → ipinb-0.3.0}/docs/execution-and-results.md +0 -0
  43. {ipinb-0.2.0 → ipinb-0.3.0}/docs/notebook-publishing.md +0 -0
  44. {ipinb-0.2.0 → ipinb-0.3.0}/docs/parallel-execution.md +0 -0
  45. {ipinb-0.2.0 → ipinb-0.3.0}/docs/releasing.md +0 -0
  46. {ipinb-0.2.0 → ipinb-0.3.0}/docs/rich-output.md +0 -0
  47. {ipinb-0.2.0 → ipinb-0.3.0}/docs/validation/compact-output-blocks-2026-09-13.md +0 -0
  48. {ipinb-0.2.0 → ipinb-0.3.0}/docs/validation/execution-api-2026-09-10.md +0 -0
  49. {ipinb-0.2.0 → ipinb-0.3.0}/docs/validation/fastmcp4-2026-09-13.md +0 -0
  50. {ipinb-0.2.0 → ipinb-0.3.0}/docs/your-first-extension.md +0 -0
  51. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/SKILL.md +0 -0
  52. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/references/authoring-guide.md +0 -0
  53. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/references/evaluations.md +0 -0
  54. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/references/source-map.md +0 -0
  55. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/references/testing-and-troubleshooting.md +0 -0
  56. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/templates/installed-plugin/pyproject.toml +0 -0
  57. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/templates/installed-plugin/src/example_ipi_plugin/__init__.py +0 -0
  58. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/templates/installed-plugin/tests/test_plugin.py +0 -0
  59. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/developing-ipi-plugins/templates/project-plugin.py +0 -0
  60. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/notebook/SKILL.md +0 -0
  61. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/notebook/references/debugging.md +0 -0
  62. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/notebook/references/execution-and-results.md +0 -0
  63. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/notebook/references/notebook-publishing.md +0 -0
  64. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/notebook/references/parallel-execution.md +0 -0
  65. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/notebook/references/rich-output.md +0 -0
  66. {ipinb-0.2.0 → ipinb-0.3.0}/ipi/skills/notebook-http/SKILL.md +0 -0
  67. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/package-lock.json +0 -0
  68. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/package.json +0 -0
  69. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/src/cell_chrome.ts +0 -0
  70. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/src/index.ts +0 -0
  71. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/src/intent.ts +0 -0
  72. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/src/metadata.ts +0 -0
  73. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/src/setup_chrome.ts +0 -0
  74. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/style/index.css +0 -0
  75. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/tests/index.spec.ts +0 -0
  76. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/tests/setup.ts +0 -0
  77. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/tsconfig.json +0 -0
  78. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/tsconfig.test.json +0 -0
  79. {ipinb-0.2.0 → ipinb-0.3.0}/jupyterlab-extension/vitest.config.ts +0 -0
  80. {ipinb-0.2.0 → ipinb-0.3.0}/setup.cfg +0 -0
  81. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/__init__.py +0 -0
  82. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_ansi.py +0 -0
  83. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_dependency_metadata.py +0 -0
  84. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_exception_groups.py +0 -0
  85. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_git_provenance.py +0 -0
  86. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_kernel_argv.py +0 -0
  87. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_kernel_launcher.py +0 -0
  88. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_launch_gate.py +0 -0
  89. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_markdown.py +0 -0
  90. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_model_output.py +0 -0
  91. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_notebook_mirror.py +0 -0
  92. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_owner_watchdog.py +0 -0
  93. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_parent_process.py +0 -0
  94. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_platform_support.py +0 -0
  95. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_self_wheel.py +0 -0
  96. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_timing.py +0 -0
  97. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_toml.py +0 -0
  98. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/__init__.py +0 -0
  99. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/address.py +0 -0
  100. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/artifacts.py +0 -0
  101. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/local.py +0 -0
  102. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/master_owner.py +0 -0
  103. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/owned_directory.py +0 -0
  104. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/portable_uv.py +0 -0
  105. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/staging.py +0 -0
  106. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_transport/supervisor_protocol.py +0 -0
  107. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/_ynotebook_ops.py +0 -0
  108. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/cell_export.py +0 -0
  109. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/config.py +0 -0
  110. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/execution_backend.py +0 -0
  111. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/jupyter_server.py +0 -0
  112. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/labextensions/@ipi/notebook-extension/package.json +0 -0
  113. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/labextensions/@ipi/notebook-extension/static/40.5dddbc48f9255afab0d7.js +0 -0
  114. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/labextensions/@ipi/notebook-extension/static/665.890acd7e2a9ea1a043aa.js +0 -0
  115. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/labextensions/@ipi/notebook-extension/static/remoteEntry.aa9e35c0f1b6fe93a828.js +0 -0
  116. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/labextensions/@ipi/notebook-extension/static/style.js +0 -0
  117. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/labextensions/@ipi/notebook-extension/static/third-party-licenses.json +0 -0
  118. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/notebook_publisher.py +0 -0
  119. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/__init__.py +0 -0
  120. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/_model.py +0 -0
  121. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/api.py +0 -0
  122. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/__init__.py +0 -0
  123. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/_agents_md_impl.py +0 -0
  124. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/_image_outputs_impl.py +0 -0
  125. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/agents_md.py +0 -0
  126. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/autoreload.py +0 -0
  127. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/dataframe_display.py +0 -0
  128. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/http_user_agent.py +0 -0
  129. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/image_outputs.py +0 -0
  130. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/import_guidance.py +0 -0
  131. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/pdb.py +0 -0
  132. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/pip_guidance.py +0 -0
  133. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/builtins/project_import.py +0 -0
  134. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/catalog.py +0 -0
  135. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/discovery.py +0 -0
  136. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/kernel.py +0 -0
  137. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/py.typed +0 -0
  138. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/plugins/server_deps.py +0 -0
  139. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/py.typed +0 -0
  140. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/records/__init__.py +0 -0
  141. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/records/py.typed +0 -0
  142. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/redaction.py +0 -0
  143. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/references.py +0 -0
  144. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resources/notebook_http_skill.md +0 -0
  145. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resources/notebook_skill.md +0 -0
  146. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resources/references/debugging.md +0 -0
  147. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resources/references/execution-and-results.md +0 -0
  148. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resources/references/notebook-publishing.md +0 -0
  149. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resources/references/parallel-execution.md +0 -0
  150. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/resources/references/rich-output.md +0 -0
  151. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/__init__.py +0 -0
  152. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/_subprocess.py +0 -0
  153. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/audit.py +0 -0
  154. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/bootstrap.py +0 -0
  155. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/cell_types.py +0 -0
  156. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/concurrent.py +0 -0
  157. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/context.py +0 -0
  158. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/debugger.py +0 -0
  159. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/formatters.py +0 -0
  160. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/magics.py +0 -0
  161. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/path_context.py +0 -0
  162. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/project.py +0 -0
  163. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/py.typed +0 -0
  164. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/replay.py +0 -0
  165. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/runtime/state_delta.py +0 -0
  166. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/__init__.py +0 -0
  167. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/__main__.py +0 -0
  168. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/builtins/__init__.py +0 -0
  169. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/builtins/view_image.py +0 -0
  170. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/execution_content.py +0 -0
  171. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/plugin_resolver.py +0 -0
  172. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/plugin_tools.py +0 -0
  173. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/py.typed +0 -0
  174. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/server/server.py +0 -0
  175. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/session.py +0 -0
  176. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipi/tunnel.py +0 -0
  177. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipinb.egg-info/SOURCES.txt +0 -0
  178. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipinb.egg-info/dependency_links.txt +0 -0
  179. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipinb.egg-info/entry_points.txt +0 -0
  180. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipinb.egg-info/requires.txt +0 -0
  181. {ipinb-0.2.0 → ipinb-0.3.0}/src/ipinb.egg-info/top_level.txt +0 -0
  182. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_ansi.py +0 -0
  183. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_config.py +0 -0
  184. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_context_runtime.py +0 -0
  185. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_credential_boundaries.py +0 -0
  186. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_credential_redaction.py +0 -0
  187. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_debugger.py +0 -0
  188. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_editable_requirements.py +0 -0
  189. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_iopub_reducer.py +0 -0
  190. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_jupyter_server.py +0 -0
  191. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_kernel_address.py +0 -0
  192. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_kernel_regressions.py +0 -0
  193. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_late_iopub.py +0 -0
  194. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_notebook_mirror.py +0 -0
  195. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_notebook_mirror_writer.py +0 -0
  196. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_notebook_publisher.py +0 -0
  197. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_notebook_publisher_e2e.py +0 -0
  198. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_output_noise.py +0 -0
  199. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_owner_watchdog.py +0 -0
  200. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_packaging_namespace.py +0 -0
  201. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_platform_support.py +0 -0
  202. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_plugin_kernel_integration.py +0 -0
  203. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_portable_uv.py +0 -0
  204. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_references.py +0 -0
  205. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_self_wheel.py +0 -0
  206. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_session.py +0 -0
  207. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_smoke.py +0 -0
  208. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_timing.py +0 -0
  209. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_toml.py +0 -0
  210. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_transport_staging.py +0 -0
  211. {ipinb-0.2.0 → ipinb-0.3.0}/tests/core/test_tunnel.py +0 -0
  212. {ipinb-0.2.0 → ipinb-0.3.0}/tests/release_smoke.py +0 -0
  213. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_bootstrap.py +0 -0
  214. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_agents_md.py +0 -0
  215. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_autoreload.py +0 -0
  216. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_dataframe_display.py +0 -0
  217. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_http_user_agent.py +0 -0
  218. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_image_outputs.py +0 -0
  219. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_import_guidance.py +0 -0
  220. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_pdb.py +0 -0
  221. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_pip_guidance.py +0 -0
  222. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_builtin_project_import.py +0 -0
  223. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_context.py +0 -0
  224. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_formatters.py +0 -0
  225. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_magics.py +0 -0
  226. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_model_output.py +0 -0
  227. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_path_context.py +0 -0
  228. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_plugin_api.py +0 -0
  229. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_plugin_catalog.py +0 -0
  230. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_plugin_discovery.py +0 -0
  231. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_plugin_kernel.py +0 -0
  232. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_plugin_server_deps.py +0 -0
  233. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_project.py +0 -0
  234. {ipinb-0.2.0 → ipinb-0.3.0}/tests/runtime/test_smoke.py +0 -0
  235. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/conftest.py +0 -0
  236. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/test_builtin_view_image.py +0 -0
  237. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/test_main.py +0 -0
  238. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/test_plugin_resolver.py +0 -0
  239. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/test_plugin_tools.py +0 -0
  240. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/test_server.py +0 -0
  241. {ipinb-0.2.0 → ipinb-0.3.0}/tests/server/test_stdio_shutdown.py +0 -0
  242. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_cell_export.py +0 -0
  243. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_consolidated_tools.py +0 -0
  244. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_debugger.py +0 -0
  245. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_execution_content_errors.py +0 -0
  246. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_namespace.py +0 -0
  247. {ipinb-0.2.0 → ipinb-0.3.0}/tests/test_package_boundaries.py +0 -0
@@ -1,5 +1,13 @@
1
1
  # Breaking Changes
2
2
 
3
+ ## 2026-10-27: Private uv kernel environments (0.3.0)
4
+
5
+ Every local and SSH kernel uses an isolated uv environment. Runtime and additional dependencies may override project pins inside that environment. IPi no longer turns every installed project version into a constraint. Project `.venv` and `uv.lock` stay unchanged. Live `%pip` changes remain private to the kernel. Strict `kernel.locked` checks the base project's lock; overlay versions may still differ.
6
+
7
+ The `venv` argument is removed from `create_kernel`, `Kernel.launch`, and transport launch requests, and from returned kernel records. IPi requires uv 0.12.3 or later on the execution host. Existing running servers and kernels retain their old behavior until restarted.
8
+
9
+ **Migration:** Upgrade uv, remove `venv` arguments, and set `cwd` to the intended uv project or scratch directory. Put project requirements in its `pyproject.toml`, and pass kernel-only requirements through `additional_dependencies`. Use project Python selection such as `.python-version`; do not build a separate venv for IPi. Review workflows that assumed kernel imports must use exactly the project's locked versions. New calls to a failed kernel now report `kernel_setup_failed` with the stored cause instead of `kernel_closed`; explicitly closed kernels keep the closed response.
10
+
3
11
  ## 2026-10-24: No host hook support (0.2.0)
4
12
 
5
13
  IPi 0.2.0 is a plain MCP notebook server. It installs no hooks and receives no host events.
@@ -3,6 +3,13 @@
3
3
  All notable user-facing changes are recorded here. Detailed API removals and
4
4
  migration steps are maintained in [BREAKING_CHANGES.md](BREAKING_CHANGES.md).
5
5
 
6
+ ## 0.3.0
7
+
8
+ - Restore native uv dependency overrides in private per-kernel environments. Runtime dependencies can override project pins without changing project environments or lockfiles; live installs stay private to each kernel.
9
+ - Remove explicit `venv` attachment from kernel APIs and records. Select the project through `cwd` and request extra packages through `additional_dependencies`.
10
+ - Require uv 0.12.3 or later for local and SSH kernels. Keep temporary lock resolution by default and optional strict base-lock validation.
11
+ - Return the original redacted kernel setup error when Python is submitted after startup failed, rather than the generic `kernel_closed` error.
12
+
6
13
  ## 0.2.0
7
14
 
8
15
  - Breaking: IPi has no host hook support. Start context, the session trace, harness Bash commands as notebook cells, `bash[...]` shell history, the inline-Python nudge, and Claude session identity in kernels are gone (Codex kernels keep `CODEX_THREAD_ID`); the `install`, `install-hooks` and `doctor-narration` commands, the plugin hook API, `ipi.import_path` (use `toolfuncs.load`), the `ipi` command alias and the empty `mcp` extra are removed; IPi requires `toolfuncs>=3,<4`. See [BREAKING_CHANGES.md](BREAKING_CHANGES.md).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipinb
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Persistent IPython notebooks for MCP agents
5
5
  Author-email: Nima Shoghi <nimashoghi@gmail.com>
6
6
  License-Expression: MIT
@@ -244,7 +244,7 @@ SSH URI:
244
244
  ```python
245
245
  create_kernel(cwd="/Users/me/local-project")
246
246
  create_kernel(cwd="ssh://gpu-box/opt/projects/model")
247
- create_kernel(cwd="ssh://alice@gpu-box:2222/opt/projects/model", venv=".venv")
247
+ create_kernel(cwd="ssh://alice@gpu-box:2222/opt/projects/model")
248
248
  ```
249
249
 
250
250
  The URI path is the absolute working directory on the target. Host aliases,
@@ -333,9 +333,9 @@ Fresh kernels make a small general-purpose module set available: `typing_extensi
333
333
 
334
334
  Use `publication.shared_dependencies` for packages that must be installed in both the kernel and the lazily created Notebook 7 environment, such as packages that ship a JupyterLab frontend extension. Shared entries are automatically included in every kernel, so do not duplicate them under `kernel.additional_dependencies`. The publisher uses the exact package versions and sources selected by its kernel. Ordinary kernel-only packages remain in `kernel.additional_dependencies`; installing one live with `%pip` requires no publication change. Put personal scientific and widget defaults in `kernel.scratch_dependencies` and `publication.scratch_shared_dependencies` to keep them out of project kernels.
335
335
 
336
- Local and SSH kernels use ephemeral `uv run --isolated` environments. uv selects the project, workspace membership, Python interpreter, sources, and normal default groups. IPi does not enable all extras or groups. Missing or stale locks are resolved temporarily without writing `uv.lock`, and IPi does not create or synchronize `.venv`. Set `kernel.locked = true` to require an existing current lock. An explicit `venv` remains supported and is reused without synchronization; it cannot be combined with locked project preparation.
336
+ Local and SSH kernels use ephemeral `uv run --isolated` environments. uv selects the project, workspace membership, Python interpreter, sources, and normal default groups. IPi does not enable all extras or groups. Missing or stale locks are resolved temporarily without writing `uv.lock`, and IPi does not create or synchronize `.venv`. Set `kernel.locked = true` to require an existing current lock. Strict-lock mode checks the base project lock; kernel overrides can still select different package versions.
337
337
 
338
- IPi captures the base environment selected by uv and constrains added kernel packages to that selection. Incompatible additions fail instead of shadowing project packages. Approved direnv settings apply to preparation and launch; IPi does not approve an `.envrc`.
338
+ IPi requires uv 0.12.3 or later. Its runtime and additional dependencies use native uv overlays and may override project package pins. Each kernel has a private writable environment: live `%pip` changes affect only that kernel. Select the project with `cwd` and request extra packages with `additional_dependencies`; explicit venv attachment is not supported. Approved direnv settings apply to preparation and launch; IPi does not approve an `.envrc`.
339
339
 
340
340
  When `tools.view_image` is omitted, the public tool is disabled on stdio and
341
341
  enabled on HTTP, SSE, and streamable HTTP. Set it explicitly to `true` or
@@ -201,7 +201,7 @@ SSH URI:
201
201
  ```python
202
202
  create_kernel(cwd="/Users/me/local-project")
203
203
  create_kernel(cwd="ssh://gpu-box/opt/projects/model")
204
- create_kernel(cwd="ssh://alice@gpu-box:2222/opt/projects/model", venv=".venv")
204
+ create_kernel(cwd="ssh://alice@gpu-box:2222/opt/projects/model")
205
205
  ```
206
206
 
207
207
  The URI path is the absolute working directory on the target. Host aliases,
@@ -290,9 +290,9 @@ Fresh kernels make a small general-purpose module set available: `typing_extensi
290
290
 
291
291
  Use `publication.shared_dependencies` for packages that must be installed in both the kernel and the lazily created Notebook 7 environment, such as packages that ship a JupyterLab frontend extension. Shared entries are automatically included in every kernel, so do not duplicate them under `kernel.additional_dependencies`. The publisher uses the exact package versions and sources selected by its kernel. Ordinary kernel-only packages remain in `kernel.additional_dependencies`; installing one live with `%pip` requires no publication change. Put personal scientific and widget defaults in `kernel.scratch_dependencies` and `publication.scratch_shared_dependencies` to keep them out of project kernels.
292
292
 
293
- Local and SSH kernels use ephemeral `uv run --isolated` environments. uv selects the project, workspace membership, Python interpreter, sources, and normal default groups. IPi does not enable all extras or groups. Missing or stale locks are resolved temporarily without writing `uv.lock`, and IPi does not create or synchronize `.venv`. Set `kernel.locked = true` to require an existing current lock. An explicit `venv` remains supported and is reused without synchronization; it cannot be combined with locked project preparation.
293
+ Local and SSH kernels use ephemeral `uv run --isolated` environments. uv selects the project, workspace membership, Python interpreter, sources, and normal default groups. IPi does not enable all extras or groups. Missing or stale locks are resolved temporarily without writing `uv.lock`, and IPi does not create or synchronize `.venv`. Set `kernel.locked = true` to require an existing current lock. Strict-lock mode checks the base project lock; kernel overrides can still select different package versions.
294
294
 
295
- IPi captures the base environment selected by uv and constrains added kernel packages to that selection. Incompatible additions fail instead of shadowing project packages. Approved direnv settings apply to preparation and launch; IPi does not approve an `.envrc`.
295
+ IPi requires uv 0.12.3 or later. Its runtime and additional dependencies use native uv overlays and may override project package pins. Each kernel has a private writable environment: live `%pip` changes affect only that kernel. Select the project with `cwd` and request extra packages with `additional_dependencies`; explicit venv attachment is not supported. Approved direnv settings apply to preparation and launch; IPi does not approve an `.envrc`.
296
296
 
297
297
  When `tools.view_image` is omitted, the public tool is disabled on stdio and
298
298
  enabled on HTTP, SSE, and streamable HTTP. Set it explicitly to `true` or
@@ -2,6 +2,12 @@
2
2
 
3
3
  IPi is a notebook runtime exposed through MCP. Codex or Claude Code owns the agent loop; IPi owns persistent IPython kernels, execution records, notebooks, and artifacts. IPi installs and receives no host hooks.
4
4
 
5
+ ## Kernel environment ownership
6
+
7
+ Local and SSH kernels require uv 0.12.3 or later and use one `uv run --isolated` path. uv discovers the project/workspace, Python, local sources, and default groups. Runtime and task requirements use native overlays that can override project pins. IPi does not freeze the base package inventory, attach existing venvs, synchronize `.venv`, or write `uv.lock`. Missing or stale locks resolve temporarily unless `kernel.locked` requires a current base lock. Each launch has a private writable environment, including when uv can skip an already-satisfied overlay. Live package changes belong to that kernel.
8
+
9
+ Kernel setup remains asynchronous. Work accepted during setup receives its retained setup failure if provisioning fails. New Python submissions after failure return the same redacted cause, with the kernel ID. Replaying an accepted request ID still recovers the original cell.
10
+
5
11
  ## Distribution Boundary
6
12
 
7
13
  Installed archive plugins accept content hashes from PEP 610 `archive_info` or the URL hash fragment retained by uv for local wheels. Both forms produce the same source identity. Conflicting hashes, malformed fragments, and archives with no content hash remain errors.
@@ -10,11 +10,11 @@ Reuse a suitable kernel ID already provided in context. When creating a kernel,
10
10
  create_kernel(cwd="/absolute/project", additional_dependencies=["numpy", "pandas"], request_id="analysis-kernel")
11
11
  ```
12
12
 
13
- The result may report `starting`. Python submitted with its returned `kernel_id` waits for setup. Inspect failed setup through the kernel record rather than assuming an accepted creation is ready. Configuration and plugins resolve for that kernel's cwd; a kernel created for another project can have a different environment.
13
+ The result may report `starting`. Python submitted with its returned `kernel_id` waits for setup. Submit work with the returned ID and observe its cell; setup failures are returned directly even if setup failed before submission. Configuration and plugins resolve for that kernel's cwd; a kernel created for another project can have a different environment.
14
14
 
15
15
  IPi uses ephemeral `uv run --isolated` environments. uv selects the project or workspace member for `cwd`, its Python version, sources, and normal default dependency groups. IPi does not create or synchronize `.venv`. It resolves missing or stale locks temporarily without writing `uv.lock`; set `kernel.locked = true` in IPi configuration when a current lock is required. Approved direnv settings apply before resolution. IPi does not approve an `.envrc`.
16
16
 
17
- Added kernel dependencies must remain compatible with every package selected by the project. Conflicts fail without replacing those packages. Prefer `additional_dependencies` at creation for task requirements. Personal defaults in `kernel.scratch_dependencies` and `publication.scratch_shared_dependencies` apply only outside a uv project. An explicit `venv` selects an existing base without synchronizing it; it cannot be combined with locked project preparation. Live `%pip` changes are disposable. The environment hint describes available modules and distribution/import aliases. Query the actual interpreter when needed:
17
+ IPi requires uv 0.12.3 or later. Initial startup includes a 180-second dependency preparation allowance in addition to the Jupyter readiness budget; `IPI_UV_BASE_ENVIRONMENT_TIMEOUT_S` changes that allowance for local and SSH launches. Select a project with `cwd` and use `additional_dependencies` for task requirements; explicit venv attachment is not supported. Native uv overlays let runtime and added packages override project pins inside the disposable kernel environment. Strict-lock mode checks the base lock, not the overlay versions. Personal defaults in `kernel.scratch_dependencies` and `publication.scratch_shared_dependencies` apply only outside a uv project. Live `%pip` changes are private to this kernel and do not change other kernels or later launches. The environment hint describes available modules and distribution/import aliases. Query the actual interpreter when needed:
18
18
 
19
19
  ```python
20
20
  import sys
@@ -10,11 +10,11 @@ Reuse a suitable kernel ID already provided in context. When creating a kernel,
10
10
  create_kernel(cwd="/absolute/project", additional_dependencies=["numpy", "pandas"], request_id="analysis-kernel")
11
11
  ```
12
12
 
13
- The result may report `starting`. Python submitted with its returned `kernel_id` waits for setup. Inspect failed setup through the kernel record rather than assuming an accepted creation is ready. Configuration and plugins resolve for that kernel's cwd; a kernel created for another project can have a different environment.
13
+ The result may report `starting`. Python submitted with its returned `kernel_id` waits for setup. Submit work with the returned ID and observe its cell; setup failures are returned directly even if setup failed before submission. Configuration and plugins resolve for that kernel's cwd; a kernel created for another project can have a different environment.
14
14
 
15
15
  IPi uses ephemeral `uv run --isolated` environments. uv selects the project or workspace member for `cwd`, its Python version, sources, and normal default dependency groups. IPi does not create or synchronize `.venv`. It resolves missing or stale locks temporarily without writing `uv.lock`; set `kernel.locked = true` in IPi configuration when a current lock is required. Approved direnv settings apply before resolution. IPi does not approve an `.envrc`.
16
16
 
17
- Added kernel dependencies must remain compatible with every package selected by the project. Conflicts fail without replacing those packages. Prefer `additional_dependencies` at creation for task requirements. Personal defaults in `kernel.scratch_dependencies` and `publication.scratch_shared_dependencies` apply only outside a uv project. An explicit `venv` selects an existing base without synchronizing it; it cannot be combined with locked project preparation. Live `%pip` changes are disposable. The environment hint describes available modules and distribution/import aliases. Query the actual interpreter when needed:
17
+ IPi requires uv 0.12.3 or later. Initial startup includes a 180-second dependency preparation allowance in addition to the Jupyter readiness budget; `IPI_UV_BASE_ENVIRONMENT_TIMEOUT_S` changes that allowance for local and SSH launches. Select a project with `cwd` and use `additional_dependencies` for task requirements; explicit venv attachment is not supported. Native uv overlays let runtime and added packages override project pins inside the disposable kernel environment. Strict-lock mode checks the base lock, not the overlay versions. Personal defaults in `kernel.scratch_dependencies` and `publication.scratch_shared_dependencies` apply only outside a uv project. Live `%pip` changes are private to this kernel and do not change other kernels or later launches. The environment hint describes available modules and distribution/import aliases. Query the actual interpreter when needed:
18
18
 
19
19
  ```python
20
20
  import sys
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ipinb"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Persistent IPython notebooks for MCP agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -23,7 +23,6 @@ class KernelLaunchRequest:
23
23
  """Validated target-side launch data, independent of one transport."""
24
24
 
25
25
  cwd: PurePath
26
- venv: PurePath | None
27
26
  requirements: tuple[str, ...]
28
27
  environment: Mapping[str, str]
29
28
  locked: bool = False
@@ -25,6 +25,7 @@ from typing import Any, cast
25
25
  from jupyter_client import KernelManager
26
26
 
27
27
  from ipi._exception_groups import BaseExceptionGroup
28
+ from ipi._uv_environment import preparation_timeout_s
28
29
  from ipi.redaction import redact_exception_message, redact_url_secrets, redact_url_userinfo
29
30
 
30
31
  from .address import SshKernelLocation
@@ -587,7 +588,6 @@ class SshPreparedTransport:
587
588
  response = supervisor.request(
588
589
  "launch",
589
590
  cwd=request.cwd.as_posix(),
590
- venv=(request.venv.as_posix() if request.venv is not None else None),
591
591
  locked=request.locked,
592
592
  requirements=list(request.requirements),
593
593
  environment=dict(request.environment),
@@ -1487,14 +1487,13 @@ class _SupervisorClient:
1487
1487
  with self._request_lock:
1488
1488
  if self._closed:
1489
1489
  raise SshTransportError("SSH supervisor control channel is closed")
1490
+ timeout = _SUPERVISOR_RESPONSE_SECONDS
1491
+ if operation == "launch":
1492
+ preparation_timeout = preparation_timeout_s()
1493
+ fields["preparation_timeout_s"] = preparation_timeout
1494
+ timeout = _SUPERVISOR_LAUNCH_RESPONSE_SECONDS + preparation_timeout
1490
1495
  request_id = self._write_request(operation, fields=fields, one_way=False)
1491
- response = self._read_message(
1492
- timeout_s=(
1493
- _SUPERVISOR_LAUNCH_RESPONSE_SECONDS
1494
- if operation == "launch"
1495
- else _SUPERVISOR_RESPONSE_SECONDS
1496
- )
1497
- )
1496
+ response = self._read_message(timeout_s=timeout)
1498
1497
  if response.get("id") != request_id or response.get("version") != _PROTOCOL_VERSION:
1499
1498
  raise SshTransportError("SSH supervisor returned a mismatched control response")
1500
1499
  if response.get("ok") is not True:
@@ -12,6 +12,7 @@ import base64
12
12
  import contextlib
13
13
  import ctypes
14
14
  import json
15
+ import math
15
16
  import os
16
17
  import re
17
18
  import secrets
@@ -226,9 +227,14 @@ class Supervisor:
226
227
  cwd = _absolute_path(request.get("cwd"), what="kernel cwd")
227
228
  if not cwd.is_dir():
228
229
  raise SupervisorError(f"kernel cwd does not exist or is not a directory: {cwd}")
229
- venv_value = request.get("venv")
230
- venv = None if venv_value is None else _absolute_path(venv_value, what="kernel venv")
231
- _validate_requested_venv(venv)
230
+ preparation_timeout = request.get("preparation_timeout_s", 180.0)
231
+ if (
232
+ isinstance(preparation_timeout, bool)
233
+ or not isinstance(preparation_timeout, (int, float))
234
+ or not math.isfinite(preparation_timeout)
235
+ or preparation_timeout <= 0
236
+ ):
237
+ raise SupervisorError("preparation_timeout_s must be finite and positive")
232
238
  locked = request.get("locked", False)
233
239
  if not isinstance(locked, bool):
234
240
  raise SupervisorError("locked must be a boolean")
@@ -266,7 +272,6 @@ class Supervisor:
266
272
  session_dir=self._session_dir,
267
273
  connection_file=self._connection_file,
268
274
  cwd=cwd,
269
- venv=venv,
270
275
  locked=locked,
271
276
  requirements=requirements,
272
277
  environment=environment,
@@ -276,7 +281,6 @@ class Supervisor:
276
281
  ownership_manifest.write_text(
277
282
  json.dumps(
278
283
  {
279
- "external_venv": str(venv) if venv is not None else None,
280
284
  "owned_environment": str(owned_environment),
281
285
  },
282
286
  sort_keys=True,
@@ -298,7 +302,7 @@ class Supervisor:
298
302
  start_new_session=True,
299
303
  )
300
304
 
301
- connection = self._wait_for_connection()
305
+ connection = self._wait_for_connection(timeout_s=KERNEL_START_SECONDS + preparation_timeout)
302
306
  return {
303
307
  "ports": {
304
308
  name: connection[name]
@@ -312,8 +316,8 @@ class Supervisor:
312
316
  }
313
317
  }
314
318
 
315
- def _wait_for_connection(self) -> dict[str, object]:
316
- deadline = time.monotonic() + KERNEL_START_SECONDS
319
+ def _wait_for_connection(self, *, timeout_s: float) -> dict[str, object]:
320
+ deadline = time.monotonic() + timeout_s
317
321
  while time.monotonic() < deadline:
318
322
  process = self._process
319
323
  if process is None:
@@ -344,7 +348,7 @@ class Supervisor:
344
348
  return connection
345
349
  time.sleep(0.05)
346
350
  self._terminate_kernel(force=True)
347
- raise SupervisorError("kernel did not bind its Jupyter ports within 120 seconds")
351
+ raise SupervisorError(f"kernel did not bind its Jupyter ports within {timeout_s:g} seconds")
348
352
 
349
353
  def _poll(self) -> int | None:
350
354
  process = self._process
@@ -582,7 +586,6 @@ def _kernel_launch_spec(
582
586
  session_dir: Path,
583
587
  connection_file: Path,
584
588
  cwd: Path,
585
- venv: Path | None,
586
589
  requirements: tuple[str, ...],
587
590
  environment: Mapping[str, str],
588
591
  owner_marker: str,
@@ -598,7 +601,6 @@ def _kernel_launch_spec(
598
601
  uv=str(uv),
599
602
  cwd=cwd,
600
603
  directory=session_dir / "environment",
601
- venv=venv,
602
604
  locked=locked,
603
605
  requirements=(str(wheel), *requirements),
604
606
  environment=child_env,
@@ -612,15 +614,6 @@ def _kernel_launch_spec(
612
614
  return command, prepared.environment, owned_environment
613
615
 
614
616
 
615
- def _validate_requested_venv(venv: Path | None) -> None:
616
- if venv is None:
617
- return
618
- if not venv.is_dir() or not (venv / "pyvenv.cfg").is_file():
619
- raise SupervisorError(
620
- f"requested target virtual environment does not exist or is invalid: {venv}"
621
- )
622
-
623
-
624
617
  def _remove_owned_session_dir(path: Path, *, expected: OwnedDirectoryIdentity) -> None:
625
618
  if not path.exists() and not path.is_symlink():
626
619
  return
@@ -1,14 +1,10 @@
1
- """Resolve kernel environments through uv, with project packages as constraints.
2
-
3
- uv owns project and workspace discovery, Python selection, dependency groups,
4
- sources, and temporary environments. A base probe captures its exact selection;
5
- constraints prevent an additive overlay from replacing that selection.
6
- """
1
+ """Launch private writable uv environments with native dependency overrides."""
7
2
 
8
3
  from __future__ import annotations
9
4
 
10
5
  import contextlib
11
6
  import json
7
+ import math
12
8
  import os
13
9
  import shlex
14
10
  import shutil
@@ -20,39 +16,12 @@ from dataclasses import dataclass
20
16
  from pathlib import Path
21
17
 
22
18
  import tomllib
19
+ from packaging.version import InvalidVersion, Version
23
20
 
24
21
  from ipi.redaction import redact_url_secrets
25
22
  from ipi.runtime.project import nearest_pyproject, read_project_name
26
23
 
27
- # Only the standard library is available in an arbitrary project interpreter.
28
- _PROBE = """
29
- import importlib.metadata as metadata
30
- import json, os, sys
31
- from urllib.parse import quote
32
- requirements = []
33
- for dist in metadata.distributions():
34
- name = dist.metadata.get("Name")
35
- if not name:
36
- continue
37
- value = name + "==" + dist.version
38
- raw = dist.read_text("direct_url.json")
39
- if raw:
40
- direct = json.loads(raw)
41
- url = direct["url"]
42
- vcs = direct.get("vcs_info")
43
- if vcs:
44
- url = vcs["vcs"] + "+" + url + "@" + vcs["commit_id"]
45
- if direct.get("subdirectory"):
46
- url += ("&" if "#" in url else "#") + "subdirectory=" + quote(direct["subdirectory"])
47
- value = name + " @ " + url
48
- requirements.append(value)
49
- print(json.dumps({
50
- "executable": os.path.realpath(sys.executable),
51
- "prefix": sys.prefix,
52
- "version": ".".join(map(str, sys.version_info[:3])),
53
- "requirements": sorted(set(requirements)),
54
- }))
55
- """
24
+ MIN_UV_VERSION = Version("0.12.3")
56
25
 
57
26
 
58
27
  @dataclass(frozen=True)
@@ -67,8 +36,6 @@ class UvEnvironment:
67
36
  argv: list[str]
68
37
  environment: dict[str, str]
69
38
  project: UvProject | None
70
- base_python: Path
71
- base_prefix: Path
72
39
 
73
40
 
74
41
  def _environment(cwd: Path, environment: Mapping[str, str]) -> dict[str, str]:
@@ -93,8 +60,16 @@ def _environment(cwd: Path, environment: Mapping[str, str]) -> dict[str, str]:
93
60
  return dict(environment)
94
61
 
95
62
 
96
- def _run(cwd: Path, command: Sequence[str], environment: Mapping[str, str]) -> str:
63
+ def preparation_timeout_s() -> float:
64
+ """Budget for cold resolution/builds, now part of the initial uv launch."""
97
65
  timeout = float(os.environ.get("IPI_UV_BASE_ENVIRONMENT_TIMEOUT_S", "") or 180)
66
+ if not math.isfinite(timeout) or timeout <= 0:
67
+ raise ValueError("IPI_UV_BASE_ENVIRONMENT_TIMEOUT_S must be finite and positive")
68
+ return timeout
69
+
70
+
71
+ def _run(cwd: Path, command: Sequence[str], environment: Mapping[str, str]) -> str:
72
+ timeout = preparation_timeout_s()
98
73
  try:
99
74
  with subprocess.Popen(
100
75
  command,
@@ -146,6 +121,22 @@ def project_context(
146
121
  return _project_context(cwd, uv=uv, env=env)
147
122
 
148
123
 
124
+ def _uv_program(cwd: Path, env: Mapping[str, str], uv: str | None) -> str:
125
+ program = uv or shutil.which("uv", path=env.get("PATH"))
126
+ if program is None:
127
+ raise RuntimeError(f"IPi requires uv >= {MIN_UV_VERSION} on PATH; install or upgrade uv")
128
+ reported = _run(cwd, [program, "--version"], env)
129
+ try:
130
+ installed = Version(reported.split()[1])
131
+ except (IndexError, InvalidVersion):
132
+ raise RuntimeError(f"Cannot determine uv version from {reported!r}; upgrade uv") from None
133
+ if installed < MIN_UV_VERSION:
134
+ raise RuntimeError(
135
+ f"IPi requires uv >= {MIN_UV_VERSION}; found {installed}. Upgrade uv before creating a kernel."
136
+ )
137
+ return program
138
+
139
+
149
140
  def _project_context(cwd: Path, *, uv: str | None, env: Mapping[str, str]) -> UvProject | None:
150
141
  selection_root = Path(env.get("UV_PROJECT") or cwd)
151
142
  if not selection_root.is_absolute():
@@ -160,9 +151,7 @@ def _project_context(cwd: Path, *, uv: str | None, env: Mapping[str, str]) -> Uv
160
151
  return None
161
152
  if "project" not in data and "workspace" not in uv_settings:
162
153
  return None
163
- program = uv or shutil.which("uv", path=env.get("PATH"))
164
- if program is None:
165
- raise RuntimeError("uv is required on PATH to resolve IPi projects")
154
+ program = uv or _uv_program(cwd, env, None)
166
155
  common = [program, "--color", "never", "--directory", str(cwd), "workspace"]
167
156
  root = Path(_run(cwd, [*common, "dir"], env)).resolve()
168
157
  members = [
@@ -179,7 +168,6 @@ def prepare_environment(
179
168
  directory: Path,
180
169
  requirements: Sequence[str],
181
170
  editables: Sequence[str] = (),
182
- venv: Path | None = None,
183
171
  locked: bool = False,
184
172
  uv: str | None = None,
185
173
  environment: Mapping[str, str] | None = None,
@@ -188,44 +176,22 @@ def prepare_environment(
188
176
  """Build one local or target-side uv command without writing project state."""
189
177
  env = _environment(cwd, os.environ if environment is None else environment)
190
178
  env.update(overrides or {})
191
- program = uv or shutil.which("uv", path=env.get("PATH"))
192
- if program is None:
193
- raise RuntimeError("uv is required on PATH to launch IPi kernels")
179
+ program = _uv_program(cwd, env, uv)
194
180
  env.pop("UV_PROJECT_ENVIRONMENT", None)
195
181
  env.pop("VIRTUAL_ENV", None)
196
- if locked and venv is not None:
197
- raise ValueError("choose either an explicit venv or locked uv project preparation")
198
182
  project = _project_context(cwd, uv=program, env=env)
199
- command = [program, "run", "--directory", str(cwd)]
200
- if venv is not None:
201
- env["VIRTUAL_ENV"] = str(venv)
202
- command.extend(("--active", "--no-sync"))
203
- else:
204
- command.append("--isolated")
183
+ command = [program, "run", "--directory", str(cwd), "--isolated"]
205
184
  if project is None:
206
185
  command.append("--no-project")
207
186
  if locked:
208
187
  if project is None:
209
188
  raise ValueError("locked kernel preparation requires a uv project")
210
189
  command.append("--locked")
211
- payload = json.loads(_run(cwd, [*command, "python", "-c", _PROBE], env).splitlines()[-1])
212
190
  directory.mkdir(parents=True, exist_ok=True)
213
- constraints = directory / "project-constraints.txt"
214
- constraints.write_text("\n".join(payload["requirements"]) + "\n")
215
191
  overlay = directory / "kernel-requirements.txt"
216
- overlay.write_text(f"-c {constraints}\n" + "\n".join(requirements) + "\n")
217
- # Preserve uv's Python choice across the probe and final launch. An explicit
218
- # venv owns its interpreter and does not accept another Python selector.
219
- if venv is None:
220
- command.extend(("--python", payload["executable"]))
192
+ overlay.write_text("\n".join(requirements) + "\n")
221
193
  command.extend(("--with-requirements", str(overlay)))
222
194
  for editable in editables:
223
195
  command.extend(("--with-editable", editable))
224
196
  command.append("python")
225
- return UvEnvironment(
226
- command,
227
- env,
228
- project,
229
- Path(payload["executable"]),
230
- Path(payload["prefix"]),
231
- )
197
+ return UvEnvironment(command, env, project)
@@ -40,7 +40,6 @@ class KernelRecord(Model):
40
40
  kernel_id: str
41
41
  state: KernelState
42
42
  cwd: str
43
- venv: str | None
44
43
  additional_dependencies: list[str]
45
44
  created_at: float
46
45
  request_id: str | None
@@ -53,10 +53,10 @@ from ipi._transport import (
53
53
  SshKernelLocation,
54
54
  display_path,
55
55
  host_artifact_root,
56
- resolve_location_path,
57
56
  )
58
57
  from ipi._transport.local import LocalPreparedTransport
59
58
  from ipi._transport.ssh import SshPreparedTransport
59
+ from ipi._uv_environment import preparation_timeout_s
60
60
  from ipi.plugins.catalog import KernelEnvironment
61
61
  from ipi.plugins.kernel import kernel_setup_code
62
62
  from ipi.redaction import (
@@ -574,7 +574,6 @@ class Kernel:
574
574
  if self._transport_request is None:
575
575
  self._transport_request = KernelLaunchRequest(
576
576
  cwd=self._location.cwd,
577
- venv=self._launch_command.resolved_virtual_env,
578
577
  requirements=self._extra_packages,
579
578
  environment={},
580
579
  )
@@ -612,7 +611,6 @@ class Kernel:
612
611
  cwd: str | Path | None = None,
613
612
  *,
614
613
  location: KernelLocation | None = None,
615
- venv: str | Path | None = None,
616
614
  locked: bool = False,
617
615
  kernel_dir: str | Path | None = None,
618
616
  text_char_limit: int = DEFAULT_TEXT_CHAR_LIMIT,
@@ -630,7 +628,6 @@ class Kernel:
630
628
  return launch_kernel(
631
629
  cwd=cwd,
632
630
  location=location,
633
- venv=venv,
634
631
  locked=locked,
635
632
  kernel_dir=kernel_dir,
636
633
  text_char_limit=text_char_limit,
@@ -1871,7 +1868,9 @@ class Kernel:
1871
1868
  with timed("kernel_launch", "kernel_ready"):
1872
1869
  _wait_for_kernel_ready(
1873
1870
  client,
1874
- timeout_s=KERNEL_READY_TIMEOUT_S,
1871
+ # Local uv now resolves/builds in this process before Jupyter starts.
1872
+ timeout_s=KERNEL_READY_TIMEOUT_S
1873
+ + (0 if self._location.remote else preparation_timeout_s()),
1875
1874
  alive=manager.is_alive,
1876
1875
  abort=self._launch_abort,
1877
1876
  )
@@ -3276,7 +3275,6 @@ def launch_kernel(
3276
3275
  cwd: str | Path | None = None,
3277
3276
  *,
3278
3277
  location: KernelLocation | None = None,
3279
- venv: str | Path | None = None,
3280
3278
  locked: bool = False,
3281
3279
  kernel_dir: str | Path | None = None,
3282
3280
  text_char_limit: int = DEFAULT_TEXT_CHAR_LIMIT,
@@ -3325,10 +3323,6 @@ def launch_kernel(
3325
3323
  target_requirements,
3326
3324
  runtime_root=prepared_transport.runtime_root,
3327
3325
  )
3328
- resolved_venv = resolve_location_path(
3329
- str(venv) if venv is not None else None,
3330
- location=location,
3331
- )
3332
3326
  launch_command = UvRunCommand(
3333
3327
  argv=[],
3334
3328
  env_delta={"PYDEVD_DISABLE_FILE_VALIDATION": "1"},
@@ -3336,7 +3330,7 @@ def launch_kernel(
3336
3330
  project_root=None,
3337
3331
  package_name=None,
3338
3332
  resolved_python_executable=None,
3339
- resolved_virtual_env=resolved_venv,
3333
+ resolved_virtual_env=None,
3340
3334
  nearest_pyproject_path=None,
3341
3335
  nearest_project_name=None,
3342
3336
  workspace_root=None,
@@ -3344,9 +3338,6 @@ def launch_kernel(
3344
3338
  )
3345
3339
  transport_request = KernelLaunchRequest(
3346
3340
  cwd=location.cwd,
3347
- venv=(
3348
- PurePosixPath(resolved_venv.as_posix()) if resolved_venv is not None else None
3349
- ),
3350
3341
  requirements=requirements,
3351
3342
  environment={},
3352
3343
  locked=locked,
@@ -3372,7 +3363,6 @@ def launch_kernel(
3372
3363
  launch_command = _build_uv_run_command(
3373
3364
  cwd=resolved_cwd,
3374
3365
  directory=resolved_kernel_dir / "environment",
3375
- venv=venv,
3376
3366
  locked=locked,
3377
3367
  environment=os.environ,
3378
3368
  extra_packages=tuple(
@@ -3561,7 +3551,6 @@ def _build_uv_run_command(
3561
3551
  *,
3562
3552
  cwd: Path,
3563
3553
  directory: Path,
3564
- venv: str | Path | None,
3565
3554
  extra_packages: tuple[str, ...],
3566
3555
  editable_packages: tuple[str, ...] = (),
3567
3556
  locked: bool = False,
@@ -3569,7 +3558,6 @@ def _build_uv_run_command(
3569
3558
  ) -> UvRunCommand:
3570
3559
  from ipi._uv_environment import prepare_environment
3571
3560
 
3572
- active_virtual_env = _resolve_virtualenv_path(venv, cwd=cwd) if venv else None
3573
3561
  overlay_args = _ipi_overlay_args() + _kernel_overlay_args(
3574
3562
  extra_packages,
3575
3563
  editable_packages=editable_packages,
@@ -3578,7 +3566,6 @@ def _build_uv_run_command(
3578
3566
  prepared = prepare_environment(
3579
3567
  cwd=cwd,
3580
3568
  directory=directory,
3581
- venv=active_virtual_env,
3582
3569
  requirements=tuple(value for flag, value in pairs if flag == "--with"),
3583
3570
  editables=tuple(value for flag, value in pairs if flag == "--with-editable"),
3584
3571
  locked=locked,
@@ -3589,8 +3576,8 @@ def _build_uv_run_command(
3589
3576
  package_name = project.name if project else None
3590
3577
  nearest_path = project.member / "pyproject.toml" if project else None
3591
3578
  workspace_path = project.root if project and project.root != project.member else None
3592
- resolved_python_executable = prepared.base_python
3593
- resolved_virtual_env = active_virtual_env
3579
+ resolved_python_executable = None
3580
+ resolved_virtual_env = None
3594
3581
  env_delta = dict(prepared.environment)
3595
3582
  env_delta.update(
3596
3583
  _kernel_environment_env_delta(
@@ -4463,33 +4450,6 @@ def _preserve_ipi_view_state(memory_ipi_cells: list[Any], disk_cells: list[Any])
4463
4450
  _carry_over_view_state(cell, disk_cell)
4464
4451
 
4465
4452
 
4466
- def _virtualenv_path(path: Path) -> Path:
4467
- if path.is_file():
4468
- path = path.parent.parent
4469
- _virtualenv_python_path(path)
4470
- return path.resolve(strict=False)
4471
-
4472
-
4473
- def _resolve_virtualenv_path(venv: str | Path, *, cwd: Path) -> Path:
4474
- path = Path(venv).expanduser()
4475
- if not path.is_absolute():
4476
- path = cwd / path
4477
- return _virtualenv_path(path)
4478
-
4479
-
4480
- def _virtualenv_python_path(path: Path) -> Path:
4481
- if path.is_file():
4482
- return path.resolve(strict=False)
4483
- for candidate in (
4484
- path / "bin" / "python",
4485
- path / "Scripts" / "python.exe",
4486
- path / "Scripts" / "python",
4487
- ):
4488
- if candidate.exists():
4489
- return candidate.resolve(strict=False)
4490
- raise RuntimeError(f"virtualenv at {path} does not contain a Python executable")
4491
-
4492
-
4493
4453
  def _collect_iopub_message(
4494
4454
  outputs: list[KernelOutput],
4495
4455
  msg: dict[str, Any],