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.
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/CHANGELOG.md +13 -0
- {lean_runtime-3.0.2/lean_runtime.egg-info → lean_runtime-3.0.3}/PKG-INFO +1 -1
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/console.py +19 -3
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/toolchain_oci.py +32 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/SOURCES.txt +1 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/pyproject.toml +1 -1
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/announcement_gate.py +17 -4
- lean_runtime-3.0.3/tests/test_announcement_gate.py +43 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_bundles.py +94 -2
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_console.py +36 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_programs.py +50 -1
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_signatures.py +50 -0
- lean_runtime-3.0.3/tests/test_toolchain_oci.py +203 -0
- lean_runtime-3.0.2/tests/test_toolchain_oci.py +0 -86
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/LICENSE +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/MANIFEST.in +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/README.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/environments.toml +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/core-v4.32.2.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/README.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/mathlib-4.33.0.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/compatibility/mathlib-4.33.0.toml +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/architecture.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/captures.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/case-study-v1.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/cli.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/development.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/environments.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/getting-started.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/index.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/lake-artifact-cache-design.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/local-projects.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/portable-copies.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/project-publishing.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/python-api.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/ready-programs.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/standalone-files.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/trust-and-limitations.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/docs/v1-precision.md +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/examples/mathlib.toml +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/__init__.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/__main__.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/_git.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/_paths.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/backends.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/bundles.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/capsules.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/cli.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/comparison.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/decisions.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/__init__.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/analyzer.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/api.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/candidate.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/catalog.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/catalog_build.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/catalog_cli.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/catalog_manifest.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/data/catalog.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/defaults.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/engine.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/errors.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/module_inventory.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/planner.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/policy.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/probe.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/py.typed +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/result.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/schema_resources.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/environments.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/errors.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/events.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/facade.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/header_cache.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/health.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/identifier_resolver.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/lake.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/lake_cache.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/locking.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/matrix.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/models.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/oci.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/oci_protocol.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/packs.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/policies.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/profiling.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/programs.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/project_execution.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/project_sharing.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/projects.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/py.typed +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/references.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/resolver.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/run_cli.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/runtime.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/serialization.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/shared_projects.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/specs.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/store.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/timings.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/toolchain_slim.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/toolchains.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/verification.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime/wire.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/mkdocs.yml +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/attestation-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/check-batch-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/execution-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/publication-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/lake_cache_gate.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/registry_preflight.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/run_compatibility.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/setup.cfg +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/conftest.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/e2e/test_real_discovery.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_analyzer.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_catalog.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_catalog_build.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_discovery_schema_resources.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_engine.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_planner.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/discovery/test_policy.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_backend.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_capsules.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_cli.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_diagnostics.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_environment_integration.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_events_health.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_facade.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_frontmatter.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_git.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_header_cache.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_identifier_resolver.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_interactive.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_lake_cache.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_lockfiles.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_locking.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_models.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_multifile.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_oci_protocol.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_packs.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_paths.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_project_publish.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_projects.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_references.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_run_cli.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_runtime.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_schema_resources.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_schemas.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_specs.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_store.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_toolchain_slim.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_toolchains.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_v1_precision.py +0 -0
- {lean_runtime-3.0.2 → lean_runtime-3.0.3}/tests/test_verification_inventory.py +0 -0
- {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.
|
|
@@ -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"
|
|
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(
|
|
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
|
|
|
@@ -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
|
-
|
|
152
|
-
|
|
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,
|
|
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
|
-
|
|
161
|
-
assert
|
|
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.
|
|
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:
|