lean-runtime 2.0.7__tar.gz → 2.0.8__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 (146) hide show
  1. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/CHANGELOG.md +7 -0
  2. {lean_runtime-2.0.7/lean_runtime.egg-info → lean_runtime-2.0.8}/PKG-INFO +1 -1
  3. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/ready-programs.md +6 -2
  4. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/cli.py +14 -0
  5. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/programs.py +45 -8
  6. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/runtime.py +2 -0
  7. {lean_runtime-2.0.7 → lean_runtime-2.0.8/lean_runtime.egg-info}/PKG-INFO +1 -1
  8. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/pyproject.toml +1 -1
  9. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_cli.py +39 -0
  10. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_programs.py +39 -0
  11. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/LICENSE +0 -0
  12. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/MANIFEST.in +0 -0
  13. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/README.md +0 -0
  14. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/catalog/environments.toml +0 -0
  15. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/catalog/locks/core-v4.32.2.lock.json +0 -0
  16. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  17. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  18. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  19. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/compatibility/README.md +0 -0
  20. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/compatibility/mathlib-4.32.2.json +0 -0
  21. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/compatibility/mathlib-4.32.2.toml +0 -0
  22. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/architecture.md +0 -0
  23. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/captures.md +0 -0
  24. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/case-study-v1.md +0 -0
  25. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/cli.md +0 -0
  26. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/development.md +0 -0
  27. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/environments.md +0 -0
  28. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/getting-started.md +0 -0
  29. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/index.md +0 -0
  30. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/local-projects.md +0 -0
  31. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/portable-copies.md +0 -0
  32. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/python-api.md +0 -0
  33. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/standalone-files.md +0 -0
  34. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/trust-and-limitations.md +0 -0
  35. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/docs/v1-precision.md +0 -0
  36. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/examples/mathlib.toml +0 -0
  37. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/__init__.py +0 -0
  38. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/__main__.py +0 -0
  39. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/_git.py +0 -0
  40. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/_paths.py +0 -0
  41. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/backends.py +0 -0
  42. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/bundles.py +0 -0
  43. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/comparison.py +0 -0
  44. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/decisions.py +0 -0
  45. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/diagnostics.py +0 -0
  46. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/__init__.py +0 -0
  47. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/analyzer.py +0 -0
  48. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/api.py +0 -0
  49. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/candidate.py +0 -0
  50. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/catalog.py +0 -0
  51. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/catalog_build.py +0 -0
  52. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/catalog_cli.py +0 -0
  53. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/catalog_manifest.py +0 -0
  54. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/data/catalog.json +0 -0
  55. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/defaults.py +0 -0
  56. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/engine.py +0 -0
  57. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/errors.py +0 -0
  58. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/module_inventory.py +0 -0
  59. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/planner.py +0 -0
  60. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/policy.py +0 -0
  61. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/probe.py +0 -0
  62. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/py.typed +0 -0
  63. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/result.py +0 -0
  64. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/schema_resources.py +0 -0
  65. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  66. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  67. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  68. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/environments.py +0 -0
  69. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/errors.py +0 -0
  70. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/events.py +0 -0
  71. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/facade.py +0 -0
  72. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/frontmatter.py +0 -0
  73. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/health.py +0 -0
  74. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/lake.py +0 -0
  75. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/lockfiles.py +0 -0
  76. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/locking.py +0 -0
  77. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/matrix.py +0 -0
  78. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/models.py +0 -0
  79. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/oci.py +0 -0
  80. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/policies.py +0 -0
  81. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/profiling.py +0 -0
  82. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/projects.py +0 -0
  83. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/publisher_verification.py +0 -0
  84. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/py.typed +0 -0
  85. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/references.py +0 -0
  86. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/resolver.py +0 -0
  87. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/run_cli.py +0 -0
  88. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/schema_resources.py +0 -0
  89. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/serialization.py +0 -0
  90. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/specs.py +0 -0
  91. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/store.py +0 -0
  92. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/timings.py +0 -0
  93. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/toolchains.py +0 -0
  94. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/verification.py +0 -0
  95. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime/wire.py +0 -0
  96. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime.egg-info/SOURCES.txt +0 -0
  97. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime.egg-info/dependency_links.txt +0 -0
  98. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime.egg-info/entry_points.txt +0 -0
  99. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime.egg-info/requires.txt +0 -0
  100. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/lean_runtime.egg-info/top_level.txt +0 -0
  101. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/mkdocs.yml +0 -0
  102. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/schemas/cleanup-v1.schema.json +0 -0
  103. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/schemas/comparison-v1.schema.json +0 -0
  104. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/schemas/execution-v1.schema.json +0 -0
  105. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/schemas/inspect-v1.schema.json +0 -0
  106. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/schemas/matrix-v1.schema.json +0 -0
  107. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/schemas/profile-v1.schema.json +0 -0
  108. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/schemas/verify-v1.schema.json +0 -0
  109. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/scripts/run_compatibility.py +0 -0
  110. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/scripts/run_v1_case_study.py +0 -0
  111. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/scripts/smoke_wheel.py +0 -0
  112. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/setup.cfg +0 -0
  113. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/conftest.py +0 -0
  114. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/discovery/e2e/test_real_discovery.py +0 -0
  115. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/discovery/test_analyzer.py +0 -0
  116. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/discovery/test_catalog.py +0 -0
  117. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/discovery/test_catalog_build.py +0 -0
  118. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/discovery/test_discovery_schema_resources.py +0 -0
  119. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/discovery/test_engine.py +0 -0
  120. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/discovery/test_planner.py +0 -0
  121. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/discovery/test_policy.py +0 -0
  122. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_backend.py +0 -0
  123. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_bundles.py +0 -0
  124. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_diagnostics.py +0 -0
  125. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_environment_integration.py +0 -0
  126. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_events_health.py +0 -0
  127. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_facade.py +0 -0
  128. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_frontmatter.py +0 -0
  129. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_git.py +0 -0
  130. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_interactive.py +0 -0
  131. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_lockfiles.py +0 -0
  132. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_multifile.py +0 -0
  133. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_paths.py +0 -0
  134. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_prebuilt_policy.py +0 -0
  135. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_projects.py +0 -0
  136. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_references.py +0 -0
  137. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_run_cli.py +0 -0
  138. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_runtime.py +0 -0
  139. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_schema_resources.py +0 -0
  140. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_schemas.py +0 -0
  141. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_signatures.py +0 -0
  142. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_specs.py +0 -0
  143. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_store.py +0 -0
  144. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_toolchains.py +0 -0
  145. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_v1_precision.py +0 -0
  146. {lean_runtime-2.0.7 → lean_runtime-2.0.8}/tests/test_verification_inventory.py +0 -0
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.0.8 - 2026-08-10
6
+
7
+ - Add immutable, content-addressed provenance to ready-to-run program
8
+ descriptions and accept it through `program-create --provenance-file`.
9
+ - Preserve the identity and import compatibility of existing version 1 program
10
+ descriptions while emitting version 2 descriptions for newly created programs.
11
+
5
12
  ## 2.0.7
6
13
 
7
14
  - Retry an OCI layer once from a clean partial after size or SHA-256 integrity
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.7
3
+ Version: 2.0.8
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -17,6 +17,7 @@ program = runtime.create_program(
17
17
  "build/program",
18
18
  command=["my-program"],
19
19
  source_revision="0123456789abcdef0123456789abcdef01234567",
20
+ provenance={"example.protocol.version": "1.0.0"},
20
21
  )
21
22
 
22
23
  with program.spawn_interactive() as session:
@@ -28,8 +29,11 @@ alive and returns one decoded response per request. This is the fast path for
28
29
  large batches: prepare and verify the checker once, then stream compact inputs
29
30
  instead of elaborating a new Lean source file for every item.
30
31
 
31
- `create_program` records the program's files and computer compatibility. Opening
32
- it later with `runtime.program(program.id)` verifies that neither has changed.
32
+ `create_program` records the program's files, computer compatibility, and
33
+ optional flat string provenance. All three contribute to the program ID.
34
+ Opening it later with `runtime.program(program.id)` verifies that none has
35
+ changed. The CLI accepts the same metadata as a JSON object through
36
+ `program-create --provenance-file`.
33
37
 
34
38
  ## Move it or share it
35
39
 
@@ -181,6 +181,11 @@ def parser() -> argparse.ArgumentParser:
181
181
  program_create.add_argument("--exact-environment-id")
182
182
  program_create.add_argument("--toolchain", default="unknown")
183
183
  program_create.add_argument("--capability-id")
184
+ program_create.add_argument(
185
+ "--provenance-file",
186
+ type=Path,
187
+ help="JSON object of content-addressed program provenance",
188
+ )
184
189
 
185
190
  program_export = commands.add_parser(
186
191
  "program-save-copy", help="save a portable copy of a ready-to-run program"
@@ -398,6 +403,14 @@ def main(argv: list[str] | None = None) -> int:
398
403
  _json(environment.inspect().to_dict())
399
404
  return 0
400
405
  if args.command == "program-create":
406
+ provenance = None
407
+ if args.provenance_file is not None:
408
+ value = json.loads(args.provenance_file.read_text(encoding="utf-8"))
409
+ if not isinstance(value, dict) or not all(
410
+ isinstance(key, str) and isinstance(item, str) for key, item in value.items()
411
+ ):
412
+ raise ValueError("program provenance file must contain a JSON string object")
413
+ provenance = value
401
414
  program = runtime.create_program(
402
415
  args.payload,
403
416
  command=args.program_command,
@@ -406,6 +419,7 @@ def main(argv: list[str] | None = None) -> int:
406
419
  exact_environment_id=args.exact_environment_id,
407
420
  toolchain=args.toolchain,
408
421
  capability_id=args.capability_id,
422
+ provenance=provenance,
409
423
  )
410
424
  _json(
411
425
  {
@@ -14,9 +14,10 @@ import tempfile
14
14
  import uuid
15
15
  from collections.abc import Mapping, Sequence
16
16
  from contextlib import suppress
17
- from dataclasses import asdict, dataclass
17
+ from dataclasses import asdict, dataclass, field
18
18
  from datetime import datetime, timezone
19
19
  from pathlib import Path, PurePosixPath
20
+ from types import MappingProxyType
20
21
  from typing import Any, cast
21
22
 
22
23
  from ._paths import remove_tree
@@ -47,8 +48,10 @@ from .policies import ExecutionPolicy
47
48
  from .serialization import canonical_json_bytes, sha256_id, write_json_atomic
48
49
  from .store import EnvironmentStore, clone_tree, platform_compatibility, platform_record
49
50
 
50
- PROGRAM_SCHEMA = "lean-runtime-execution-program/1"
51
- PROGRAM_CONFIG_MEDIA_TYPE = "application/vnd.lean-runtime.program.config.v1+json"
51
+ LEGACY_PROGRAM_SCHEMA = "lean-runtime-execution-program/1"
52
+ PROGRAM_SCHEMA = "lean-runtime-execution-program/2"
53
+ LEGACY_PROGRAM_CONFIG_MEDIA_TYPE = "application/vnd.lean-runtime.program.config.v1+json"
54
+ PROGRAM_CONFIG_MEDIA_TYPE = "application/vnd.lean-runtime.program.config.v2+json"
52
55
  PROGRAM_LAYER_MEDIA_TYPE = "application/vnd.lean-runtime.program.layer.v1.tar+gzip"
53
56
  _PROGRAM_ID = re.compile(r"program_[0-9a-f]{64}")
54
57
 
@@ -106,11 +109,14 @@ class ProgramDescription:
106
109
  exact_environment_id: str | None = None
107
110
  toolchain: str = "unknown"
108
111
  capability_id: str | None = None
112
+ provenance: Mapping[str, str] = field(default_factory=dict)
109
113
  schema: str = PROGRAM_SCHEMA
110
114
 
111
115
  def __post_init__(self) -> None:
112
- if self.schema != PROGRAM_SCHEMA:
116
+ if self.schema not in {LEGACY_PROGRAM_SCHEMA, PROGRAM_SCHEMA}:
113
117
  raise EnvironmentError(f"unsupported program schema: {self.schema!r}")
118
+ if self.schema == LEGACY_PROGRAM_SCHEMA and self.provenance:
119
+ raise EnvironmentError("legacy program descriptions cannot contain provenance")
114
120
  if not self.command or any(not isinstance(part, str) or not part for part in self.command):
115
121
  raise EnvironmentError("program command must contain non-empty strings")
116
122
  _relative_executable(self.command[0])
@@ -118,13 +124,26 @@ class ProgramDescription:
118
124
  raise EnvironmentError("program command executable is absent from its file inventory")
119
125
  if not re.fullmatch(r"[0-9a-f]{40,64}", self.source_revision):
120
126
  raise EnvironmentError("program source revision must be an exact Git commit")
127
+ if not all(
128
+ isinstance(key, str)
129
+ and bool(re.fullmatch(r"[a-z0-9][a-z0-9._-]*", key))
130
+ and isinstance(value, str)
131
+ and bool(value)
132
+ for key, value in self.provenance.items()
133
+ ):
134
+ raise EnvironmentError(
135
+ "program provenance must contain non-empty string values under canonical keys"
136
+ )
137
+ object.__setattr__(
138
+ self, "provenance", MappingProxyType(dict(sorted(self.provenance.items())))
139
+ )
121
140
 
122
141
  @property
123
142
  def program_id(self) -> str:
124
143
  return sha256_id("program", self.to_dict(include_id=False))
125
144
 
126
145
  def to_dict(self, *, include_id: bool = True) -> dict[str, Any]:
127
- value = {
146
+ value: dict[str, Any] = {
128
147
  "schema": self.schema,
129
148
  "command": list(self.command),
130
149
  "files": dict(sorted(self.files.items())),
@@ -135,6 +154,8 @@ class ProgramDescription:
135
154
  "toolchain": self.toolchain,
136
155
  "capability_id": self.capability_id,
137
156
  }
157
+ if self.schema == PROGRAM_SCHEMA:
158
+ value["provenance"] = dict(sorted(self.provenance.items()))
138
159
  return {"program_id": self.program_id, **value} if include_id else value
139
160
 
140
161
  @classmethod
@@ -150,6 +171,10 @@ class ProgramDescription:
150
171
  raise EnvironmentError("program manifest file inventory is invalid")
151
172
  if not isinstance(compatibility, dict):
152
173
  raise EnvironmentError("program platform compatibility is invalid")
174
+ schema = str(value.get("schema", ""))
175
+ provenance = value.get("provenance", {})
176
+ if not isinstance(provenance, dict):
177
+ raise EnvironmentError("program provenance is invalid")
153
178
  manifest = cls(
154
179
  command=tuple(command),
155
180
  files={str(name): dict(record) for name, record in files.items()},
@@ -169,7 +194,8 @@ class ProgramDescription:
169
194
  capability_id=(
170
195
  str(value["capability_id"]) if value.get("capability_id") is not None else None
171
196
  ),
172
- schema=str(value.get("schema", "")),
197
+ provenance={str(key): item for key, item in provenance.items()},
198
+ schema=schema,
173
199
  )
174
200
  recorded = value.get("program_id")
175
201
  if recorded is not None and recorded != manifest.program_id:
@@ -323,6 +349,7 @@ class ProgramManager:
323
349
  exact_environment_id: str | None = None,
324
350
  toolchain: str = "unknown",
325
351
  capability_id: str | None = None,
352
+ provenance: Mapping[str, str] | None = None,
326
353
  ) -> ReadyProgram:
327
354
  payload = payload.expanduser().resolve()
328
355
  if not payload.is_dir():
@@ -336,6 +363,7 @@ class ProgramManager:
336
363
  exact_environment_id=exact_environment_id,
337
364
  toolchain=toolchain,
338
365
  capability_id=capability_id,
366
+ provenance=dict(provenance or {}),
339
367
  )
340
368
  destination = self.store.programs / manifest.program_id
341
369
  with FileLock(self.store.lock_dir / f"{manifest.program_id}.lock", timeout=1800):
@@ -392,7 +420,12 @@ class ProgramManager:
392
420
  )
393
421
  config_path = staging / "config.json"
394
422
  config_path.write_bytes(canonical_json_bytes(program.description.to_dict()))
395
- config = _blob_descriptor_path(config_path, PROGRAM_CONFIG_MEDIA_TYPE)
423
+ config_media_type = (
424
+ LEGACY_PROGRAM_CONFIG_MEDIA_TYPE
425
+ if program.description.schema == LEGACY_PROGRAM_SCHEMA
426
+ else PROGRAM_CONFIG_MEDIA_TYPE
427
+ )
428
+ config = _blob_descriptor_path(config_path, config_media_type)
396
429
  manifest_path = staging / "manifest.json"
397
430
  manifest_path.write_bytes(
398
431
  canonical_json_bytes(
@@ -507,7 +540,11 @@ class ProgramManager:
507
540
  or len(layers) != 1
508
541
  ):
509
542
  raise EnvironmentError("program OCI manifest is incomplete")
510
- _require_media_type(config_descriptor, PROGRAM_CONFIG_MEDIA_TYPE, "program config")
543
+ if config_descriptor.get("mediaType") not in {
544
+ LEGACY_PROGRAM_CONFIG_MEDIA_TYPE,
545
+ PROGRAM_CONFIG_MEDIA_TYPE,
546
+ }:
547
+ raise EnvironmentError("program config has an unsupported media type")
511
548
  _require_media_type(layers[0], PROGRAM_LAYER_MEDIA_TYPE, "program layer")
512
549
  config_path = _descriptor_blob_path(entries, config_descriptor, "program config")
513
550
  manifest = ProgramDescription.from_dict(
@@ -219,6 +219,7 @@ class Runtime:
219
219
  exact_environment_id: str | None = None,
220
220
  toolchain: str = "unknown",
221
221
  capability_id: str | None = None,
222
+ provenance: Mapping[str, str] | None = None,
222
223
  ) -> ReadyProgram:
223
224
  """Create a verified ready-to-run program from a payload directory."""
224
225
  return self.programs.create(
@@ -229,6 +230,7 @@ class Runtime:
229
230
  exact_environment_id=exact_environment_id,
230
231
  toolchain=toolchain,
231
232
  capability_id=capability_id,
233
+ provenance=provenance,
232
234
  )
233
235
 
234
236
  def program(self, program_id: str) -> ReadyProgram:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.7
3
+ Version: 2.0.8
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "2.0.7"
7
+ version = "2.0.8"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
+ from types import SimpleNamespace
5
6
 
6
7
  from lean_runtime.bundles import PortableCopyInfo
7
8
  from lean_runtime.cli import _print_operation_failure, main, parser
@@ -172,6 +173,44 @@ def test_save_copy_cli_reports_copy_identity(monkeypatch, tmp_path: Path, capsys
172
173
  assert json.loads(capsys.readouterr().out)["copy_id"] == info.copy_id
173
174
 
174
175
 
176
+ def test_program_create_cli_loads_content_addressed_provenance(
177
+ monkeypatch, tmp_path: Path, capsys
178
+ ) -> None:
179
+ payload = tmp_path / "payload"
180
+ payload.mkdir()
181
+ profile = tmp_path / "profile.json"
182
+ profile.write_text(json.dumps({"example.protocol.version": "1.0.0"}))
183
+ observed = {}
184
+
185
+ def create_program(_runtime, selected_payload, **options):
186
+ observed.update(payload=selected_payload, **options)
187
+ return SimpleNamespace(
188
+ id="program_" + "a" * 64,
189
+ description=SimpleNamespace(to_dict=lambda: {"schema": "test"}),
190
+ root=payload,
191
+ )
192
+
193
+ monkeypatch.setattr("lean_runtime.cli.Runtime.create_program", create_program)
194
+ assert (
195
+ main(
196
+ [
197
+ "--quiet",
198
+ "program-create",
199
+ str(payload),
200
+ "--command",
201
+ "bin/checker",
202
+ "--source-revision",
203
+ "b" * 40,
204
+ "--provenance-file",
205
+ str(profile),
206
+ ]
207
+ )
208
+ == 0
209
+ )
210
+ assert observed["provenance"] == {"example.protocol.version": "1.0.0"}
211
+ assert json.loads(capsys.readouterr().out)["program_id"].startswith("program_")
212
+
213
+
175
214
  def test_finalize_publication_reads_computer_records(monkeypatch, tmp_path: Path, capsys) -> None:
176
215
  result = tmp_path / "computer.json"
177
216
  record = {"digest": "sha256:" + "a" * 64, "size": 42}
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  import pytest
8
8
 
9
9
  from lean_runtime import EnvironmentError, ExecutionPolicy, Runtime
10
+ from lean_runtime.programs import LEGACY_PROGRAM_SCHEMA, ProgramDescription
10
11
 
11
12
 
12
13
  def _revision() -> str:
@@ -30,6 +31,10 @@ def test_program_identity_payload_validation_and_interactive_execution(tmp_path:
30
31
  source_revision=_revision(),
31
32
  toolchain="leanprover/lean4:v4.32.2",
32
33
  capability_id="sha256:" + hashlib.sha256(b"capabilities").hexdigest(),
34
+ provenance={
35
+ "lean.toolchain": "leanprover/lean4:v4.32.2",
36
+ "leancert.core.revision": "b" * 40,
37
+ },
33
38
  )
34
39
 
35
40
  assert program.id.startswith("program_")
@@ -40,6 +45,40 @@ def test_program_identity_payload_validation_and_interactive_execution(tmp_path:
40
45
 
41
46
  reopened = runtime.program(program.id)
42
47
  assert reopened.description.source_revision == _revision()
48
+ assert reopened.description.provenance["leancert.core.revision"] == "b" * 40
49
+
50
+
51
+ def test_program_provenance_is_content_addressed(tmp_path: Path) -> None:
52
+ runtime = Runtime(home=tmp_path / "runtime")
53
+ payload = _payload(tmp_path)
54
+ first = runtime.create_program(
55
+ payload,
56
+ command=("echo-bridge",),
57
+ source_revision=_revision(),
58
+ provenance={"leancert.core.revision": "b" * 40},
59
+ )
60
+ second = runtime.create_program(
61
+ payload,
62
+ command=("echo-bridge",),
63
+ source_revision=_revision(),
64
+ provenance={"leancert.core.revision": "c" * 40},
65
+ )
66
+
67
+ assert first.id != second.id
68
+
69
+
70
+ def test_legacy_program_identity_remains_stable() -> None:
71
+ description = ProgramDescription(
72
+ command=("echo-bridge",),
73
+ files={"echo-bridge": {"kind": "file", "sha256": "sha256:" + "0" * 64}},
74
+ computer_compatibility={"schema": "computer/1"},
75
+ source_revision=_revision(),
76
+ schema=LEGACY_PROGRAM_SCHEMA,
77
+ )
78
+
79
+ restored = ProgramDescription.from_dict(description.to_dict())
80
+ assert restored.program_id == description.program_id
81
+ assert "provenance" not in restored.to_dict()
43
82
 
44
83
 
45
84
  def test_program_rejects_payload_tampering(tmp_path: Path) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes