lean-runtime 3.0.2__tar.gz → 3.0.3__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 (190) hide show
  1. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/CHANGELOG.md +13 -0
  2. {lean_runtime-3.0.2/lean_runtime.egg-info → lean_runtime-3.0.3}/PKG-INFO +1 -1
  3. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/console.py +19 -3
  4. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/toolchain_oci.py +32 -0
  5. {lean_runtime-3.0.2 → lean_runtime-3.0.3/lean_runtime.egg-info}/PKG-INFO +1 -1
  6. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/SOURCES.txt +1 -0
  7. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/pyproject.toml +1 -1
  8. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/announcement_gate.py +17 -4
  9. lean_runtime-3.0.3/tests/test_announcement_gate.py +43 -0
  10. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_bundles.py +94 -2
  11. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_console.py +36 -0
  12. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_programs.py +50 -1
  13. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_signatures.py +50 -0
  14. lean_runtime-3.0.3/tests/test_toolchain_oci.py +203 -0
  15. lean_runtime-3.0.2/tests/test_toolchain_oci.py +0 -86
  16. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/LICENSE +0 -0
  17. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/MANIFEST.in +0 -0
  18. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/README.md +0 -0
  19. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/environments.toml +0 -0
  20. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/core-v4.32.2.lock.json +0 -0
  21. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
  22. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
  23. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
  24. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
  25. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  26. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  27. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  28. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
  29. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/README.md +0 -0
  30. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
  31. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/mathlib-4.32.2.json +0 -0
  32. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/mathlib-4.32.2.toml +0 -0
  33. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/mathlib-4.33.0.json +0 -0
  34. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/mathlib-4.33.0.toml +0 -0
  35. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/architecture.md +0 -0
  36. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/captures.md +0 -0
  37. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/case-study-v1.md +0 -0
  38. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/cli.md +0 -0
  39. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/development.md +0 -0
  40. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/environments.md +0 -0
  41. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/getting-started.md +0 -0
  42. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/index.md +0 -0
  43. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/lake-artifact-cache-design.md +0 -0
  44. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/local-projects.md +0 -0
  45. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/portable-copies.md +0 -0
  46. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/project-publishing.md +0 -0
  47. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/python-api.md +0 -0
  48. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/ready-programs.md +0 -0
  49. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/standalone-files.md +0 -0
  50. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/trust-and-limitations.md +0 -0
  51. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/v1-precision.md +0 -0
  52. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/examples/mathlib.toml +0 -0
  53. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/__init__.py +0 -0
  54. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/__main__.py +0 -0
  55. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/_git.py +0 -0
  56. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/_paths.py +0 -0
  57. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/backends.py +0 -0
  58. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/bundles.py +0 -0
  59. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/capsules.py +0 -0
  60. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/cli.py +0 -0
  61. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/comparison.py +0 -0
  62. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/decisions.py +0 -0
  63. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/diagnostics.py +0 -0
  64. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/__init__.py +0 -0
  65. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/analyzer.py +0 -0
  66. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/api.py +0 -0
  67. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/candidate.py +0 -0
  68. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/catalog.py +0 -0
  69. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/catalog_build.py +0 -0
  70. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/catalog_cli.py +0 -0
  71. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/catalog_manifest.py +0 -0
  72. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/data/catalog.json +0 -0
  73. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/defaults.py +0 -0
  74. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/engine.py +0 -0
  75. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/errors.py +0 -0
  76. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/module_inventory.py +0 -0
  77. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/planner.py +0 -0
  78. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/policy.py +0 -0
  79. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/probe.py +0 -0
  80. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/py.typed +0 -0
  81. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/result.py +0 -0
  82. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/schema_resources.py +0 -0
  83. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  84. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  85. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  86. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/environments.py +0 -0
  87. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/errors.py +0 -0
  88. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/events.py +0 -0
  89. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/facade.py +0 -0
  90. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/frontmatter.py +0 -0
  91. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/header_cache.py +0 -0
  92. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/health.py +0 -0
  93. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/identifier_resolver.py +0 -0
  94. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/lake.py +0 -0
  95. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/lake_cache.py +0 -0
  96. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/lockfiles.py +0 -0
  97. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/locking.py +0 -0
  98. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/matrix.py +0 -0
  99. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/models.py +0 -0
  100. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/oci.py +0 -0
  101. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/oci_protocol.py +0 -0
  102. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/packs.py +0 -0
  103. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/policies.py +0 -0
  104. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/profiling.py +0 -0
  105. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/programs.py +0 -0
  106. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/project_execution.py +0 -0
  107. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/project_sharing.py +0 -0
  108. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/projects.py +0 -0
  109. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/publisher_verification.py +0 -0
  110. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/py.typed +0 -0
  111. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/references.py +0 -0
  112. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/resolver.py +0 -0
  113. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/run_cli.py +0 -0
  114. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/runtime.py +0 -0
  115. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/schema_resources.py +0 -0
  116. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/serialization.py +0 -0
  117. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/shared_projects.py +0 -0
  118. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/specs.py +0 -0
  119. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/store.py +0 -0
  120. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/timings.py +0 -0
  121. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/toolchain_slim.py +0 -0
  122. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/toolchains.py +0 -0
  123. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/verification.py +0 -0
  124. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/wire.py +0 -0
  125. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/dependency_links.txt +0 -0
  126. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/entry_points.txt +0 -0
  127. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/requires.txt +0 -0
  128. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/top_level.txt +0 -0
  129. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/mkdocs.yml +0 -0
  130. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/attestation-v1.schema.json +0 -0
  131. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/check-batch-v1.schema.json +0 -0
  132. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/cleanup-v1.schema.json +0 -0
  133. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/comparison-v1.schema.json +0 -0
  134. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/execution-v1.schema.json +0 -0
  135. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/inspect-v1.schema.json +0 -0
  136. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/matrix-v1.schema.json +0 -0
  137. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/plan-v1.schema.json +0 -0
  138. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/profile-v1.schema.json +0 -0
  139. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/publication-v1.schema.json +0 -0
  140. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/verify-v1.schema.json +0 -0
  141. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/lake_cache_gate.py +0 -0
  142. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/registry_preflight.py +0 -0
  143. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/run_compatibility.py +0 -0
  144. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/run_v1_case_study.py +0 -0
  145. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/smoke_wheel.py +0 -0
  146. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/setup.cfg +0 -0
  147. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/conftest.py +0 -0
  148. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/e2e/test_real_discovery.py +0 -0
  149. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_analyzer.py +0 -0
  150. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_catalog.py +0 -0
  151. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_catalog_build.py +0 -0
  152. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_discovery_schema_resources.py +0 -0
  153. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_engine.py +0 -0
  154. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_planner.py +0 -0
  155. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_policy.py +0 -0
  156. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_backend.py +0 -0
  157. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_capsules.py +0 -0
  158. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_cli.py +0 -0
  159. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_diagnostics.py +0 -0
  160. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_environment_integration.py +0 -0
  161. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_events_health.py +0 -0
  162. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_facade.py +0 -0
  163. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_frontmatter.py +0 -0
  164. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_git.py +0 -0
  165. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_header_cache.py +0 -0
  166. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_identifier_resolver.py +0 -0
  167. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_interactive.py +0 -0
  168. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_lake_cache.py +0 -0
  169. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_lockfiles.py +0 -0
  170. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_locking.py +0 -0
  171. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_models.py +0 -0
  172. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_multifile.py +0 -0
  173. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_oci_protocol.py +0 -0
  174. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_packs.py +0 -0
  175. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_paths.py +0 -0
  176. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_prebuilt_policy.py +0 -0
  177. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_project_publish.py +0 -0
  178. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_projects.py +0 -0
  179. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_references.py +0 -0
  180. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_run_cli.py +0 -0
  181. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_runtime.py +0 -0
  182. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_schema_resources.py +0 -0
  183. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_schemas.py +0 -0
  184. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_specs.py +0 -0
  185. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_store.py +0 -0
  186. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_toolchain_slim.py +0 -0
  187. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_toolchains.py +0 -0
  188. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_v1_precision.py +0 -0
  189. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_verification_inventory.py +0 -0
  190. {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_verification_probe.py +0 -0
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.0.3 - 2026-08-18
6
+
7
+ - Give sparse-environment and slim-toolchain downloads independent progress
8
+ lifecycles, so the toolchain transfer reports its real size instead of
9
+ inheriting a completed environment total.
10
+ - Reject duplicate members and symlink extraction destinations in
11
+ slim-toolchain archives, matching the portable-environment boundary.
12
+ - Correct the announcement gate's versioned plan-envelope parsing and add the
13
+ missing macOS Intel clean-consumer job.
14
+ - Expand release acceptance coverage for archive limits and traversal,
15
+ deterministic multi-platform finalization, duplicate platforms, supported
16
+ Cosign releases, missing Cosign, and publisher identity/issuer mismatches.
17
+
5
18
  ## 3.0.2 - 2026-08-17
6
19
 
7
20
  - Route the first check directly through a newly downloaded slim Lean runtime.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -129,6 +129,16 @@ class ConsoleRenderer:
129
129
  cached = event.data.get("cached_bytes")
130
130
  if not isinstance(download, int) or download <= 0:
131
131
  return
132
+ self._start_download("Downloading environment", download, cached)
133
+
134
+ def _render_toolchain_download_planned(self, event: RuntimeEvent) -> None:
135
+ download = event.data.get("download_bytes")
136
+ cached = event.data.get("cached_bytes")
137
+ if not isinstance(download, int) or download <= 0:
138
+ return
139
+ self._start_download("Downloading Lean toolchain", download, cached)
140
+
141
+ def _start_download(self, label: str, download: int, cached: object) -> None:
132
142
  self._download_total = download
133
143
  self._layer_bytes.clear()
134
144
  self._next_checkpoint = _PLAIN_CHECKPOINT_PERCENT
@@ -139,11 +149,11 @@ class ConsoleRenderer:
139
149
  if isinstance(cached, int) and cached > 0
140
150
  else ""
141
151
  )
142
- self._print(f"Downloading environment: {format_byte_size(download)}{suffix}")
152
+ self._print(f"{label}: {format_byte_size(download)}{suffix}")
143
153
 
144
154
  def _render_library_layer_progress(self, event: RuntimeEvent) -> None:
145
155
  digest = str(event.data.get("digest", ""))
146
- if event.current_bytes is None or self._download_total is None:
156
+ if event.current_bytes is None or self._download_total is None or self._download_finished:
147
157
  return
148
158
  frame_current = event.data.get("frame_current")
149
159
  frame_total = event.data.get("frame_total")
@@ -181,13 +191,19 @@ class ConsoleRenderer:
181
191
  self._print("Retrying download after integrity verification failed")
182
192
 
183
193
  def _render_library_verified(self, event: RuntimeEvent) -> None:
194
+ self._finish_download("Downloaded and verified environment")
195
+
196
+ def _render_toolchain_ready(self, event: RuntimeEvent) -> None:
197
+ self._finish_download("Downloaded and verified Lean toolchain")
198
+
199
+ def _finish_download(self, message: str) -> None:
184
200
  if self._download_total is None or self._download_finished:
185
201
  return
186
202
  self._download_finished = True
187
203
  if self.mode == "tty":
188
204
  self._draw_bar(self._download_total, self._download_total)
189
205
  self._end_progress_line()
190
- self._print(self.style.green("Downloaded and verified environment"))
206
+ self._print(self.style.green(message))
191
207
 
192
208
  def _render_source_fetch_started(self, event: RuntimeEvent) -> None:
193
209
  package = event.data.get("package", "sources")
@@ -44,6 +44,7 @@ from .oci_protocol import (
44
44
  from .oci_protocol import (
45
45
  platform_matches as _platform_matches,
46
46
  )
47
+ from .policies import format_byte_size
47
48
  from .serialization import canonical_json_bytes
48
49
  from .store import EnvironmentStore, platform_compatibility
49
50
  from .toolchain_slim import SlimManifest, materialize, verify_capabilities
@@ -120,6 +121,9 @@ def _safe_member(name: str) -> PurePosixPath:
120
121
 
121
122
  def _extract_layer(layer: Path, destination: Path) -> None:
122
123
  total = count = 0
124
+ seen: set[PurePosixPath] = set()
125
+ if destination.is_symlink():
126
+ raise ToolchainError("check toolchain extraction destination must not be a symlink")
123
127
  try:
124
128
  with (
125
129
  layer.open("rb") as raw,
@@ -132,6 +136,9 @@ def _extract_layer(layer: Path, destination: Path) -> None:
132
136
  if count > MAX_TOOLCHAIN_FILES or total > MAX_TOOLCHAIN_BYTES:
133
137
  raise ToolchainError("check toolchain exceeds extraction limits")
134
138
  relative = _safe_member(member.name)
139
+ if relative in seen:
140
+ raise ToolchainError(f"duplicate check toolchain member: {member.name!r}")
141
+ seen.add(relative)
135
142
  target = destination.joinpath(*relative.parts)
136
143
  for parent in target.parents:
137
144
  if parent == destination:
@@ -147,6 +154,8 @@ def _extract_layer(layer: Path, destination: Path) -> None:
147
154
  target.chmod(member.mode & 0o777)
148
155
  elif member.isfile():
149
156
  target.parent.mkdir(parents=True, exist_ok=True)
157
+ if target.exists() or target.is_symlink():
158
+ raise ToolchainError(f"duplicate check toolchain member: {member.name!r}")
150
159
  source = archive.extractfile(member)
151
160
  if source is None:
152
161
  raise ToolchainError("check toolchain file has no payload")
@@ -166,6 +175,8 @@ def _extract_layer(layer: Path, destination: Path) -> None:
166
175
  ):
167
176
  raise ToolchainError("check toolchain contains an unsafe symlink")
168
177
  target.parent.mkdir(parents=True, exist_ok=True)
178
+ if target.exists() or target.is_symlink():
179
+ raise ToolchainError(f"duplicate check toolchain member: {member.name!r}")
169
180
  target.symlink_to(linkname)
170
181
  else:
171
182
  raise ToolchainError(
@@ -325,6 +336,15 @@ class OCIToolchainLibrary:
325
336
  if callable(local_check) and local_check(toolchain):
326
337
  return True
327
338
  plan = self.plan(toolchain)
339
+ if plan.download_bytes:
340
+ self.events.emit(
341
+ "toolchain.download_planned",
342
+ f"Check toolchain download planned: {format_byte_size(plan.download_bytes)}",
343
+ phase="acquisition",
344
+ toolchain=plan.toolchain,
345
+ download_bytes=plan.download_bytes,
346
+ cached_bytes=plan.cached_bytes,
347
+ )
328
348
  self.client.cache_verified_blob(plan.config_data, plan.config_descriptor, self.store)
329
349
  try:
330
350
  layer = self.client.download_blob(
@@ -346,6 +366,12 @@ class OCIToolchainLibrary:
346
366
  cancel=cancel,
347
367
  ):
348
368
  if self.toolchains.is_available_locally(toolchain):
369
+ self.events.emit(
370
+ "toolchain.ready",
371
+ "Lean check toolchain is ready",
372
+ phase="acquisition",
373
+ toolchain=plan.toolchain,
374
+ )
349
375
  return True
350
376
  staging = destination.parent / f".{destination.name}.staging-{os.getpid()}"
351
377
  if staging.exists():
@@ -381,6 +407,12 @@ class OCIToolchainLibrary:
381
407
  finally:
382
408
  if staging.exists():
383
409
  shutil.rmtree(staging)
410
+ self.events.emit(
411
+ "toolchain.ready",
412
+ "Lean check toolchain is ready",
413
+ phase="acquisition",
414
+ toolchain=plan.toolchain,
415
+ )
384
416
  return True
385
417
 
386
418
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -136,6 +136,7 @@ scripts/run_compatibility.py
136
136
  scripts/run_v1_case_study.py
137
137
  scripts/smoke_wheel.py
138
138
  tests/conftest.py
139
+ tests/test_announcement_gate.py
139
140
  tests/test_backend.py
140
141
  tests/test_bundles.py
141
142
  tests/test_capsules.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "3.0.2"
7
+ version = "3.0.3"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -43,6 +43,20 @@ FULL_INCREMENTAL_LIMIT = 2 * 1024**3
43
43
  HOME_LIMIT = 9 * 1024**3
44
44
 
45
45
 
46
+ def plan_data(value: object) -> dict[str, object]:
47
+ """Return data from a successful, versioned plan envelope."""
48
+ if not isinstance(value, dict):
49
+ return {}
50
+ data = value.get("data")
51
+ if (
52
+ value.get("schema") != "lean-runtime.plan/v1"
53
+ or value.get("ok") is not True
54
+ or not isinstance(data, dict)
55
+ ):
56
+ return {}
57
+ return data
58
+
59
+
46
60
  def run(
47
61
  command: list[str],
48
62
  *,
@@ -148,9 +162,8 @@ def main() -> int:
148
162
  value = json.loads(output)
149
163
  except json.JSONDecodeError:
150
164
  value = {}
151
- ok = code == 0 and isinstance(value, dict)
152
- if ok:
153
- ok = bool(value.get("download_bytes_complete"))
165
+ data = plan_data(value)
166
+ ok = code == 0 and data.get("download_bytes_complete") is True
154
167
  steps.append(
155
168
  {
156
169
  "name": name,
@@ -164,7 +177,7 @@ def main() -> int:
164
177
  }
165
178
  )
166
179
  print(f"{'ok ' if ok else 'FAIL'} {name} ({elapsed:.1f}s)")
167
- return ok, value
180
+ return ok, data
168
181
 
169
182
  ok, narrow_plan = plan("narrow-plan", narrow_lean)
170
183
  narrow_environment = narrow_plan.get("environment_download_bytes")
@@ -0,0 +1,43 @@
1
+ from collections.abc import Callable
2
+ from pathlib import Path
3
+ from runpy import run_path
4
+ from typing import cast
5
+
6
+ plan_data = cast(
7
+ Callable[[object], dict[str, object]],
8
+ run_path(str(Path(__file__).parents[1] / "scripts" / "announcement_gate.py"))["plan_data"],
9
+ )
10
+
11
+
12
+ def test_announcement_gate_reads_budget_fields_from_plan_envelope() -> None:
13
+ assert plan_data(
14
+ {
15
+ "schema": "lean-runtime.plan/v1",
16
+ "ok": True,
17
+ "errors": [],
18
+ "warnings": [],
19
+ "data": {
20
+ "download_bytes": 200,
21
+ "environment_download_bytes": 100,
22
+ "download_bytes_complete": True,
23
+ },
24
+ }
25
+ ) == {
26
+ "download_bytes": 200,
27
+ "environment_download_bytes": 100,
28
+ "download_bytes_complete": True,
29
+ }
30
+
31
+
32
+ def test_announcement_gate_rejects_failed_or_unversioned_plan_envelopes() -> None:
33
+ assert plan_data({"ok": True, "data": {"download_bytes_complete": True}}) == {}
34
+ assert (
35
+ plan_data(
36
+ {
37
+ "schema": "lean-runtime.plan/v1",
38
+ "ok": False,
39
+ "data": {"download_bytes_complete": True},
40
+ }
41
+ )
42
+ == {}
43
+ )
@@ -157,11 +157,15 @@ def _layer(*members: tuple[str, str, bytes | str]) -> bytes:
157
157
  assert isinstance(value, bytes)
158
158
  info.size = len(value)
159
159
  archive.addfile(info, io.BytesIO(value))
160
- else:
161
- assert kind == "symlink" and isinstance(value, str)
160
+ elif kind == "symlink":
161
+ assert isinstance(value, str)
162
162
  info.type = tarfile.SYMTYPE
163
163
  info.linkname = value
164
164
  archive.addfile(info)
165
+ else:
166
+ assert kind == "fifo"
167
+ info.type = tarfile.FIFOTYPE
168
+ archive.addfile(info)
165
169
  return output.getvalue()
166
170
 
167
171
 
@@ -198,6 +202,42 @@ def test_layer_extract_rejects_later_write_through_symlink(tmp_path: Path) -> No
198
202
  _extract_layer(layer, tmp_path / "output")
199
203
 
200
204
 
205
+ @pytest.mark.parametrize("name", ["/absolute", "../escape", "dir/../../escape", "dir\\escape"])
206
+ def test_layer_extract_rejects_unsafe_paths(tmp_path: Path, name: str) -> None:
207
+ with pytest.raises(EnvironmentError, match="unsafe bundle member path"):
208
+ _extract_layer(_layer(("file", name, b"payload")), tmp_path / "output")
209
+
210
+
211
+ def test_layer_extract_rejects_duplicate_member(tmp_path: Path) -> None:
212
+ layer = _layer(
213
+ ("file", "payload", b"first"),
214
+ ("file", "payload", b"replacement"),
215
+ )
216
+ with pytest.raises(EnvironmentError, match="duplicate bundle member"):
217
+ _extract_layer(layer, tmp_path / "output")
218
+
219
+
220
+ def test_layer_extract_rejects_special_member(tmp_path: Path) -> None:
221
+ with pytest.raises(EnvironmentError, match="unsupported bundle member"):
222
+ _extract_layer(_layer(("fifo", "pipe", b"")), tmp_path / "output")
223
+
224
+
225
+ def test_layer_extract_rejects_limits(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
226
+ monkeypatch.setattr("lean_runtime.bundles.MAX_BUNDLE_BYTES", 1)
227
+ with pytest.raises(EnvironmentError, match="exceeds extraction limits"):
228
+ _extract_layer(_layer(("file", "large", b"ab")), tmp_path / "output")
229
+
230
+
231
+ @pytest.mark.skipif(os.name == "nt", reason="Windows runners may not permit symlink creation")
232
+ def test_layer_extract_rejects_symlink_destination(tmp_path: Path) -> None:
233
+ outside = tmp_path / "outside"
234
+ outside.mkdir()
235
+ destination = tmp_path / "destination"
236
+ destination.symlink_to(outside, target_is_directory=True)
237
+ with pytest.raises(EnvironmentError, match="destination must not be a symlink"):
238
+ _extract_layer(_layer(("file", "payload", b"payload")), destination)
239
+
240
+
201
241
  def test_bundle_export_is_deterministic_and_imports_into_fresh_store(tmp_path: Path) -> None:
202
242
  producer, environment_id, lock = _published_runtime(tmp_path / "producer")
203
243
  first = tmp_path / "first.oci.tar.gz"
@@ -1098,6 +1138,58 @@ def test_oci_publisher_reports_partial_state_when_tag_finalization_fails(
1098
1138
  assert not any(event.kind == "library.published" for event in events)
1099
1139
 
1100
1140
 
1141
+ def test_environment_index_finalization_is_ordered_and_rejects_duplicate_platforms(
1142
+ tmp_path: Path,
1143
+ ) -> None:
1144
+ producer, _environment_id, lock = _published_runtime(tmp_path / "producer")
1145
+ publisher = OCIEnvironmentPublisher(
1146
+ OCIRepository.parse("oci://registry.example/owner/cache"),
1147
+ producer.store,
1148
+ producer.bundles,
1149
+ EventEmitter(),
1150
+ )
1151
+ published: list[tuple[str, bytes]] = []
1152
+
1153
+ class IndexClient:
1154
+ def check_push_access(self) -> None:
1155
+ pass
1156
+
1157
+ def manifest_exists(self, _digest: str) -> bool:
1158
+ return True
1159
+
1160
+ def publish_manifest(self, reference: str, data: bytes, _media_type: str) -> str:
1161
+ published.append((reference, data))
1162
+ return "sha256:" + hashlib.sha256(data).hexdigest()
1163
+
1164
+ publisher.client = IndexClient() # type: ignore[assignment]
1165
+
1166
+ def descriptor(system: str, architecture: str, abi: str, digest: str) -> dict[str, object]:
1167
+ return {
1168
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
1169
+ "digest": "sha256:" + digest * 64,
1170
+ "size": 42,
1171
+ "platform": {"os": system, "architecture": architecture},
1172
+ "annotations": {"org.lean-runtime.platform.abi": abi},
1173
+ }
1174
+
1175
+ linux = descriptor("linux", "amd64", "glibc-2.35", "a")
1176
+ macos = descriptor("darwin", "arm64", "darwin-arm64", "b")
1177
+ publication_id = publisher.publish_index(lock.lock_id, [linux, macos], tags=("dogfood",))
1178
+
1179
+ assert publication_id.startswith("sha256:")
1180
+ assert [reference for reference, _data in published] == [
1181
+ "capsule-" + lock.lock_id,
1182
+ "dogfood",
1183
+ ]
1184
+ assert published[0][1] == published[1][1]
1185
+ index = json.loads(published[0][1])
1186
+ assert [item["platform"]["os"] for item in index["manifests"]] == ["darwin", "linux"]
1187
+
1188
+ duplicate = descriptor("linux", "amd64", "glibc-2.35", "c")
1189
+ with pytest.raises(ValueError, match="duplicate OCI platform result"):
1190
+ publisher.publish_index(lock.lock_id, [linux, duplicate])
1191
+
1192
+
1101
1193
  def test_oci_truncated_blob_download_resumes_with_range(tmp_path: Path) -> None:
1102
1194
  data = b"large layer contents " * 512
1103
1195
  cut = len(data) // 3
@@ -112,6 +112,42 @@ def test_plan_with_nothing_to_download_is_silent() -> None:
112
112
  assert stream.getvalue() == ""
113
113
 
114
114
 
115
+ def test_toolchain_download_starts_a_new_progress_lifecycle() -> None:
116
+ renderer, stream = _renderer("plain")
117
+ renderer(_event("acquisition.planned", download_bytes=100, cached_bytes=0))
118
+ renderer(_event("library.layer_progress", digest="sha256:environment", current_bytes=100))
119
+ renderer(_event("library.verified", "done"))
120
+
121
+ # Progress emitted between completed acquisitions must not be folded into
122
+ # the environment total. The toolchain plan establishes its own aggregate.
123
+ renderer(_event("library.layer_progress", digest="sha256:late", current_bytes=10_000))
124
+ renderer(
125
+ _event(
126
+ "toolchain.download_planned",
127
+ download_bytes=1000,
128
+ cached_bytes=25,
129
+ )
130
+ )
131
+ renderer(_event("library.layer_progress", digest="sha256:toolchain", current_bytes=500))
132
+ renderer(_event("library.layer_progress", digest="sha256:toolchain", current_bytes=1000))
133
+ renderer(_event("toolchain.ready", "done"))
134
+
135
+ assert stream.getvalue().splitlines() == [
136
+ "Downloading environment: 100 B",
137
+ "Downloaded 25%",
138
+ "Downloaded 50%",
139
+ "Downloaded 75%",
140
+ "Downloaded 100%",
141
+ "Downloaded and verified environment",
142
+ "Downloading Lean toolchain: 1000 B (25 B already cached)",
143
+ "Downloaded 25%",
144
+ "Downloaded 50%",
145
+ "Downloaded 75%",
146
+ "Downloaded 100%",
147
+ "Downloaded and verified Lean toolchain",
148
+ ]
149
+
150
+
115
151
  def test_tty_mode_redraws_one_line_and_finishes_it() -> None:
116
152
  time = [0.0]
117
153
  renderer, stream = _renderer("tty", clock=lambda: time[0])
@@ -1,13 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import hashlib
4
+ import json
4
5
  import os
5
6
  from pathlib import Path
6
7
 
7
8
  import pytest
8
9
 
9
10
  from lean_runtime import EnvironmentError, ExecutionPolicy, Runtime
10
- from lean_runtime.programs import LEGACY_PROGRAM_SCHEMA, ProgramDescription
11
+ from lean_runtime.oci import OCIRepository
12
+ from lean_runtime.programs import LEGACY_PROGRAM_SCHEMA, ProgramDescription, ProgramLibrary
11
13
 
12
14
 
13
15
  def _revision() -> str:
@@ -111,6 +113,53 @@ def test_program_oci_round_trip_is_deterministic(tmp_path: Path) -> None:
111
113
  assert imported.copy_id == first_info.copy_id
112
114
 
113
115
 
116
+ def test_program_index_finalization_is_ordered_and_rejects_duplicate_platforms() -> None:
117
+ library = ProgramLibrary(
118
+ OCIRepository.parse("oci://registry.example/owner/programs"),
119
+ None, # type: ignore[arg-type]
120
+ None, # type: ignore[arg-type]
121
+ None, # type: ignore[arg-type]
122
+ )
123
+ published: list[tuple[str, bytes]] = []
124
+
125
+ class IndexClient:
126
+ def manifest_exists(self, _digest: str) -> bool:
127
+ return True
128
+
129
+ def publish_manifest(self, reference: str, data: bytes, _media_type: str) -> str:
130
+ published.append((reference, data))
131
+ return "sha256:" + hashlib.sha256(data).hexdigest()
132
+
133
+ library.client = IndexClient() # type: ignore[assignment]
134
+
135
+ def descriptor(system: str, architecture: str, abi: str, digest: str) -> dict[str, object]:
136
+ return {
137
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
138
+ "digest": "sha256:" + digest * 64,
139
+ "size": 42,
140
+ "platform": {"os": system, "architecture": architecture},
141
+ "annotations": {
142
+ "org.lean-runtime.artifact.kind": "execution-program",
143
+ "org.lean-runtime.platform.abi": abi,
144
+ },
145
+ }
146
+
147
+ linux = descriptor("linux", "amd64", "glibc-2.35", "a")
148
+ macos = descriptor("darwin", "arm64", "darwin-arm64", "b")
149
+ revision = "c" * 40
150
+ publication_id = library.publish_index(revision, [linux, macos], tags=("dogfood",))
151
+
152
+ assert publication_id.startswith("sha256:")
153
+ assert [reference for reference, _data in published] == [revision, "dogfood"]
154
+ assert published[0][1] == published[1][1]
155
+ index = json.loads(published[0][1])
156
+ assert [item["platform"]["os"] for item in index["manifests"]] == ["darwin", "linux"]
157
+
158
+ duplicate = descriptor("linux", "amd64", "glibc-2.35", "d")
159
+ with pytest.raises(ValueError, match="duplicate program platform"):
160
+ library.publish_index(revision, [linux, duplicate])
161
+
162
+
114
163
  def test_program_rejects_command_outside_payload(tmp_path: Path) -> None:
115
164
  runtime = Runtime(home=tmp_path / "runtime")
116
165
  with pytest.raises(EnvironmentError, match="absent"):
@@ -51,6 +51,56 @@ def test_vulnerable_cosign_release_is_rejected(
51
51
  CosignVerifier("identity", "issuer", executable=executable)
52
52
 
53
53
 
54
+ @pytest.mark.parametrize("version", ["v2.6.2", "v2.99.0", "v3.0.4", "v3.9.0"])
55
+ def test_supported_cosign_releases_are_accepted(
56
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, version: str
57
+ ) -> None:
58
+ executable = tmp_path / "verification_tool"
59
+ executable.write_text("")
60
+ monkeypatch.setattr(
61
+ "lean_runtime.publisher_verification.subprocess.run",
62
+ lambda *_args, **_kwargs: SimpleNamespace(
63
+ returncode=0, stdout=f'{{"gitVersion":"{version}"}}', stderr=""
64
+ ),
65
+ )
66
+ CosignVerifier("identity", "issuer", executable=executable)
67
+
68
+
69
+ def test_missing_cosign_is_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
70
+ monkeypatch.setattr("lean_runtime.publisher_verification.shutil.which", lambda _name: None)
71
+ with pytest.raises(EnvironmentError, match="required.*not installed"):
72
+ CosignVerifier("identity", "issuer", executable="definitely-missing-cosign")
73
+
74
+
75
+ def test_publisher_identity_and_issuer_must_be_configured_together() -> None:
76
+ with pytest.raises(ValueError, match="both identity and OIDC issuer"):
77
+ CosignVerifier("identity", None)
78
+ with pytest.raises(ValueError, match="both identity and OIDC issuer"):
79
+ CosignVerifier(None, "issuer")
80
+
81
+
82
+ def test_wrong_publisher_identity_or_issuer_is_an_integrity_failure(
83
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
84
+ ) -> None:
85
+ executable = tmp_path / "verification_tool"
86
+ executable.write_text("")
87
+
88
+ def run(command: list[str], **_kwargs: object) -> SimpleNamespace:
89
+ if command[1:3] == ["version", "--json"]:
90
+ return SimpleNamespace(returncode=0, stdout='{"gitVersion":"v3.0.4"}', stderr="")
91
+ assert command[1] == "verify"
92
+ return SimpleNamespace(
93
+ returncode=1,
94
+ stdout="",
95
+ stderr="no matching signatures: certificate identity or issuer mismatch",
96
+ )
97
+
98
+ monkeypatch.setattr("lean_runtime.publisher_verification.subprocess.run", run)
99
+ verifier = CosignVerifier("wrong-identity", "wrong-issuer", executable=executable)
100
+ with pytest.raises(EnvironmentError, match="signature verification failed.*identity or issuer"):
101
+ verifier.verify(OCIRepository.parse("oci://ghcr.io/owner/cache"), "sha256:" + "a" * 64)
102
+
103
+
54
104
  def test_cosign_attestation_binds_predicate_to_digest(
55
105
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
56
106
  ) -> None: