lean-lockstep 0.1.0__py3-none-any.whl

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 (71) hide show
  1. lean_lockstep-0.1.0.dist-info/METADATA +218 -0
  2. lean_lockstep-0.1.0.dist-info/RECORD +71 -0
  3. lean_lockstep-0.1.0.dist-info/WHEEL +5 -0
  4. lean_lockstep-0.1.0.dist-info/entry_points.txt +3 -0
  5. lean_lockstep-0.1.0.dist-info/licenses/LICENSE +22 -0
  6. lean_lockstep-0.1.0.dist-info/top_level.txt +1 -0
  7. lockstep/__init__.py +17 -0
  8. lockstep/__main__.py +3 -0
  9. lockstep/allowlist.py +40 -0
  10. lockstep/artifact_cache.py +515 -0
  11. lockstep/artifacts.py +185 -0
  12. lockstep/automation_schema.py +30 -0
  13. lockstep/build.py +468 -0
  14. lockstep/bundles.py +37 -0
  15. lockstep/cli.py +148 -0
  16. lockstep/commands/__init__.py +28 -0
  17. lockstep/commands/audit.py +46 -0
  18. lockstep/commands/automation.py +84 -0
  19. lockstep/commands/build.py +61 -0
  20. lockstep/commands/cache.py +58 -0
  21. lockstep/commands/check.py +81 -0
  22. lockstep/commands/claims.py +62 -0
  23. lockstep/commands/close.py +193 -0
  24. lockstep/commands/doctor.py +86 -0
  25. lockstep/commands/extract.py +169 -0
  26. lockstep/commands/falsify.py +108 -0
  27. lockstep/commands/index.py +16 -0
  28. lockstep/commands/jobs.py +188 -0
  29. lockstep/commands/layers.py +52 -0
  30. lockstep/commands/library.py +67 -0
  31. lockstep/commands/lock.py +58 -0
  32. lockstep/commands/log.py +132 -0
  33. lockstep/commands/node.py +429 -0
  34. lockstep/commands/note.py +76 -0
  35. lockstep/commands/probe.py +11 -0
  36. lockstep/commands/proof.py +31 -0
  37. lockstep/commands/ready.py +48 -0
  38. lockstep/commands/rules.py +61 -0
  39. lockstep/commands/search.py +37 -0
  40. lockstep/commands/status.py +172 -0
  41. lockstep/config.py +489 -0
  42. lockstep/context.py +33 -0
  43. lockstep/data/AuditCandidate.lean +18 -0
  44. lockstep/data/CacheArtifacts.lean +37 -0
  45. lockstep/data/Extract.lean.tmpl +362 -0
  46. lockstep/data/node.lean.tmpl +9 -0
  47. lockstep/delivery.py +45 -0
  48. lockstep/errors.py +28 -0
  49. lockstep/finalize.py +102 -0
  50. lockstep/gitutil.py +90 -0
  51. lockstep/hooks.py +143 -0
  52. lockstep/jobs.py +349 -0
  53. lockstep/layers.py +156 -0
  54. lockstep/lean.py +107 -0
  55. lockstep/library.py +188 -0
  56. lockstep/lock.py +68 -0
  57. lockstep/mcp_server.py +84 -0
  58. lockstep/metrics.py +12 -0
  59. lockstep/modules.py +197 -0
  60. lockstep/nodes.py +475 -0
  61. lockstep/operations.py +265 -0
  62. lockstep/output.py +45 -0
  63. lockstep/probes.py +72 -0
  64. lockstep/proofs.py +250 -0
  65. lockstep/resources.py +100 -0
  66. lockstep/shadow_index.py +47 -0
  67. lockstep/spec.py +36 -0
  68. lockstep/stage_runner.py +47 -0
  69. lockstep/storage.py +186 -0
  70. lockstep/store.py +338 -0
  71. lockstep/tags.py +84 -0
@@ -0,0 +1,515 @@
1
+ """Verified sharing of immutable Lean certificate build artifacts.
2
+
3
+ Lake owns the content-addressed blob format. This module adds the stronger
4
+ SHA-256 identity check and atomic adoption needed to deduplicate already-built
5
+ outputs without manufacturing Lake input-to-output mappings.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import asdict, dataclass
11
+ import hashlib
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ import signal
16
+ import stat
17
+ import subprocess
18
+ import time
19
+ from typing import Iterable
20
+
21
+ from lockstep import DATA
22
+ from lockstep.build import lean_compilers_running
23
+ from lockstep.config import Config, load_config
24
+ from lockstep.errors import CheckFailed, UsageError
25
+ from lockstep.modules import module_to_path, scan_modules
26
+ from lockstep.storage import StorageTarget, exclusive_storage_maintenance
27
+
28
+
29
+ FACET_DIRS = {
30
+ "olean": Path(".lake/build/lib/lean"),
31
+ "olean.server": Path(".lake/build/lib/lean"),
32
+ "olean.private": Path(".lake/build/lib/lean"),
33
+ "ilean": Path(".lake/build/lib/lean"),
34
+ "ir": Path(".lake/build/lib/lean"),
35
+ "c": Path(".lake/build/ir"),
36
+ "bc": Path(".lake/build/ir"),
37
+ }
38
+ DEFAULT_FACETS = tuple(FACET_DIRS)
39
+ SCHEMA = "lockstep-artifact-share-v1"
40
+ SUPPORTED_TOOLCHAINS = {
41
+ "leanprover/lean4:v4.29.1": "lean-v4.29.1-f72c35b",
42
+ }
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class FileIdentity:
47
+ device: int
48
+ inode: int
49
+ size: int
50
+ mtime_ns: int
51
+ ctime_ns: int
52
+ links: int
53
+ allocated_bytes: int
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class Candidate:
58
+ module: str
59
+ facet: str
60
+ relative_output: str
61
+ primary: Path
62
+ other: Path
63
+ size: int
64
+
65
+
66
+ def _identity(path: Path) -> FileIdentity:
67
+ info = path.lstat()
68
+ if not stat.S_ISREG(info.st_mode):
69
+ kind = "symlink" if stat.S_ISLNK(info.st_mode) else "non-regular file"
70
+ raise CheckFailed(f"refusing {kind}: {path}")
71
+ return FileIdentity(
72
+ info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns,
73
+ info.st_nlink, info.st_blocks * 512,
74
+ )
75
+
76
+
77
+ def _absolute_without_symlinks(path: Path) -> Path:
78
+ return Path(os.path.abspath(os.fspath(path.expanduser())))
79
+
80
+
81
+ def reject_symlink_ancestors(path: Path, boundary: Path | None = None) -> None:
82
+ """Reject an existing symlink in ``path`` or its parents down to ``boundary``.
83
+
84
+ ``Path.resolve`` is intentionally not used: resolving first would hide the
85
+ condition this check protects against.
86
+ """
87
+ path = _absolute_without_symlinks(path)
88
+ boundary = _absolute_without_symlinks(boundary or Path(path.anchor))
89
+ try:
90
+ relative = path.relative_to(boundary)
91
+ except ValueError as exc:
92
+ raise CheckFailed(f"path escapes its trusted boundary {boundary}: {path}") from exc
93
+ current = boundary
94
+ for part in ("", *relative.parts):
95
+ if part:
96
+ current = current / part
97
+ try:
98
+ info = current.lstat()
99
+ except FileNotFoundError:
100
+ continue
101
+ if stat.S_ISLNK(info.st_mode):
102
+ raise CheckFailed(f"refusing symlink or symlink ancestor: {current}")
103
+ if current != path and not stat.S_ISDIR(info.st_mode):
104
+ raise CheckFailed(f"refusing non-directory path ancestor: {current}")
105
+
106
+
107
+ def sha256_file(path: Path) -> str:
108
+ digest = hashlib.sha256()
109
+ with path.open("rb", buffering=0) as stream:
110
+ for block in iter(lambda: stream.read(8 * 1024 * 1024), b""):
111
+ digest.update(block)
112
+ return digest.hexdigest()
113
+
114
+
115
+ def _toolchain(root: Path) -> str:
116
+ path = root / "lean-toolchain"
117
+ if not path.is_file() or path.is_symlink():
118
+ raise CheckFailed(f"missing or unsafe lean-toolchain: {path}")
119
+ return path.read_text().strip()
120
+
121
+
122
+ def supported_toolchain(root: Path) -> tuple[str, str]:
123
+ toolchain = _toolchain(root)
124
+ try:
125
+ namespace = SUPPORTED_TOOLCHAINS[toolchain]
126
+ except KeyError as exc:
127
+ raise CheckFailed(f"unsupported Lean toolchain for artifact sharing: {toolchain}") from exc
128
+ return toolchain, namespace
129
+
130
+
131
+ def default_cache_root(cfg: Config) -> Path:
132
+ _, namespace = supported_toolchain(cfg.root)
133
+ return cfg.state_home / "lake-artifacts" / namespace
134
+
135
+
136
+ def validate_roots(cfg: Config, other_root: Path) -> Config:
137
+ primary = cfg.root.resolve()
138
+ other = other_root.resolve()
139
+ if primary == other:
140
+ raise UsageError("the other checkout must differ from the current checkout")
141
+ other_cfg = load_config(other)
142
+ other_cfg.root = other
143
+ if cfg.project_id != other_cfg.project_id:
144
+ raise CheckFailed(f"project mismatch: {cfg.project_id} != {other_cfg.project_id}")
145
+ if cfg.db_path.resolve() != other_cfg.db_path.resolve():
146
+ raise CheckFailed("checkouts must share the harness database so every writer honors the same storage reservations")
147
+ primary_toolchain, _ = supported_toolchain(primary)
148
+ other_toolchain, _ = supported_toolchain(other)
149
+ if primary_toolchain != other_toolchain:
150
+ raise CheckFailed("Lean toolchain mismatch between checkouts")
151
+ if primary.stat().st_dev != other.stat().st_dev:
152
+ raise CheckFailed("checkouts are on different filesystems; hard-link sharing is unavailable")
153
+ return other_cfg
154
+
155
+
156
+ def _output_path(root: Path, module: str, facet: str) -> Path:
157
+ base = (root / FACET_DIRS[facet] / module_to_path(module)).with_suffix("")
158
+ return Path(str(base) + "." + facet)
159
+
160
+
161
+ def certificate_candidates(
162
+ cfg: Config, other_cfg: Config, facets: Iterable[str] = DEFAULT_FACETS
163
+ ) -> tuple[list[Candidate], dict[str, int]]:
164
+ selected = tuple(dict.fromkeys(facets))
165
+ unknown = sorted(set(selected) - set(FACET_DIRS))
166
+ if unknown:
167
+ raise UsageError("unsupported compiled facet(s): " + ", ".join(unknown))
168
+ primary_modules = scan_modules(cfg)
169
+ other_modules = scan_modules(other_cfg)
170
+ layer = cfg.certificates_layer
171
+ other_layer = other_cfg.certificates_layer
172
+ generated = {
173
+ name for name, mod in primary_modules.items()
174
+ if mod.generated and mod.layer == layer
175
+ and name in other_modules and other_modules[name].generated
176
+ and other_modules[name].layer == other_layer
177
+ }
178
+ stats = {"generated_modules": len(generated), "missing": 0, "size_mismatch": 0, "already_shared": 0}
179
+ candidates: list[Candidate] = []
180
+ for module in sorted(generated):
181
+ for facet in selected:
182
+ primary = _output_path(cfg.root, module, facet)
183
+ other = _output_path(other_cfg.root, module, facet)
184
+ reject_symlink_ancestors(primary, cfg.root)
185
+ reject_symlink_ancestors(other, other_cfg.root)
186
+ if not primary.exists() or not other.exists():
187
+ stats["missing"] += 1
188
+ continue
189
+ p_id = _identity(primary)
190
+ o_id = _identity(other)
191
+ if p_id.size != o_id.size:
192
+ stats["size_mismatch"] += 1
193
+ continue
194
+ if (p_id.device, p_id.inode) == (o_id.device, o_id.inode):
195
+ stats["already_shared"] += 1
196
+ continue
197
+ rel = str(primary.relative_to(cfg.root))
198
+ candidates.append(Candidate(module, facet, rel, primary, other, p_id.size))
199
+ stats["candidates"] = len(candidates)
200
+ stats["candidate_bytes"] = sum(item.size for item in candidates)
201
+ return candidates, stats
202
+
203
+
204
+ class Receipt:
205
+ def __init__(self, path: Path, header: dict[str, object]):
206
+ self.path = path
207
+ path.parent.mkdir(parents=True, exist_ok=True)
208
+ if path.exists() and path.stat().st_size:
209
+ rows = self._repair_and_read()
210
+ if not rows:
211
+ self.append(header)
212
+ return
213
+ first = next((row for row in rows if row), "")
214
+ try:
215
+ previous = json.loads(first)
216
+ except json.JSONDecodeError as exc:
217
+ raise CheckFailed(f"invalid existing receipt {path}: {exc}") from exc
218
+ keys = ("schema", "primary", "other", "cache", "toolchain", "facets")
219
+ if any(previous.get(key) != header.get(key) for key in keys):
220
+ raise CheckFailed(f"receipt parameters do not match this invocation: {path}")
221
+ else:
222
+ self.append(header)
223
+
224
+ def _repair_and_read(self) -> list[str]:
225
+ """Drop only an invalid torn tail and make a valid unterminated tail append-safe."""
226
+ data = self.path.read_bytes()
227
+ lines = data.splitlines(keepends=True)
228
+ valid: list[str] = []
229
+ offset = 0
230
+ truncate_at: int | None = None
231
+ for index, raw in enumerate(lines):
232
+ text = raw.rstrip(b"\r\n").decode(errors="strict")
233
+ if not text:
234
+ offset += len(raw)
235
+ continue
236
+ try:
237
+ json.loads(text)
238
+ except json.JSONDecodeError as exc:
239
+ if index != len(lines) - 1:
240
+ raise CheckFailed(f"invalid receipt record in {self.path}: {exc}") from exc
241
+ truncate_at = offset
242
+ break
243
+ valid.append(text)
244
+ offset += len(raw)
245
+ if truncate_at is not None:
246
+ with self.path.open("r+b") as stream:
247
+ stream.truncate(truncate_at)
248
+ stream.flush()
249
+ os.fsync(stream.fileno())
250
+ data = data[:truncate_at]
251
+ if data and not data.endswith(b"\n"):
252
+ fd = os.open(self.path, os.O_WRONLY | os.O_APPEND)
253
+ try:
254
+ os.write(fd, b"\n")
255
+ os.fsync(fd)
256
+ finally:
257
+ os.close(fd)
258
+ return valid
259
+
260
+ def append(self, record: dict[str, object]) -> None:
261
+ encoded = json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n"
262
+ fd = os.open(self.path, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
263
+ try:
264
+ os.write(fd, encoded.encode())
265
+ os.fsync(fd)
266
+ finally:
267
+ os.close(fd)
268
+
269
+ def adopted(self) -> set[tuple[str, str]]:
270
+ out: set[tuple[str, str]] = set()
271
+ for line in self._repair_and_read()[1:]:
272
+ try:
273
+ row = json.loads(line)
274
+ except json.JSONDecodeError:
275
+ break # an interrupted final append is ignored
276
+ if row.get("event") == "adopted":
277
+ out.add((str(row["module"]), str(row["facet"])))
278
+ return out
279
+
280
+
281
+ def verify_candidates(candidates: Iterable[Candidate], receipt: Receipt) -> list[tuple[Candidate, str]]:
282
+ verified: list[tuple[Candidate, str]] = []
283
+ for item in candidates:
284
+ primary_before = _identity(item.primary)
285
+ other_before = _identity(item.other)
286
+ primary_sha = sha256_file(item.primary)
287
+ other_sha = sha256_file(item.other)
288
+ if _identity(item.primary) != primary_before or _identity(item.other) != other_before:
289
+ raise CheckFailed(f"artifact changed while hashing: {item.relative_output}")
290
+ if primary_sha != other_sha:
291
+ receipt.append({"event": "mismatch", "module": item.module, "facet": item.facet,
292
+ "bytes": item.size, "primary_sha256": primary_sha, "other_sha256": other_sha})
293
+ continue
294
+ receipt.append({"event": "verified", "module": item.module, "facet": item.facet,
295
+ "relative_output": item.relative_output, "bytes": item.size, "sha256": primary_sha,
296
+ "primary_identity": asdict(primary_before), "other_identity": asdict(other_before)})
297
+ verified.append((item, primary_sha))
298
+ return verified
299
+
300
+
301
+ def _driver_path() -> Path:
302
+ return DATA / "CacheArtifacts.lean"
303
+
304
+
305
+ def save_native_artifacts(
306
+ cfg: Config, cache_root: Path, verified: list[tuple[Candidate, str]], work: Path,
307
+ *, timeout_s: int = 21600,
308
+ ) -> dict[str, Path]:
309
+ manifest = work / "native-input.tsv"
310
+ output = work / "native-output.tsv"
311
+ work.mkdir(parents=True, exist_ok=True)
312
+ for item, _ in verified:
313
+ if any(char in str(item.primary) for char in ("\t", "\n", "\r")):
314
+ raise CheckFailed(f"unsupported control character in artifact path: {item.primary}")
315
+ manifest.write_text("".join(f"{item.primary}\t{item.facet}\n" for item, _ in verified))
316
+ argv = [cfg.lake, "env", "lean", "--run", str(_driver_path()), str(cache_root), str(manifest), str(output)]
317
+ try:
318
+ proc = subprocess.Popen(
319
+ argv, cwd=cfg.root, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
320
+ start_new_session=True,
321
+ )
322
+ except OSError as exc:
323
+ raise CheckFailed(f"could not start Lake artifact writer: {exc}") from exc
324
+
325
+ def stop_group() -> None:
326
+ try:
327
+ os.killpg(proc.pid, signal.SIGTERM)
328
+ except ProcessLookupError:
329
+ proc.wait()
330
+ return
331
+ deadline = time.monotonic() + 10
332
+ while time.monotonic() < deadline:
333
+ try:
334
+ os.killpg(proc.pid, 0)
335
+ except ProcessLookupError:
336
+ proc.wait()
337
+ return
338
+ time.sleep(0.05)
339
+ try:
340
+ os.killpg(proc.pid, signal.SIGKILL)
341
+ except ProcessLookupError:
342
+ pass
343
+ proc.wait()
344
+ # Do not return to the storage-reservation context while a descendant
345
+ # from the cache-writing process group can still run.
346
+ while True:
347
+ try:
348
+ os.killpg(proc.pid, 0)
349
+ except ProcessLookupError:
350
+ return
351
+ time.sleep(0.05)
352
+
353
+ try:
354
+ stdout, _ = proc.communicate(timeout=timeout_s)
355
+ except subprocess.TimeoutExpired as exc:
356
+ stop_group()
357
+ raise CheckFailed(f"Lake artifact writer exceeded {timeout_s}s and its process group was stopped") from exc
358
+ except BaseException:
359
+ stop_group()
360
+ raise
361
+ if proc.returncode:
362
+ raise CheckFailed(f"Lake artifact writer failed ({proc.returncode}):\n{stdout[-4000:]}")
363
+ saved: dict[str, Path] = {}
364
+ for line in output.read_text().splitlines():
365
+ parts = line.split("\t")
366
+ if len(parts) != 2:
367
+ raise CheckFailed(f"invalid native artifact writer output: {line}")
368
+ local, relative = parts
369
+ saved[local] = cache_root / "artifacts" / relative
370
+ if set(saved) != {str(item.primary) for item, _ in verified}:
371
+ raise CheckFailed("native artifact writer returned an incomplete or unexpected manifest")
372
+ return saved
373
+
374
+
375
+ def atomic_adopt(path: Path, blob: Path, expected_sha256: str) -> tuple[FileIdentity, FileIdentity]:
376
+ blob_mode = blob.lstat().st_mode
377
+ if blob_mode & 0o222:
378
+ os.chmod(blob, 0o444)
379
+ before = _identity(path)
380
+ blob_id = _identity(blob)
381
+ blob_content_id = (
382
+ blob_id.device, blob_id.inode, blob_id.size, blob_id.mtime_ns, blob_id.allocated_bytes
383
+ )
384
+ if before.device != blob_id.device:
385
+ raise CheckFailed(f"artifact and cache blob are on different filesystems: {path}")
386
+ if before.size != blob_id.size or sha256_file(blob) != expected_sha256 or _identity(blob) != blob_id:
387
+ raise CheckFailed(f"native cache hash collision or corrupt blob for {path}")
388
+ if sha256_file(path) != expected_sha256 or _identity(path) != before:
389
+ raise CheckFailed(f"artifact changed before adoption: {path}")
390
+ if (before.device, before.inode) == (blob_id.device, blob_id.inode):
391
+ return before, before
392
+ temp = path.with_name(f".{path.name}.lockstep-share-{os.getpid()}-{time.time_ns()}")
393
+ try:
394
+ try:
395
+ os.link(blob, temp, follow_symlinks=False)
396
+ temp_id = _identity(temp)
397
+ linked_blob = _identity(blob)
398
+ linked_content_id = (
399
+ linked_blob.device, linked_blob.inode, linked_blob.size,
400
+ linked_blob.mtime_ns, linked_blob.allocated_bytes,
401
+ )
402
+ if linked_content_id != blob_content_id or (temp_id.device, temp_id.inode) != (blob_id.device, blob_id.inode):
403
+ raise CheckFailed(f"native cache blob changed during adoption: {blob}")
404
+ if _identity(path) != before:
405
+ raise CheckFailed(f"artifact changed during adoption: {path}")
406
+ os.replace(temp, path)
407
+ directory_fd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
408
+ try:
409
+ os.fsync(directory_fd)
410
+ finally:
411
+ os.close(directory_fd)
412
+ after = _identity(path)
413
+ final_blob = _identity(blob)
414
+ final_content_id = (
415
+ final_blob.device, final_blob.inode, final_blob.size,
416
+ final_blob.mtime_ns, final_blob.allocated_bytes,
417
+ )
418
+ if final_content_id != blob_content_id or (after.device, after.inode) != (blob_id.device, blob_id.inode):
419
+ raise CheckFailed(f"atomic adoption did not install the native cache inode: {path}")
420
+ return before, after
421
+ except CheckFailed:
422
+ raise
423
+ except OSError as exc:
424
+ raise CheckFailed(f"atomic adoption failed for {path}: {exc}") from exc
425
+ finally:
426
+ try:
427
+ temp.unlink()
428
+ except FileNotFoundError:
429
+ pass
430
+
431
+
432
+ def share_certificates(
433
+ cfg: Config,
434
+ other_root: Path,
435
+ *,
436
+ cache_root: Path,
437
+ receipt_path: Path,
438
+ facets: Iterable[str] = DEFAULT_FACETS,
439
+ apply: bool = False,
440
+ ) -> dict[str, object]:
441
+ if not os.environ.get("LOCKSTEP_JOB_ID") or os.environ.get("LOCKSTEP_IN_GUARDED_STAGE") != "1":
442
+ raise UsageError("artifact sharing must run inside `lockstep job run` (guarded job identity is missing)")
443
+ other_cfg = validate_roots(cfg, other_root)
444
+ reject_symlink_ancestors(cache_root)
445
+ cache_root = _absolute_without_symlinks(cache_root)
446
+ if cache_root.exists() and not cache_root.is_dir():
447
+ raise CheckFailed(f"artifact cache path is not a directory: {cache_root}")
448
+ facets = tuple(dict.fromkeys(facets))
449
+ cache_probe = cache_root
450
+ while not cache_probe.exists() and cache_probe != cache_probe.parent:
451
+ cache_probe = cache_probe.parent
452
+ if cache_probe.stat().st_dev != cfg.root.stat().st_dev:
453
+ raise CheckFailed("artifact cache and checkouts must be on the same filesystem")
454
+ header = {
455
+ "schema": SCHEMA, "event": "start", "primary": str(cfg.root.resolve()),
456
+ "other": str(other_cfg.root.resolve()), "cache": str(cache_root),
457
+ "toolchain": supported_toolchain(cfg.root)[0], "facets": list(facets), "apply": apply,
458
+ }
459
+ owner_job_id = os.environ.get("LOCKSTEP_JOB_ID")
460
+ target = StorageTarget("artifact-cache", cache_root)
461
+ with exclusive_storage_maintenance(
462
+ cfg, roots=(cfg.root, other_cfg.root), targets=(target,),
463
+ reason="verified sharing of immutable certificate artifacts", owner_job_id=owner_job_id,
464
+ ):
465
+ receipt = Receipt(receipt_path.resolve(), header)
466
+ if lean_compilers_running():
467
+ raise CheckFailed("a Lean compiler is running outside this storage reservation")
468
+ candidates, stats = certificate_candidates(cfg, other_cfg, facets)
469
+ adopted_before = receipt.adopted()
470
+ verified = verify_candidates(candidates, receipt)
471
+ mismatch_count = len(candidates) - len(verified)
472
+ adopted_count = 0
473
+ deduplicated_logical_bytes = 0
474
+ eligible_freed_allocated_bytes = 0
475
+ free_before = os.statvfs(cache_probe).f_bavail * os.statvfs(cache_probe).f_frsize
476
+ if apply and verified:
477
+ cache_root.mkdir(parents=True, exist_ok=True)
478
+ reject_symlink_ancestors(cache_root)
479
+ work = receipt_path.resolve().parent / (receipt_path.name + ".work")
480
+ saved = save_native_artifacts(cfg, cache_root, verified, work)
481
+ for item, digest in verified:
482
+ blob = saved[str(item.primary)]
483
+ # A full SHA-256 comparison catches Lake's documented 64-bit-hash collision case.
484
+ if sha256_file(blob) != digest:
485
+ raise CheckFailed(f"native cache hash collision for {item.relative_output}")
486
+ os.chmod(blob, 0o444)
487
+ before_primary, after_primary = atomic_adopt(item.primary, blob, digest)
488
+ before_other, after_other = atomic_adopt(item.other, blob, digest)
489
+ receipt.append({
490
+ "event": "adopted", "module": item.module, "facet": item.facet,
491
+ "relative_output": item.relative_output, "bytes": item.size, "sha256": digest,
492
+ "lake_artifact": str(blob.relative_to(cache_root)),
493
+ "primary_before": asdict(before_primary), "primary_after": asdict(after_primary),
494
+ "other_before": asdict(before_other), "other_after": asdict(after_other),
495
+ })
496
+ adopted_count += 1
497
+ deduplicated_logical_bytes += item.size
498
+ for before, after in ((before_primary, after_primary), (before_other, after_other)):
499
+ if before.inode != after.inode and before.links == 1:
500
+ eligible_freed_allocated_bytes += before.allocated_bytes
501
+ free_after = os.statvfs(cache_probe).f_bavail * os.statvfs(cache_probe).f_frsize
502
+ result = {
503
+ "schema": SCHEMA, "mode": "apply" if apply else "dry-run", "receipt": str(receipt.path),
504
+ "cache": str(cache_root), "facets": list(facets), **stats,
505
+ "verified": len(verified), "mismatches": mismatch_count,
506
+ "adopted": adopted_count,
507
+ "deduplicated_logical_bytes": deduplicated_logical_bytes,
508
+ "eligible_freed_allocated_bytes": eligible_freed_allocated_bytes,
509
+ "observed_free_bytes_before": free_before,
510
+ "observed_free_bytes_after": free_after,
511
+ "observed_free_bytes_delta": free_after - free_before,
512
+ "resumed_adoptions": len(adopted_before),
513
+ }
514
+ receipt.append({"event": "complete", **result})
515
+ return result