lean-runtime 3.0.0__tar.gz → 3.0.2__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 (188) hide show
  1. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/CHANGELOG.md +28 -0
  2. {lean_runtime-3.0.0/lean_runtime.egg-info → lean_runtime-3.0.2}/PKG-INFO +1 -1
  3. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/portable-copies.md +10 -10
  4. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/v1-precision.md +25 -0
  5. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/api.py +2 -1
  6. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/engine.py +7 -1
  7. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/probe.py +4 -0
  8. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/oci.py +14 -1
  9. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/run_cli.py +6 -7
  10. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/runtime.py +15 -3
  11. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/toolchain_oci.py +23 -3
  12. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/toolchains.py +15 -8
  13. {lean_runtime-3.0.0 → lean_runtime-3.0.2/lean_runtime.egg-info}/PKG-INFO +1 -1
  14. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/pyproject.toml +1 -1
  15. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/inspect-v1.schema.json +28 -5
  16. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/discovery/test_engine.py +19 -1
  17. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_bundles.py +15 -0
  18. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_prebuilt_policy.py +23 -0
  19. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_run_cli.py +25 -0
  20. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_schemas.py +26 -0
  21. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_toolchain_slim.py +100 -1
  22. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/LICENSE +0 -0
  23. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/MANIFEST.in +0 -0
  24. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/README.md +0 -0
  25. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/environments.toml +0 -0
  26. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/core-v4.32.2.lock.json +0 -0
  27. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
  28. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
  29. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
  30. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
  31. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  32. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  33. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  34. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
  35. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/compatibility/README.md +0 -0
  36. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
  37. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/compatibility/mathlib-4.32.2.json +0 -0
  38. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/compatibility/mathlib-4.32.2.toml +0 -0
  39. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/compatibility/mathlib-4.33.0.json +0 -0
  40. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/compatibility/mathlib-4.33.0.toml +0 -0
  41. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/architecture.md +0 -0
  42. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/captures.md +0 -0
  43. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/case-study-v1.md +0 -0
  44. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/cli.md +0 -0
  45. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/development.md +0 -0
  46. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/environments.md +0 -0
  47. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/getting-started.md +0 -0
  48. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/index.md +0 -0
  49. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/lake-artifact-cache-design.md +0 -0
  50. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/local-projects.md +0 -0
  51. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/project-publishing.md +0 -0
  52. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/python-api.md +0 -0
  53. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/ready-programs.md +0 -0
  54. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/standalone-files.md +0 -0
  55. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/docs/trust-and-limitations.md +0 -0
  56. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/examples/mathlib.toml +0 -0
  57. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/__init__.py +0 -0
  58. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/__main__.py +0 -0
  59. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/_git.py +0 -0
  60. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/_paths.py +0 -0
  61. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/backends.py +0 -0
  62. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/bundles.py +0 -0
  63. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/capsules.py +0 -0
  64. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/cli.py +0 -0
  65. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/comparison.py +0 -0
  66. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/console.py +0 -0
  67. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/decisions.py +0 -0
  68. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/diagnostics.py +0 -0
  69. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/__init__.py +0 -0
  70. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/analyzer.py +0 -0
  71. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/candidate.py +0 -0
  72. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/catalog.py +0 -0
  73. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/catalog_build.py +0 -0
  74. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/catalog_cli.py +0 -0
  75. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/catalog_manifest.py +0 -0
  76. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/data/catalog.json +0 -0
  77. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/defaults.py +0 -0
  78. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/errors.py +0 -0
  79. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/module_inventory.py +0 -0
  80. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/planner.py +0 -0
  81. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/policy.py +0 -0
  82. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/py.typed +0 -0
  83. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/result.py +0 -0
  84. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/schema_resources.py +0 -0
  85. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  86. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  87. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  88. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/environments.py +0 -0
  89. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/errors.py +0 -0
  90. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/events.py +0 -0
  91. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/facade.py +0 -0
  92. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/frontmatter.py +0 -0
  93. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/header_cache.py +0 -0
  94. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/health.py +0 -0
  95. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/identifier_resolver.py +0 -0
  96. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/lake.py +0 -0
  97. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/lake_cache.py +0 -0
  98. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/lockfiles.py +0 -0
  99. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/locking.py +0 -0
  100. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/matrix.py +0 -0
  101. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/models.py +0 -0
  102. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/oci_protocol.py +0 -0
  103. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/packs.py +0 -0
  104. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/policies.py +0 -0
  105. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/profiling.py +0 -0
  106. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/programs.py +0 -0
  107. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/project_execution.py +0 -0
  108. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/project_sharing.py +0 -0
  109. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/projects.py +0 -0
  110. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/publisher_verification.py +0 -0
  111. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/py.typed +0 -0
  112. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/references.py +0 -0
  113. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/resolver.py +0 -0
  114. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/schema_resources.py +0 -0
  115. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/serialization.py +0 -0
  116. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/shared_projects.py +0 -0
  117. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/specs.py +0 -0
  118. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/store.py +0 -0
  119. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/timings.py +0 -0
  120. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/toolchain_slim.py +0 -0
  121. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/verification.py +0 -0
  122. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime/wire.py +0 -0
  123. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime.egg-info/SOURCES.txt +0 -0
  124. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime.egg-info/dependency_links.txt +0 -0
  125. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime.egg-info/entry_points.txt +0 -0
  126. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime.egg-info/requires.txt +0 -0
  127. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/lean_runtime.egg-info/top_level.txt +0 -0
  128. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/mkdocs.yml +0 -0
  129. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/attestation-v1.schema.json +0 -0
  130. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/check-batch-v1.schema.json +0 -0
  131. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/cleanup-v1.schema.json +0 -0
  132. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/comparison-v1.schema.json +0 -0
  133. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/execution-v1.schema.json +0 -0
  134. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/matrix-v1.schema.json +0 -0
  135. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/plan-v1.schema.json +0 -0
  136. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/profile-v1.schema.json +0 -0
  137. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/publication-v1.schema.json +0 -0
  138. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/schemas/verify-v1.schema.json +0 -0
  139. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/scripts/announcement_gate.py +0 -0
  140. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/scripts/lake_cache_gate.py +0 -0
  141. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/scripts/registry_preflight.py +0 -0
  142. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/scripts/run_compatibility.py +0 -0
  143. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/scripts/run_v1_case_study.py +0 -0
  144. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/scripts/smoke_wheel.py +0 -0
  145. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/setup.cfg +0 -0
  146. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/conftest.py +0 -0
  147. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/discovery/e2e/test_real_discovery.py +0 -0
  148. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/discovery/test_analyzer.py +0 -0
  149. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/discovery/test_catalog.py +0 -0
  150. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/discovery/test_catalog_build.py +0 -0
  151. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/discovery/test_discovery_schema_resources.py +0 -0
  152. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/discovery/test_planner.py +0 -0
  153. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/discovery/test_policy.py +0 -0
  154. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_backend.py +0 -0
  155. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_capsules.py +0 -0
  156. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_cli.py +0 -0
  157. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_console.py +0 -0
  158. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_diagnostics.py +0 -0
  159. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_environment_integration.py +0 -0
  160. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_events_health.py +0 -0
  161. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_facade.py +0 -0
  162. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_frontmatter.py +0 -0
  163. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_git.py +0 -0
  164. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_header_cache.py +0 -0
  165. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_identifier_resolver.py +0 -0
  166. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_interactive.py +0 -0
  167. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_lake_cache.py +0 -0
  168. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_lockfiles.py +0 -0
  169. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_locking.py +0 -0
  170. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_models.py +0 -0
  171. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_multifile.py +0 -0
  172. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_oci_protocol.py +0 -0
  173. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_packs.py +0 -0
  174. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_paths.py +0 -0
  175. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_programs.py +0 -0
  176. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_project_publish.py +0 -0
  177. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_projects.py +0 -0
  178. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_references.py +0 -0
  179. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_runtime.py +0 -0
  180. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_schema_resources.py +0 -0
  181. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_signatures.py +0 -0
  182. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_specs.py +0 -0
  183. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_store.py +0 -0
  184. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_toolchain_oci.py +0 -0
  185. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_toolchains.py +0 -0
  186. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_v1_precision.py +0 -0
  187. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_verification_inventory.py +0 -0
  188. {lean_runtime-3.0.0 → lean_runtime-3.0.2}/tests/test_verification_probe.py +0 -0
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.0.2 - 2026-08-17
6
+
7
+ - Route the first check directly through a newly downloaded slim Lean runtime.
8
+ Command construction and executable hashing now re-evaluate toolchain state
9
+ after remote acquisition and ignore incomplete Elan toolchain directories,
10
+ preventing cold discovery from starting an unintended full toolchain install.
11
+
12
+
13
+ ## 3.0.1 - 2026-08-17
14
+
15
+ - Point the portable-copy consuming examples at
16
+ `ghcr.io/alerad/lean-runtime-cache`, which serves check capsules a 3.0 client
17
+ can read, and make the publishing examples use the `ghcr.io/OWNER/...`
18
+ placeholder style the other pages use. The previous
19
+ `ghcr.io/alerad/leancert-runtime` examples named a library that now holds only
20
+ pre-capsule content.
21
+ - Make standalone discovery acquire the verified slim Lean runtime inside the
22
+ acquisition budget, leaving the candidate timeout exclusively for the Lean
23
+ compiler probe and preserving the specific timeout diagnostic.
24
+ - Make `run --offline` genuinely retained-only: a missing exact environment
25
+ now fails before source materialization, Elan installation, or network
26
+ access.
27
+ - Make every `run --explain --json` routing shape validate against the closed
28
+ `lean-runtime.inspect/v1` schema.
29
+ - Return exit 130 with a concise message when either `lean-run` or
30
+ `lean-runtime run` is interrupted during preparation.
31
+
32
+
5
33
  ## 3.0.0 - 2026-08-16
6
34
 
7
35
  The first release with one canonical command vocabulary, capsule-only
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 3.0.0
3
+ Version: 3.0.2
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -46,7 +46,7 @@ specification or lock:
46
46
 
47
47
  ```python
48
48
  runtime = Runtime(
49
- libraries=["ghcr.io/alerad/leancert-runtime"],
49
+ libraries=["ghcr.io/alerad/lean-runtime-cache"],
50
50
  availability="auto",
51
51
  )
52
52
  environment = runtime.open_exact(lock)
@@ -55,7 +55,7 @@ environment = runtime.open_exact(lock)
55
55
  The equivalent environment variables are:
56
56
 
57
57
  ```bash
58
- export LEAN_RUNTIME_LIBRARIES=ghcr.io/alerad/leancert-runtime
58
+ export LEAN_RUNTIME_LIBRARIES=ghcr.io/alerad/lean-runtime-cache
59
59
  export LEAN_RUNTIME_AVAILABILITY=auto
60
60
  ```
61
61
 
@@ -68,7 +68,7 @@ Explicit prefetch uses the same verified path:
68
68
 
69
69
  ```bash
70
70
  lean-runtime \
71
- --library ghcr.io/alerad/leancert-runtime \
71
+ --library ghcr.io/alerad/lean-runtime-cache \
72
72
  download environment.lock.json
73
73
  ```
74
74
 
@@ -98,10 +98,10 @@ Actions identity:
98
98
 
99
99
  ```python
100
100
  runtime = Runtime(
101
- libraries=["ghcr.io/alerad/leancert-runtime"],
101
+ libraries=["ghcr.io/alerad/lean-runtime-cache"],
102
102
  publisher_verification="required",
103
103
  trusted_publisher=(
104
- "https://github.com/alerad/leancert/.github/workflows/cache.yml@refs/heads/main"
104
+ "https://github.com/alerad/lean-runtime/.github/workflows/public-cache.yml@refs/heads/main"
105
105
  ),
106
106
  trusted_issuer="https://token.actions.githubusercontent.com",
107
107
  )
@@ -121,7 +121,7 @@ Check push access without building or uploading an environment:
121
121
 
122
122
  ```bash
123
123
  lean-runtime publish environment \
124
- --publish-to ghcr.io/alerad/leancert-runtime \
124
+ --publish-to ghcr.io/OWNER/lean-environments \
125
125
  --check-access
126
126
  ```
127
127
 
@@ -156,7 +156,7 @@ export LEAN_RUNTIME_REGISTRY_USERNAME=alerad
156
156
  export LEAN_RUNTIME_REGISTRY_PASSWORD="$GHCR_TOKEN"
157
157
 
158
158
  lean-runtime publish environment environment.lock.json \
159
- --publish-to ghcr.io/alerad/leancert-runtime \
159
+ --publish-to ghcr.io/OWNER/lean-environments \
160
160
  --tag v4.32.2.4 \
161
161
  --sign --attest
162
162
  ```
@@ -208,7 +208,7 @@ steps:
208
208
  - uses: ./.github/actions/publish-environment
209
209
  with:
210
210
  lock: environment.lock.json
211
- library: ghcr.io/${{ github.repository_owner }}/leancert-runtime
211
+ library: ghcr.io/${{ github.repository_owner }}/lean-environments
212
212
  tag: ${{ github.ref_name }}
213
213
  username: ${{ github.actor }}
214
214
  password: ${{ secrets.GITHUB_TOKEN }}
@@ -219,7 +219,7 @@ index and retain its JSON result:
219
219
 
220
220
  ```bash
221
221
  lean-runtime publish environment environment.lock.json \
222
- --publish-to ghcr.io/alerad/leancert-runtime \
222
+ --publish-to ghcr.io/OWNER/lean-environments \
223
223
  --platform-only > platform-result.json
224
224
  ```
225
225
 
@@ -228,7 +228,7 @@ multi-platform index and human aliases:
228
228
 
229
229
  ```bash
230
230
  lean-runtime finalize environment "$LOCK_ID" results/*.json \
231
- --library ghcr.io/alerad/leancert-runtime \
231
+ --library ghcr.io/OWNER/lean-environments \
232
232
  --tag "$GITHUB_REF_NAME"
233
233
  ```
234
234
 
@@ -103,6 +103,31 @@ improve without a schema change. Other `--json` commands (for example `storage`,
103
103
  contracts. Exit code 0 means success, 1 is a completed negative
104
104
  result, and 2 is invalid invocation or an exceptional runtime failure.
105
105
 
106
+ Schemas reference shared definitions by their canonical absolute IDs. To validate entirely
107
+ from the installed wheel, register all packaged schemas instead of allowing a validator to
108
+ retrieve those IDs over the network:
109
+
110
+ ```python
111
+ import json
112
+ from pathlib import Path
113
+
114
+ import lean_runtime
115
+ from jsonschema import Draft202012Validator
116
+ from referencing import Registry, Resource
117
+
118
+ documents = [
119
+ json.loads(lean_runtime.schema_path(name).read_text()) for name in lean_runtime.SCHEMA_NAMES
120
+ ]
121
+ registry = Registry().with_resources(
122
+ (document["$id"], Resource.from_contents(document)) for document in documents
123
+ )
124
+ schema = next(
125
+ document for document in documents if document["$id"].endswith("/execution-v1.schema.json")
126
+ )
127
+ value = json.loads(Path("result.execution.json").read_text())
128
+ Draft202012Validator(schema, registry=registry).validate(value)
129
+ ```
130
+
106
131
  ## Reproducible case study
107
132
 
108
133
  After preparing an exact environment, run:
@@ -58,8 +58,9 @@ class Discovery:
58
58
  availability=availability,
59
59
  libraries=libraries,
60
60
  on_event=events.append,
61
+ allow_source_build=self.policy.allow_source_build,
61
62
  )
62
- if not self.policy.allow_source_build and runtime.availability == "auto":
63
+ if not self.policy.allow_source_build and runtime.allow_source_build:
63
64
  raise PolicyError(
64
65
  "the injected Runtime permits source fallback but allow_source_build is false"
65
66
  )
@@ -361,11 +361,17 @@ def discover(
361
361
  if attempt.status == "lean_rejected"
362
362
  for diagnostic in attempt.diagnostics
363
363
  )
364
+ timeout_diagnostics = tuple(
365
+ diagnostic
366
+ for attempt in attempts
367
+ if attempt.status == "timeout"
368
+ for diagnostic in attempt.diagnostics
369
+ )
364
370
  return DiscoveryResult(
365
371
  status="not_found",
366
372
  confidence="exhausted",
367
373
  plan=plan,
368
374
  attempts=tuple(attempts),
369
375
  duration_seconds=time.monotonic() - started,
370
- diagnostics=(reason, *rejection_diagnostics),
376
+ diagnostics=(*timeout_diagnostics, reason, *rejection_diagnostics),
371
377
  )
@@ -94,6 +94,10 @@ class LeanRuntimeProbe:
94
94
  import_roots=self.import_roots,
95
95
  cancel=cancel,
96
96
  )
97
+ # Acquisition is complete only when the exact candidate can invoke
98
+ # Lean without downloading or installing anything during the
99
+ # compiler probe.
100
+ self.runtime.toolchains.ensure(candidate.entry.toolchain, cancel=cancel)
97
101
  except MaterializationError as exc:
98
102
  if exc.phase in {"lock-validation", "source-validation", "bundle-validation"}:
99
103
  raise ProbeIntegrityFailure(str(exc)) from exc
@@ -10,6 +10,7 @@ import re
10
10
  import shutil
11
11
  import subprocess
12
12
  import tempfile
13
+ import threading
13
14
  import time
14
15
  import urllib.error
15
16
  import urllib.parse
@@ -415,6 +416,8 @@ class OCIRegistryClient:
415
416
  descriptor: dict[str, Any],
416
417
  store: EnvironmentStore,
417
418
  events: EventEmitter,
419
+ *,
420
+ cancel: threading.Event | None = None,
418
421
  ) -> Path:
419
422
  digest = descriptor.get("digest")
420
423
  size = descriptor.get("size")
@@ -422,7 +425,13 @@ class OCIRegistryClient:
422
425
  if match is None or not isinstance(size, int) or size < 0:
423
426
  raise EnvironmentError("OCI manifest contains an invalid blob descriptor")
424
427
  destination = store.oci_blobs / match.group(1)
425
- with FileLock(store.lock_dir / f"oci-{match.group(1)}.lock", timeout=1800):
428
+ with FileLock(
429
+ store.lock_dir / f"oci-{match.group(1)}.lock",
430
+ timeout=1800,
431
+ cancel=cancel,
432
+ ):
433
+ if cancel is not None and cancel.is_set():
434
+ raise EnvironmentError("OCI blob download was cancelled")
426
435
  if destination.is_file() and destination.stat().st_size == size:
427
436
  if _digest_path(destination) == digest:
428
437
  events.emit(
@@ -432,6 +441,8 @@ class OCIRegistryClient:
432
441
  destination.unlink()
433
442
  temporary = destination.with_name(f".{destination.name}.partial")
434
443
  for attempt in range(1, _BLOB_INTEGRITY_ATTEMPTS + 1):
444
+ if cancel is not None and cancel.is_set():
445
+ raise EnvironmentError("OCI blob download was cancelled")
435
446
  if temporary.exists() and temporary.stat().st_size > size:
436
447
  temporary.unlink()
437
448
  offset = temporary.stat().st_size if temporary.exists() else 0
@@ -470,6 +481,8 @@ class OCIRegistryClient:
470
481
  last_progress = 0.0
471
482
  with response, temporary.open(mode) as output:
472
483
  while chunk := response.read(1024 * 1024):
484
+ if cancel is not None and cancel.is_set():
485
+ raise EnvironmentError("OCI blob download was cancelled")
473
486
  written += len(chunk)
474
487
  if written > size:
475
488
  raise EnvironmentError("OCI blob exceeds its declared size")
@@ -397,13 +397,7 @@ def run(arguments: argparse.Namespace, *, command_name: str = "lean-run") -> int
397
397
  if arguments.json:
398
398
  print(
399
399
  json.dumps(
400
- {
401
- "schema": "lean-runtime.inspect/v1",
402
- "ok": True,
403
- "data": explanation,
404
- "warnings": [],
405
- "errors": [],
406
- },
400
+ envelope("lean-runtime.inspect/v1", ok=True, data=explanation),
407
401
  ensure_ascii=False,
408
402
  indent=2,
409
403
  sort_keys=True,
@@ -443,6 +437,7 @@ def run(arguments: argparse.Namespace, *, command_name: str = "lean-run") -> int
443
437
  availability=availability,
444
438
  libraries=libraries,
445
439
  max_download_bytes=arguments.max_download,
440
+ allow_source_build=not arguments.offline and not arguments.no_source_build,
446
441
  )
447
442
  policy = ExecutionPolicy(timeout_seconds=arguments.check_timeout)
448
443
  if context.lock is not None:
@@ -528,6 +523,10 @@ def run(arguments: argparse.Namespace, *, command_name: str = "lean-run") -> int
528
523
  show_timings=arguments.timings,
529
524
  )
530
525
  return 0 if result.ok else 1
526
+ except KeyboardInterrupt:
527
+ renderer.close()
528
+ print(f"{command_name}: interrupted", file=sys.stderr)
529
+ return 130
531
530
  except (DiscoveryError, LeanRuntimeError, OSError, UnicodeError, ValueError) as exc:
532
531
  renderer.close()
533
532
  if arguments.json:
@@ -209,6 +209,7 @@ class Runtime:
209
209
  availability: str | None = None,
210
210
  libraries: Sequence[str] | None = None,
211
211
  max_download_bytes: int | None = None,
212
+ allow_source_build: bool = True,
212
213
  publisher_verification: str = "ignore",
213
214
  trusted_publisher: str | None = None,
214
215
  trusted_issuer: str | None = None,
@@ -255,6 +256,7 @@ class Runtime:
255
256
  else DEFAULT_ENVIRONMENT_LIBRARIES
256
257
  )
257
258
  self.availability = availability
259
+ self.allow_source_build = allow_source_build
258
260
  self.max_download_bytes = max_download_bytes
259
261
  self.verification_executable = verification_tool
260
262
  self.signature_verifier = (
@@ -286,7 +288,11 @@ class Runtime:
286
288
  self.toolchains.remote_ensure = self._acquire_check_toolchain
287
289
  self.environments.sparse_acquirer = self._acquire_sparse_modules
288
290
 
289
- def _acquire_check_toolchain(self, toolchain: str) -> bool:
291
+ def _acquire_check_toolchain(
292
+ self,
293
+ toolchain: str,
294
+ cancel: threading.Event | None = None,
295
+ ) -> bool:
290
296
  if self.availability == "local":
291
297
  raise ToolchainError(
292
298
  f"toolchain {normalize_toolchain(toolchain)!r} is not available locally; "
@@ -294,7 +300,7 @@ class Runtime:
294
300
  )
295
301
  for library in self.toolchain_libraries:
296
302
  try:
297
- return library.pull(toolchain)
303
+ return library.pull(toolchain, cancel=cancel)
298
304
  except DownloadUnavailable:
299
305
  continue
300
306
  return False
@@ -347,6 +353,7 @@ class Runtime:
347
353
  ) -> Environment:
348
354
  environment_id = environment_identity(lock, build_profile)
349
355
  destination = self.store.environment_path(environment_id)
356
+ imported = False
350
357
  if self.max_download_bytes is not None and self.availability != "local":
351
358
  preflight = self.plan_exact(
352
359
  lock,
@@ -377,7 +384,6 @@ class Runtime:
377
384
  "lean-run --plan"
378
385
  )
379
386
  if not destination.is_dir() and self.availability != "local":
380
- imported = False
381
387
  rejections: list[str] = []
382
388
  for library in self.libraries:
383
389
  try:
@@ -412,6 +418,12 @@ class Runtime:
412
418
  capability="source_build",
413
419
  environment_id=environment_id,
414
420
  )
421
+ if not destination.is_dir() and not imported and not self.allow_source_build:
422
+ raise EnvironmentError(
423
+ f"environment {environment_id} is not retained locally; offline mode "
424
+ "does not permit downloading, toolchain installation, or source "
425
+ "materialization"
426
+ )
415
427
  return self.environments.ensure(
416
428
  lock,
417
429
  name=name,
@@ -10,6 +10,7 @@ import shutil
10
10
  import subprocess
11
11
  import tarfile
12
12
  import tempfile
13
+ import threading
13
14
  from dataclasses import dataclass
14
15
  from pathlib import Path, PurePosixPath
15
16
  from typing import Any
@@ -314,16 +315,35 @@ class OCIToolchainLibrary:
314
315
  dict(config["slim_manifest"]),
315
316
  )
316
317
 
317
- def pull(self, toolchain: str) -> bool:
318
+ def pull(
319
+ self,
320
+ toolchain: str,
321
+ *,
322
+ cancel: threading.Event | None = None,
323
+ ) -> bool:
318
324
  local_check = getattr(self.toolchains, "is_available_locally", None)
319
325
  if callable(local_check) and local_check(toolchain):
320
326
  return True
321
327
  plan = self.plan(toolchain)
322
328
  self.client.cache_verified_blob(plan.config_data, plan.config_descriptor, self.store)
323
- layer = self.client.download_blob(plan.descriptor, self.store, self.events)
329
+ try:
330
+ layer = self.client.download_blob(
331
+ plan.descriptor,
332
+ self.store,
333
+ self.events,
334
+ cancel=cancel,
335
+ )
336
+ except EnvironmentError as exc:
337
+ if cancel is not None and cancel.is_set():
338
+ raise ToolchainError(
339
+ f"Lean toolchain download was cancelled: {normalize_toolchain(toolchain)!r}"
340
+ ) from exc
341
+ raise
324
342
  destination = self.toolchains.slim_path(toolchain)
325
343
  with FileLock(
326
- self.store.lock_dir / f"toolchain-{toolchain_reference(toolchain)}.lock", timeout=1800
344
+ self.store.lock_dir / f"toolchain-{toolchain_reference(toolchain)}.lock",
345
+ timeout=1800,
346
+ cancel=cancel,
327
347
  ):
328
348
  if self.toolchains.is_available_locally(toolchain):
329
349
  return True
@@ -81,7 +81,7 @@ class ToolchainManager:
81
81
  self.home = Path(home).expanduser().resolve() if home else default_runtime_home()
82
82
  self.elan_home = self.home / "elan"
83
83
  self.events = events or EventEmitter()
84
- self.remote_ensure: Callable[[str], bool] | None = None
84
+ self.remote_ensure: Callable[[str, threading.Event | None], bool] | None = None
85
85
  self._executable_digests: dict[str, tuple[int, int, str]] = {}
86
86
 
87
87
  @property
@@ -264,7 +264,7 @@ class ToolchainManager:
264
264
  name = normalize_toolchain(toolchain)
265
265
  if self.has_slim(name) or (self._elan_toolchain_dir(name) / "bin" / "lean").is_file():
266
266
  return name
267
- if self.remote_ensure is not None and self.remote_ensure(name):
267
+ if self.remote_ensure is not None and self.remote_ensure(name, cancel):
268
268
  if not self.has_slim(name):
269
269
  raise ToolchainError("remote toolchain acquisition returned without a slim copy")
270
270
  return name
@@ -325,26 +325,33 @@ class ToolchainManager:
325
325
  A full Elan installation is preferred; when only a verified slim copy
326
326
  remains, its executables are invoked directly.
327
327
  """
328
- name = normalize_toolchain(toolchain)
329
- if self.has_slim(name) and not self._elan_toolchain_dir(name).is_dir():
328
+ name = self.ensure(toolchain)
329
+ full = self._elan_toolchain_dir(name)
330
+ full_lean = full / "bin" / "lean"
331
+ if os.name == "nt":
332
+ full_lean = full_lean.with_suffix(".exe")
333
+ if self.has_slim(name) and not full_lean.is_file():
330
334
  binary = self.slim_path(name) / "bin" / executable
335
+ if os.name == "nt":
336
+ binary = binary.with_suffix(".exe")
331
337
  if not binary.is_file():
332
338
  raise ToolchainError(
333
339
  f"slim toolchain {name!r} does not provide {executable!r}; "
334
340
  "reinstall the full toolchain for this operation"
335
341
  )
336
342
  return [str(binary), *args]
337
- name = self.ensure(name)
338
343
  return [str(self.elan_path()), "run", name, executable, *args]
339
344
 
340
345
  def executable_digest(self, toolchain: str, executable: str) -> str:
341
346
  """Hash the exact resolved executable for compatibility-cache identities."""
342
- name = normalize_toolchain(toolchain)
347
+ name = self.ensure(toolchain)
343
348
  full = self._elan_toolchain_dir(name)
344
- if self.has_slim(name) and not full.is_dir():
349
+ full_lean = full / "bin" / "lean"
350
+ if os.name == "nt":
351
+ full_lean = full_lean.with_suffix(".exe")
352
+ if self.has_slim(name) and not full_lean.is_file():
345
353
  binary = self.slim_path(name) / "bin" / executable
346
354
  else:
347
- self.ensure(name)
348
355
  binary = full / "bin" / executable
349
356
  if os.name == "nt" and not binary.is_file():
350
357
  binary = binary.with_suffix(".exe")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 3.0.0
3
+ Version: 3.0.2
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "3.0.0"
7
+ version = "3.0.2"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -8,8 +8,19 @@
8
8
  "schema": {"const": "lean-runtime.inspect/v1"},
9
9
  "ok": {"type": "boolean"},
10
10
  "data": {
11
+ "oneOf": [
12
+ {"$ref": "#/$defs/inspectionData"},
13
+ {"$ref": "#/$defs/routingData"},
14
+ {"type": "object", "maxProperties": 0}
15
+ ]
16
+ },
17
+ "warnings": {"$ref": "https://lean-runtime.dev/schemas/execution-v1.schema.json#/$defs/issues"},
18
+ "errors": {"$ref": "https://lean-runtime.dev/schemas/execution-v1.schema.json#/$defs/issues"}
19
+ },
20
+ "$defs": {
21
+ "inspectionData": {
11
22
  "type": "object",
12
- "anyOf": [{"required": ["subject", "subject_kind", "lock_id", "environment", "package_locks", "decisions"]}, {"maxProperties": 0}],
23
+ "required": ["subject", "subject_kind", "lock_id", "environment", "package_locks", "decisions"],
13
24
  "additionalProperties": false,
14
25
  "properties": {
15
26
  "subject": {"type": "string"},
@@ -20,10 +31,22 @@
20
31
  "decisions": {"type": "array", "items": {"$ref": "#/$defs/decision"}}
21
32
  }
22
33
  },
23
- "warnings": {"$ref": "https://lean-runtime.dev/schemas/execution-v1.schema.json#/$defs/issues"},
24
- "errors": {"$ref": "https://lean-runtime.dev/schemas/execution-v1.schema.json#/$defs/issues"}
25
- },
26
- "$defs": {
34
+ "routingData": {
35
+ "type": "object",
36
+ "required": ["decision", "context", "subject"],
37
+ "additionalProperties": false,
38
+ "properties": {
39
+ "decision": {"enum": ["context_selected", "automatic_discovery"]},
40
+ "context": {"type": "string"},
41
+ "subject": {
42
+ "oneOf": [
43
+ {"type": "string"},
44
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
45
+ ]
46
+ },
47
+ "plan": {"type": "object"}
48
+ }
49
+ },
27
50
  "environment": {
28
51
  "type": "object",
29
52
  "required": ["environment_id", "lock_id", "toolchain", "packages", "platform", "build_profile", "status", "created_at", "names"],
@@ -222,6 +222,8 @@ def test_per_candidate_timeout_is_enforced(sample_catalog) -> None: # type: ign
222
222
  ).discover_and_check("import Mathlib\n")
223
223
  assert result.status == "not_found"
224
224
  assert result.attempts[0].status == "timeout"
225
+ assert result.diagnostics[0].code == "CANDIDATE_TIMEOUT"
226
+ assert "time budget" in result.diagnostics[0].detail
225
227
  assert result.duration_seconds < 0.5
226
228
 
227
229
 
@@ -305,6 +307,17 @@ def test_lean_runtime_probe_passes_the_acquisition_budget_to_builds(sample_catal
305
307
 
306
308
  captured: dict[str, float] = {}
307
309
 
310
+ class Toolchains:
311
+ def ensure(
312
+ self,
313
+ toolchain: str,
314
+ *,
315
+ cancel: threading.Event | None = None,
316
+ ) -> str:
317
+ assert cancel is not None
318
+ captured["toolchain_ready"] = 1
319
+ return toolchain
320
+
308
321
  def open_exact( # type: ignore[no-untyped-def]
309
322
  lock, *, build_timeout, import_roots=(), cancel=None
310
323
  ):
@@ -312,11 +325,16 @@ def test_lean_runtime_probe_passes_the_acquisition_budget_to_builds(sample_catal
312
325
  captured["build_timeout"] = build_timeout
313
326
  return SimpleNamespace(id="env_fake")
314
327
 
315
- runtime = SimpleNamespace(open_exact=open_exact, availability="auto")
328
+ runtime = SimpleNamespace(
329
+ open_exact=open_exact,
330
+ availability="auto",
331
+ toolchains=Toolchains(),
332
+ )
316
333
  probe = LeanRuntimeProbe(runtime=runtime) # type: ignore[arg-type]
317
334
  candidate = Discovery(catalog=sample_catalog).plan("import Mathlib\n").candidates[0]
318
335
  acquired = probe.acquire(candidate, timeout_seconds=123.0, cancel=threading.Event())
319
336
  assert captured["build_timeout"] == 123.0
337
+ assert captured["toolchain_ready"] == 1
320
338
  assert acquired.environment_id == "env_fake"
321
339
 
322
340
 
@@ -657,6 +657,21 @@ def test_oci_blob_integrity_failure_retries_once_from_scratch(tmp_path: Path) ->
657
657
  thread.join(timeout=5)
658
658
 
659
659
 
660
+ def test_oci_blob_download_honors_preexisting_cancellation(tmp_path: Path) -> None:
661
+ cancel = threading.Event()
662
+ cancel.set()
663
+ store = EnvironmentStore(tmp_path / "consumer")
664
+ repository = OCIRepository.parse("oci+http://127.0.0.1:1/owner/cache")
665
+ with pytest.raises(EnvironmentError, match="cancelled"):
666
+ OCIRegistryClient(repository).download_blob(
667
+ {"digest": "sha256:" + "a" * 64, "size": 1},
668
+ store,
669
+ EventEmitter(),
670
+ cancel=cancel,
671
+ )
672
+ assert not list(store.oci_blobs.iterdir())
673
+
674
+
660
675
  def _publish_full_layout(monkeypatch: pytest.MonkeyPatch) -> None:
661
676
  """Let publisher transport tests skip capsule construction.
662
677
 
@@ -102,6 +102,29 @@ def test_require_rejects_missing_cache_configuration(tmp_path: Path) -> None:
102
102
  runtime.open_exact(_lock())
103
103
 
104
104
 
105
+ def test_retained_only_open_rejects_before_materialization(
106
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
107
+ ) -> None:
108
+ runtime = Runtime(
109
+ home=tmp_path,
110
+ availability="local",
111
+ libraries=[],
112
+ allow_source_build=False,
113
+ )
114
+ called = False
115
+
116
+ def materialize(*_args: object, **_kwargs: object) -> object:
117
+ nonlocal called
118
+ called = True
119
+ return object()
120
+
121
+ monkeypatch.setattr(runtime.environments, "ensure", materialize)
122
+ with pytest.raises(EnvironmentError, match="not retained locally"):
123
+ runtime.open_exact(_lock())
124
+ assert not called
125
+ assert not runtime.toolchains.elan_home.exists()
126
+
127
+
105
128
  def test_registry_redirect_does_not_forward_authorization_cross_host() -> None:
106
129
  request = urllib.request.Request("https://registry.example/v2/owner/cache/blobs/sha256:x")
107
130
  request.add_header("Authorization", "Bearer secret")
@@ -483,6 +483,31 @@ def test_run_subcommand_rejects_contradictory_availability(
483
483
  assert "--offline cannot be combined" in capsys.readouterr().err
484
484
 
485
485
 
486
+ def test_offline_run_disables_source_materialization(monkeypatch, tmp_path: Path) -> None:
487
+ source = tmp_path / "Main.lean"
488
+ source.write_text("example : True := by trivial\n")
489
+ monkeypatch.setattr("lean_runtime.run_cli.Runtime", FakeRuntime)
490
+ assert main([str(source), "--toolchain", "v4.32.2", "--offline", "--quiet"]) == 0
491
+ assert FakeRuntime.instance.kwargs["availability"] == "local"
492
+ assert FakeRuntime.instance.kwargs["libraries"] == ()
493
+ assert FakeRuntime.instance.kwargs["allow_source_build"] is False
494
+
495
+
496
+ def test_lean_run_interrupt_has_no_traceback(monkeypatch, tmp_path: Path, capsys) -> None:
497
+ source = tmp_path / "Main.lean"
498
+ source.write_text("example : True := by trivial\n")
499
+
500
+ class InterruptedRuntime:
501
+ def __init__(self, **_kwargs) -> None:
502
+ raise KeyboardInterrupt
503
+
504
+ monkeypatch.setattr("lean_runtime.run_cli.Runtime", InterruptedRuntime)
505
+ assert main([str(source), "--toolchain", "v4.32.2"]) == 130
506
+ captured = capsys.readouterr()
507
+ assert captured.out == ""
508
+ assert captured.err == "lean-run: interrupted\n"
509
+
510
+
486
511
  def test_run_subcommand_forwards_global_libraries(monkeypatch, tmp_path: Path) -> None:
487
512
  from lean_runtime.cli import main as lean_runtime_main
488
513