techtree 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 (174) hide show
  1. techtree/__init__.py +35 -0
  2. techtree/__main__.py +14 -0
  3. techtree/canonical.py +239 -0
  4. techtree/catalog/__init__.py +25 -0
  5. techtree/catalog/repository.py +400 -0
  6. techtree/catalog/service.py +419 -0
  7. techtree/cli/__init__.py +1 -0
  8. techtree/cli/app.py +416 -0
  9. techtree/cli/commands/__init__.py +1 -0
  10. techtree/cli/commands/climb.py +1223 -0
  11. techtree/cli/commands/doctor.py +147 -0
  12. techtree/cli/commands/engine.py +207 -0
  13. techtree/cli/commands/proof.py +556 -0
  14. techtree/cli/commands/publish.py +447 -0
  15. techtree/cli/commands/release.py +303 -0
  16. techtree/cli/commands/run.py +1067 -0
  17. techtree/cli/commands/setup.py +181 -0
  18. techtree/cli/commands/skill.py +221 -0
  19. techtree/cli/commands/uplift.py +698 -0
  20. techtree/cli/commands/withdraw.py +212 -0
  21. techtree/cli/confirm.py +47 -0
  22. techtree/cli/context.py +96 -0
  23. techtree/cli/invoke.py +220 -0
  24. techtree/cli/output.py +280 -0
  25. techtree/constants.py +138 -0
  26. techtree/crypto.py +128 -0
  27. techtree/doctor/__init__.py +1 -0
  28. techtree/doctor/checks.py +675 -0
  29. techtree/doctor/execution_checks.py +435 -0
  30. techtree/doctor/service.py +326 -0
  31. techtree/drafts/__init__.py +32 -0
  32. techtree/drafts/source.py +146 -0
  33. techtree/drafts/store.py +992 -0
  34. techtree/engines/__init__.py +1 -0
  35. techtree/engines/bundle.py +251 -0
  36. techtree/engines/installer.py +679 -0
  37. techtree/engines/registry.py +235 -0
  38. techtree/engines/runner.py +170 -0
  39. techtree/errors.py +262 -0
  40. techtree/fs.py +234 -0
  41. techtree/harness.py +108 -0
  42. techtree/identity/__init__.py +41 -0
  43. techtree/identity/models.py +113 -0
  44. techtree/identity/service.py +199 -0
  45. techtree/identity/store.py +263 -0
  46. techtree/ids.py +85 -0
  47. techtree/manifests/__init__.py +39 -0
  48. techtree/manifests/builder.py +433 -0
  49. techtree/manifests/compare.py +376 -0
  50. techtree/models/__init__.py +282 -0
  51. techtree/models/base.py +201 -0
  52. techtree/models/campaign.py +484 -0
  53. techtree/models/catalog.py +227 -0
  54. techtree/models/cli.py +151 -0
  55. techtree/models/climb.py +254 -0
  56. techtree/models/data_policy.py +130 -0
  57. techtree/models/engine.py +156 -0
  58. techtree/models/episode_receipt.py +130 -0
  59. techtree/models/evaluation_backend.py +113 -0
  60. techtree/models/experiment.py +154 -0
  61. techtree/models/run.py +214 -0
  62. techtree/models/skill.py +156 -0
  63. techtree/models/uplift_report.py +158 -0
  64. techtree/models/validation.py +299 -0
  65. techtree/paths.py +116 -0
  66. techtree/presentation/__init__.py +31 -0
  67. techtree/presentation/build.py +1242 -0
  68. techtree/presentation/compact.py +246 -0
  69. techtree/presentation/evidence.py +169 -0
  70. techtree/presentation/models.py +358 -0
  71. techtree/presentation/rich.py +312 -0
  72. techtree/presentation/sanitize.py +156 -0
  73. techtree/publication/__init__.py +44 -0
  74. techtree/publication/address.py +180 -0
  75. techtree/publication/coordinates.py +26 -0
  76. techtree/publication/journal.py +212 -0
  77. techtree/publication/keccak.py +183 -0
  78. techtree/publication/models.py +209 -0
  79. techtree/publication/offer.py +35 -0
  80. techtree/publication/service.py +618 -0
  81. techtree/publication/transport.py +296 -0
  82. techtree/publication/verify.py +242 -0
  83. techtree/publication/withdraw.py +156 -0
  84. techtree/py.typed +0 -0
  85. techtree/receipts/__init__.py +52 -0
  86. techtree/receipts/bundle.py +578 -0
  87. techtree/receipts/compare.py +1065 -0
  88. techtree/receipts/episode.py +672 -0
  89. techtree/receipts/execution.py +630 -0
  90. techtree/receipts/observed.py +474 -0
  91. techtree/receipts/set.py +336 -0
  92. techtree/receipts/uplift.py +655 -0
  93. techtree/receipts/verify.py +1055 -0
  94. techtree/release/__init__.py +9 -0
  95. techtree/release/bootstrap.py +509 -0
  96. techtree/release/checks.py +376 -0
  97. techtree/release/document.py +125 -0
  98. techtree/release/generate.py +221 -0
  99. techtree/release/models.py +293 -0
  100. techtree/release/provenance.py +109 -0
  101. techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
  102. techtree/resources/catalog/catalog.json +32 -0
  103. techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
  104. techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
  105. techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
  106. techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
  107. techtree/resources/engines/default/engine.json +20 -0
  108. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
  109. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
  110. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
  111. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
  112. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
  113. techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
  114. techtree/resources/engines/default/pyproject.toml +23 -0
  115. techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
  116. techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
  117. techtree/resources/engines/default/tools/normalize_validation.py +222 -0
  118. techtree/resources/engines/default/uv.lock +1758 -0
  119. techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
  120. techtree/resources/release/build-provenance.json +4 -0
  121. techtree/resources/release/release-core.json +24 -0
  122. techtree/runs/__init__.py +31 -0
  123. techtree/runs/artifacts.py +750 -0
  124. techtree/runs/child_registry.py +228 -0
  125. techtree/runs/events.py +478 -0
  126. techtree/runs/executor.py +140 -0
  127. techtree/runs/fake.py +741 -0
  128. techtree/runs/launcher.py +253 -0
  129. techtree/runs/machine.py +489 -0
  130. techtree/runs/real.py +789 -0
  131. techtree/runs/service.py +616 -0
  132. techtree/runs/store.py +555 -0
  133. techtree/runs/validation.py +259 -0
  134. techtree/runs/variants.py +684 -0
  135. techtree/settings.py +143 -0
  136. techtree/skills/__init__.py +14 -0
  137. techtree/skills/archive.py +282 -0
  138. techtree/skills/policy.py +62 -0
  139. techtree/skills/scanner.py +394 -0
  140. techtree/skills/service.py +752 -0
  141. techtree/skills/starter.py +434 -0
  142. techtree/tasksets/__init__.py +1 -0
  143. techtree/tasksets/membership.py +269 -0
  144. techtree/tasksets/provider.py +207 -0
  145. techtree/tasksets/resolver.py +311 -0
  146. techtree/tasksets/service.py +484 -0
  147. techtree/tasksets/verifiers_cli.py +538 -0
  148. techtree/uplift/__init__.py +20 -0
  149. techtree/uplift/context.py +544 -0
  150. techtree/uplift/derive.py +203 -0
  151. techtree/uplift/public_tasks.py +151 -0
  152. techtree/uplift/service.py +719 -0
  153. techtree/uplift/source.py +160 -0
  154. techtree/verifiers/__init__.py +31 -0
  155. techtree/verifiers/budget.py +219 -0
  156. techtree/verifiers/child.py +633 -0
  157. techtree/verifiers/compiler.py +432 -0
  158. techtree/verifiers/config.py +365 -0
  159. techtree/verifiers/credentials.py +321 -0
  160. techtree/verifiers/image.py +126 -0
  161. techtree/verifiers/models.py +527 -0
  162. techtree/verifiers/outputs.py +368 -0
  163. techtree/verifiers/progress.py +192 -0
  164. techtree/verifiers/supervisor.py +341 -0
  165. techtree/verifiers/verify.py +782 -0
  166. techtree/version.py +39 -0
  167. techtree/worker/__init__.py +18 -0
  168. techtree/worker/execute.py +487 -0
  169. techtree/worker/main.py +57 -0
  170. techtree-0.1.0.dist-info/METADATA +344 -0
  171. techtree-0.1.0.dist-info/RECORD +174 -0
  172. techtree-0.1.0.dist-info/WHEEL +4 -0
  173. techtree-0.1.0.dist-info/entry_points.txt +3 -0
  174. techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,221 @@
1
+ """Binding one release together. Spec sections 9.3.1, 9.3.3 and 9.4.
2
+
3
+ Generation is the step that turns "a source tree and a handful of founder
4
+ decisions" into one document every other repository copies. It runs *before*
5
+ the wheel is built and before the plugin is tagged, so nothing it reads may
6
+ depend on either — that ordering is the whole reason the cross-repository
7
+ binding has no cycle in it (spec section 9.3.3).
8
+
9
+ Two kinds of value meet here and they are kept strictly apart.
10
+
11
+ The founder's decisions arrive as :class:`~techtree.release.models.
12
+ ReleaseInputs`, read from a file a person edits. This module never invents one
13
+ and never guesses a default for one.
14
+
15
+ Everything else is *read out of the tree being released* — the protocol
16
+ version, the digest of the engine bundle, the digest of the catalog index, and
17
+ the subject harness version the introductory Climb's Campaign pins. Those are
18
+ facts about the source, so asking a person for them would only create a second
19
+ answer that could disagree with the first.
20
+
21
+ The introductory Climb reference sits on the seam: the founder chooses which
22
+ Climb the release leads with, and this module refuses the choice unless the
23
+ catalog actually ships it under exactly that reference. A release that
24
+ advertises a Climb nobody shipped is the failure this check exists for.
25
+
26
+ Generation is deterministic. The same source tree and the same inputs produce
27
+ the same bytes on any machine, at any time, which is what lets a drift check
28
+ regenerate the artifacts into a throwaway copy of the repository and compare.
29
+ Nothing here reads the clock, the environment, the network, or version control.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from collections.abc import Mapping
35
+ from dataclasses import dataclass
36
+ from importlib.resources.abc import Traversable
37
+ from typing import Final
38
+
39
+ from techtree.catalog.repository import (
40
+ CATALOG_INDEX_FILENAME,
41
+ EmbeddedCatalogRepository,
42
+ packaged_catalog_root,
43
+ )
44
+ from techtree.engines.bundle import embedded_engine_root, engine_bundle_digest
45
+ from techtree.errors import ValidationError
46
+ from techtree.models.base import Digest, JsonValue
47
+ from techtree.release.document import document_digest
48
+ from techtree.release.models import (
49
+ BUILD_INFO_SCHEMA_VERSION,
50
+ RELEASE_CORE_SCHEMA_VERSION,
51
+ ReleaseCore,
52
+ ReleaseInputs,
53
+ )
54
+ from techtree.version import PROTOCOL_VERSION
55
+
56
+ __all__ = [
57
+ "BUILD_PROVENANCE_MECHANISM",
58
+ "GENERATOR_VERSION",
59
+ "ReleaseSources",
60
+ "build_info_document",
61
+ "build_release_core",
62
+ "packaged_sources",
63
+ ]
64
+
65
+ #: Bumped when the generated shape changes, so a stored artifact can say which
66
+ #: generator produced it.
67
+ GENERATOR_VERSION: Final = "0.1.0"
68
+
69
+ #: How a wheel comes to know which commit it was built from (decisions 0026).
70
+ #: One mechanism, described where the release process is recorded, so that a
71
+ #: reader of ``release/build-info.json`` never has to read the build hook to
72
+ #: learn what the stamp inside a wheel means or how much it is worth.
73
+ BUILD_PROVENANCE_MECHANISM: Final[Mapping[str, str]] = {
74
+ "mechanism": (
75
+ "a hatchling build hook, tools/stamp_provenance.py, writes the source "
76
+ "commit into the package while the wheel is being built"
77
+ ),
78
+ "build_command": "uv build --wheel",
79
+ "stamped_file": "techtree/resources/release/build-provenance.json",
80
+ "stamp_schema_version": "techtree.build-provenance.v1",
81
+ "commit_source": "git rev-parse HEAD, asked of the tree being packaged",
82
+ "refuses": (
83
+ "a build with no git, no commit, or any packaged path (src/techtree, "
84
+ "pyproject.toml, README.md, LICENSE) differing from that commit; there "
85
+ "is no unknown value and no default"
86
+ ),
87
+ "in_the_committed_tree": (
88
+ "never; the stamp belongs to one artifact and is written and removed "
89
+ "by the build"
90
+ ),
91
+ "editable_installs": (
92
+ "not stamped: an editable install is the working tree rather than a "
93
+ "distributable artifact, and `techtree release info` reports that no "
94
+ "commit was stamped instead of naming one"
95
+ ),
96
+ "reported_by": "techtree release info",
97
+ }
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class ReleaseSources:
102
+ """The part of a release that is read rather than decided."""
103
+
104
+ catalog_root: Traversable
105
+ engine_root: Traversable
106
+ protocol_version: str
107
+
108
+ def catalog_index_bytes(self) -> bytes:
109
+ """Return the exact bytes of the catalog index."""
110
+ return (self.catalog_root / CATALOG_INDEX_FILENAME).read_bytes()
111
+
112
+
113
+ def packaged_sources() -> ReleaseSources:
114
+ """Return the sources of the build this process is running from."""
115
+ return ReleaseSources(
116
+ catalog_root=packaged_catalog_root(),
117
+ engine_root=embedded_engine_root(),
118
+ protocol_version=PROTOCOL_VERSION,
119
+ )
120
+
121
+
122
+ def build_release_core(inputs: ReleaseInputs, sources: ReleaseSources) -> ReleaseCore:
123
+ """Bind founder decisions and source facts into one ReleaseCore."""
124
+ catalog = EmbeddedCatalogRepository(sources.catalog_root)
125
+ reference = _exact_climb_reference(catalog, inputs.intro_climb_reference)
126
+ climb = catalog.load_climb(reference)
127
+ campaign = catalog.load_campaign(climb.campaign_spec_digest)
128
+
129
+ values = {
130
+ "cli_version": inputs.cli_version,
131
+ "maximum_tested_host_hermes_version": (
132
+ inputs.maximum_tested_host_hermes_version
133
+ ),
134
+ "minimum_host_hermes_version": inputs.minimum_host_hermes_version,
135
+ "release_id": inputs.release_id,
136
+ "skill_improver_digest": inputs.skill_improver_digest,
137
+ "starter_skill_digest": inputs.starter_skill_digest,
138
+ "starter_skill_object_url": inputs.starter_skill_object_url,
139
+ }
140
+
141
+ return ReleaseCore(
142
+ schema_version=RELEASE_CORE_SCHEMA_VERSION,
143
+ protocol_version=sources.protocol_version,
144
+ engine_digest=engine_bundle_digest(sources.engine_root),
145
+ catalog_digest=document_digest(sources.catalog_index_bytes()),
146
+ intro_climb_reference=reference,
147
+ subject_hermes_version=campaign.subject.harness.version,
148
+ # Where a run is published, where it is then read, and the public half
149
+ # of the key that countersigns the answer. None of the three can be
150
+ # derived from this source tree — they are facts about a deployment and
151
+ # about a key the founder generates — so they arrive with the founder's
152
+ # other decisions and are copied through unchanged. Decisions 0038.
153
+ publication=inputs.publication,
154
+ **values,
155
+ )
156
+
157
+
158
+ def build_info_document(
159
+ *,
160
+ inputs_digest: Digest,
161
+ pyproject_version: str,
162
+ core: ReleaseCore,
163
+ artifacts: Mapping[str, Digest],
164
+ ) -> dict[str, JsonValue]:
165
+ """Return what was built, from what, without reading the clock.
166
+
167
+ A build record normally starts with a timestamp. This one cannot: the
168
+ artifacts it describes are regenerated in a throwaway copy of the
169
+ repository and compared byte for byte, so anything that changes between two
170
+ identical builds would turn every drift check into a false alarm. What
171
+ matters for support is which inputs produced which bytes, and that is what
172
+ is recorded.
173
+
174
+ ``pyproject_version`` is recorded even though the ReleaseCore names the
175
+ published CLI version separately. They are two different statements — what
176
+ this source tree declares, and what the release publishes — and both a
177
+ support conversation and the version check in
178
+ :mod:`techtree.release.checks` need to see both.
179
+
180
+ ``build_provenance`` describes how the *wheel* gets the one coordinate this
181
+ document deliberately does not carry: the commit it was built from
182
+ (decisions 0026). The description is here because this is the record of how
183
+ a release is produced; the value itself exists only inside a built wheel.
184
+
185
+ ``artifacts`` does not include this document. A build record cannot state
186
+ its own digest, and pretending otherwise would only produce a value that
187
+ could never be recomputed.
188
+ """
189
+ return {
190
+ "schema_version": BUILD_INFO_SCHEMA_VERSION,
191
+ "generator": "tools/build_release_core.py",
192
+ "generator_version": GENERATOR_VERSION,
193
+ "build_provenance": dict(BUILD_PROVENANCE_MECHANISM),
194
+ "inputs": {
195
+ "release_inputs_digest": inputs_digest,
196
+ "pyproject_version": pyproject_version,
197
+ "protocol_version": core.protocol_version,
198
+ "engine_digest": core.engine_digest,
199
+ "catalog_digest": core.catalog_digest,
200
+ "intro_climb_reference": core.intro_climb_reference,
201
+ },
202
+ "artifacts": dict(sorted(artifacts.items())),
203
+ }
204
+
205
+
206
+ def _exact_climb_reference(catalog: EmbeddedCatalogRepository, reference: str) -> str:
207
+ """Return the reference, having proved the catalog ships exactly it.
208
+
209
+ The repository resolves a bare slug to the newest version, which is right
210
+ for a person browsing and wrong for a release document: a release names one
211
+ immutable Climb, and it has to name it the way the catalog files it.
212
+ """
213
+ available = catalog.list_climb_references()
214
+ if reference not in available:
215
+ raise ValidationError(
216
+ f"the release names {reference!r} as its introductory Climb, but "
217
+ "this catalog does not ship that exact reference",
218
+ code="intro_climb_not_in_catalog",
219
+ details={"reference": reference, "available": list(available)},
220
+ )
221
+ return reference
@@ -0,0 +1,293 @@
1
+ """What a ReleaseCore is, and what makes one honest. Spec sections 6.6, 9.3.
2
+
3
+ A ReleaseCore is the single document that says which CLI, which engine, which
4
+ catalog, which Climb, and which Skills are one release. It is generated before
5
+ the CLI wheel and the plugin commit exist, which is what keeps the
6
+ cross-repository binding acyclic (spec section 9.3.3): the wheel and the plugin
7
+ embed these exact bytes, and the website's later ``BootstrapRelease`` wraps
8
+ them.
9
+
10
+ Decisions document 0026 settles what may be in it. A ReleaseCore is a
11
+ *contract*: it holds only values a person can know when they author it, so it
12
+ is honestly final the moment it is committed. It therefore says nothing about
13
+ the artifact built from it — no wheel hash, no plugin commit, and no source
14
+ commit, because an artifact never describes its own identity. Identity is
15
+ stamped onto the wheel at build time (:mod:`techtree.release.provenance`) and
16
+ witnessed externally by the website's bootstrap document.
17
+
18
+ Every coordinate here is concrete, and the schema is what makes that true
19
+ rather than a convention anyone has to remember. A version is three numbers, a
20
+ release identifier is a name, a digest is sixty-four hexadecimal characters
21
+ that are not all zero, and the starter Skill's address is a content address
22
+ ending in the digest of the bytes it returns. None of those admit a spelling
23
+ that means "nobody has chosen this yet", so a document that validates is a
24
+ document whose every coordinate was chosen.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import base64
30
+ import re
31
+ from typing import Annotated, Final, Literal, Self
32
+
33
+ from pydantic import AfterValidator, StringConstraints, model_validator
34
+
35
+ from techtree.canonical import sha256_digest_bytes
36
+ from techtree.constants import DIGEST_PREFIX
37
+ from techtree.crypto import ED25519_PUBLIC_KEY_BYTES
38
+ from techtree.models.base import Digest, NonEmptyString, ProtocolModel, PublicKeyRef
39
+
40
+ __all__ = [
41
+ "BUILD_INFO_SCHEMA_VERSION",
42
+ "CONCRETE_DIGEST_LENGTH",
43
+ "CONCRETE_DIGEST_PATTERN",
44
+ "OBJECT_URL_PATTERN",
45
+ "PLAIN_HTTPS_URL_PATTERN",
46
+ "RELEASE_CORE_SCHEMA_VERSION",
47
+ "RELEASE_ID_PATTERN",
48
+ "RELEASE_INPUTS_SCHEMA_VERSION",
49
+ "VERSION_PATTERN",
50
+ "ConcreteDigest",
51
+ "ObjectUrl",
52
+ "PinnedNetworkKey",
53
+ "PlainHttpsUrl",
54
+ "PublicationCoordinates",
55
+ "ReleaseCore",
56
+ "ReleaseId",
57
+ "ReleaseInputs",
58
+ "Version",
59
+ "object_url_digest",
60
+ ]
61
+
62
+ RELEASE_CORE_SCHEMA_VERSION: Final = "techtree.release-core.v1"
63
+ RELEASE_INPUTS_SCHEMA_VERSION: Final = "techtree.release-inputs.v1"
64
+ BUILD_INFO_SCHEMA_VERSION: Final = "techtree.release-build-info.v1"
65
+
66
+ #: A published version is three numbers. Everything this release names — the
67
+ #: CLI, the host Hermes range, the subject harness — is versioned that way, and
68
+ #: nothing that is merely *proposed* has a version at all, so the pattern needs
69
+ #: no room for a suffix that would let one read as the other.
70
+ VERSION_PATTERN: Final = r"^[0-9]+(?:\.[0-9]+){2}$"
71
+
72
+ type Version = Annotated[str, StringConstraints(pattern=VERSION_PATTERN)]
73
+
74
+ #: A release identifier is a name a person gives one release — ``climb-v0.1.0``
75
+ #: — so it begins with a letter and joins lowercase words with ``-`` or ``.``.
76
+ #: A bare number is a version, and a version is not an identity.
77
+ RELEASE_ID_PATTERN: Final = r"^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$"
78
+
79
+ type ReleaseId = Annotated[str, StringConstraints(pattern=RELEASE_ID_PATTERN)]
80
+
81
+ #: Sixty-four hexadecimal characters with at least one of them non-zero. The
82
+ #: length is fixed by :data:`CONCRETE_DIGEST_LENGTH` beside it, so this pattern
83
+ #: only has to say "not all zeros": nothing hashes to zero, so a zeroed digest
84
+ #: is never a measurement.
85
+ CONCRETE_DIGEST_PATTERN: Final = rf"^{DIGEST_PREFIX}0*[1-9a-f][0-9a-f]*$"
86
+ CONCRETE_DIGEST_LENGTH: Final = len(DIGEST_PREFIX) + 64
87
+
88
+ type ConcreteDigest = Annotated[
89
+ str,
90
+ StringConstraints(
91
+ pattern=CONCRETE_DIGEST_PATTERN,
92
+ min_length=CONCRETE_DIGEST_LENGTH,
93
+ max_length=CONCRETE_DIGEST_LENGTH,
94
+ ),
95
+ ]
96
+
97
+ #: Where a published artifact is served from. Spec section 4.1 requires the
98
+ #: starter Skill to be an exact read-only object URL, and decision 0007 R10
99
+ #: requires every coordinate of a real release to be concrete and immutable, so
100
+ #: this is an absolute ``https`` content address: it ends in the digest of the
101
+ #: bytes it returns, which is how the website files public objects and how a
102
+ #: fetcher checks a response before it has scanned anything.
103
+ #:
104
+ #: The authority may not carry userinfo. A published coordinate is copied into
105
+ #: the plugin, into the website, into an approval packet and into support
106
+ #: transcripts, and ``https://user:token@host/…`` would carry a credential
107
+ #: through every one of them — while also making the address mean different
108
+ #: things to different readers. A public read-only object needs no credential,
109
+ #: so an address that offers one is not the coordinate this field is for.
110
+ OBJECT_URL_PATTERN: Final = rf"^https://[^\s/@]+/[^\s]*{DIGEST_PREFIX}[0-9a-f]{{64}}$"
111
+
112
+
113
+ def _require_resolvable_host(value: str) -> str:
114
+ """Reject an address under a top-level domain that can never resolve.
115
+
116
+ RFC 2606 reserves ``.invalid`` so that names under it are guaranteed not to
117
+ resolve. An address nothing can ever fetch is not a place something is
118
+ published, whatever else it looks like.
119
+ """
120
+ authority = value.removeprefix("https://").split("/", 1)[0]
121
+ host = authority.rsplit(":", 1)[0] if ":" in authority else authority
122
+ if host.lower().endswith(".invalid"):
123
+ raise ValueError("an address under .invalid can never resolve")
124
+ return value
125
+
126
+
127
+ type ObjectUrl = Annotated[
128
+ str,
129
+ StringConstraints(pattern=OBJECT_URL_PATTERN),
130
+ AfterValidator(_require_resolvable_host),
131
+ ]
132
+
133
+
134
+ _OBJECT_URL_DIGEST_RE: Final = re.compile(rf"{DIGEST_PREFIX}[0-9a-f]{{64}}$")
135
+
136
+
137
+ def object_url_digest(url: str) -> Digest:
138
+ """Return the file digest a content address is keyed by.
139
+
140
+ The address ends in the digest of the bytes it serves, which is what makes
141
+ it safe to fetch from at all: whatever arrives is checked against the
142
+ address it came from before anything else looks at it.
143
+
144
+ Raises:
145
+ ValueError: when the address ends in no digest, and therefore promises
146
+ nothing about what it serves.
147
+ """
148
+ found = _OBJECT_URL_DIGEST_RE.search(url)
149
+ if found is None:
150
+ raise ValueError(f"{url!r} is not keyed by the digest of what it serves")
151
+ return found.group()
152
+
153
+
154
+ #: A published address with nothing after its path. Every URL a release pins is
155
+ #: read by a machine and printed to a person, so it is ``https`` and it carries
156
+ #: no query string and no fragment: a submission travels in a request body and
157
+ #: never in a URL, and an address that could carry one would be a place a proof
158
+ #: or a volunteered address could end up in somebody's access log.
159
+ PLAIN_HTTPS_URL_PATTERN: Final = r"^https://[^\s/@?#]+(?:/[^\s?#]*)?$"
160
+
161
+ type PlainHttpsUrl = Annotated[
162
+ str,
163
+ StringConstraints(pattern=PLAIN_HTTPS_URL_PATTERN),
164
+ AfterValidator(_require_resolvable_host),
165
+ ]
166
+
167
+
168
+ class PinnedNetworkKey(PublicKeyRef):
169
+ """The public half of the key the run log countersigns receipts with.
170
+
171
+ Requiring a receipt to carry a key and a signature proves nothing on its
172
+ own: a server that wanted to lie would invent a key, sign with it, and hand
173
+ both to the participant. What makes a countersignature worth having is that
174
+ the participant already knows which key to expect, so the key is pinned in
175
+ the release rather than learned from the answer.
176
+
177
+ The identifier is the digest of the public key and is checked to be, as
178
+ every other key identifier in this protocol is (decisions 0038, founder
179
+ ruling of 2026-08-27). A derived identifier cannot name one key and carry
180
+ another, so a receipt that names a key it does not carry is caught without
181
+ anybody writing a check for it. Rotation is a new key and therefore a new
182
+ identifier, and a release pins exactly the one it trusts.
183
+
184
+ Only the public half ever reaches this repository. Nothing here can sign,
185
+ and nothing here is a place a private key could be put.
186
+ """
187
+
188
+ @model_validator(mode="after")
189
+ def _check_the_identifier_is_the_digest_of_the_key(self) -> Self:
190
+ """Reject a key whose identifier was assigned rather than derived."""
191
+ raw = base64.b64decode(self.public_key, validate=True)
192
+ if len(raw) != ED25519_PUBLIC_KEY_BYTES:
193
+ raise ValueError(
194
+ f"an Ed25519 public key is {ED25519_PUBLIC_KEY_BYTES} raw bytes"
195
+ )
196
+ if not any(raw):
197
+ raise ValueError(
198
+ "an all-zero public key names no key; it is the spelling of a "
199
+ "coordinate nobody has chosen yet, and a release pins only "
200
+ "coordinates somebody chose"
201
+ )
202
+ derived = sha256_digest_bytes(raw)
203
+ if self.key_id != derived:
204
+ raise ValueError(
205
+ f"a network key identifier is the digest of the key it carries: "
206
+ f"this one says {self.key_id} and carries {derived}"
207
+ )
208
+ return self
209
+
210
+
211
+ class PublicationCoordinates(ProtocolModel):
212
+ """Where a run is published, where it is then read, and who countersigns.
213
+
214
+ All three are release coordinates rather than settings, for one reason each
215
+ (decisions 0038, founder ruling of 2026-08-27).
216
+
217
+ ``submission_endpoint`` is pinned because a stable release has to be able to
218
+ publish with nothing configured. An address that only arrived from the
219
+ environment meant a published wheel could not publish out of the box, and a
220
+ person who had to set a variable before publishing would be a person who
221
+ could set it to the wrong thing.
222
+
223
+ ``public_log_url`` is pinned because a receipt names where the entry now
224
+ lives, and an entry address the participant did not already expect is an
225
+ address the answering server chose. The receipt's address is checked to be
226
+ on this origin before the receipt is written down.
227
+
228
+ ``network_key`` is pinned because a countersignature is only worth the
229
+ prior knowledge of which key made it.
230
+ """
231
+
232
+ submission_endpoint: PlainHttpsUrl
233
+ public_log_url: PlainHttpsUrl
234
+ network_key: PinnedNetworkKey
235
+
236
+
237
+ class ReleaseCore(ProtocolModel):
238
+ """The frozen coordinates of one Climb release. Spec section 6.6.
239
+
240
+ Every field is required and concrete. There is no field that describes the
241
+ wheel, the plugin, or the commit this document was authored at: those are
242
+ facts about artifacts that do not exist yet when the release is written,
243
+ and decision 0026 puts them where they can be known — stamped into the
244
+ wheel, and witnessed by the website's bootstrap document.
245
+
246
+ ``starter_skill_digest`` and ``starter_skill_object_url`` are two halves of
247
+ one coordinate and neither substitutes for the other. The digest says
248
+ *which* Skill the release measured — spec section 4.1 and decisions 0008
249
+ make it the ordered content-tree digest, not a file hash — and the URL says
250
+ where a machine that does not already hold those bytes can obtain them, and
251
+ which bytes the address itself promises.
252
+ """
253
+
254
+ schema_version: Literal["techtree.release-core.v1"]
255
+ release_id: ReleaseId
256
+ cli_version: Version
257
+ protocol_version: NonEmptyString
258
+ engine_digest: ConcreteDigest
259
+ catalog_digest: ConcreteDigest
260
+ intro_climb_reference: NonEmptyString
261
+ starter_skill_digest: ConcreteDigest
262
+ starter_skill_object_url: ObjectUrl
263
+ skill_improver_digest: ConcreteDigest
264
+ minimum_host_hermes_version: Version
265
+ maximum_tested_host_hermes_version: Version
266
+ subject_hermes_version: Version
267
+ publication: PublicationCoordinates
268
+
269
+
270
+ class ReleaseInputs(ProtocolModel):
271
+ """The founder-owned half of a ReleaseCore. Spec section 4.5.
272
+
273
+ Every value here is a decision a person makes and freezes — the published
274
+ version, the release identifier, the Skill artifacts, the host Hermes
275
+ range. None of them can be derived from this repository, so they are held
276
+ in one hand-edited file that the generator reads, rather than passed as
277
+ command options that would leave the generated document unreproducible.
278
+
279
+ The remaining ReleaseCore coordinates are absent on purpose. A release
280
+ input file that tried to state the engine digest or the protocol version
281
+ would be asserting something the source tree already answers.
282
+ """
283
+
284
+ schema_version: Literal["techtree.release-inputs.v1"]
285
+ release_id: ReleaseId
286
+ cli_version: Version
287
+ intro_climb_reference: NonEmptyString
288
+ starter_skill_digest: ConcreteDigest
289
+ starter_skill_object_url: ObjectUrl
290
+ skill_improver_digest: ConcreteDigest
291
+ minimum_host_hermes_version: Version
292
+ maximum_tested_host_hermes_version: Version
293
+ publication: PublicationCoordinates
@@ -0,0 +1,109 @@
1
+ """Which commit a built artifact came from. Decisions document 0026.
2
+
3
+ An artifact never describes its own identity. The ReleaseCore is written before
4
+ the wheel exists and cannot name the commit the wheel is built from without
5
+ inventing it, so the commit is *stamped onto the wheel while the wheel is being
6
+ built* and read back out of it here.
7
+
8
+ The stamp is one small document, ``build-provenance.json``, written into the
9
+ package's release resources by the build hook in ``tools/stamp_provenance.py``
10
+ and never present in the committed tree. Two properties of that hook are what
11
+ make this module's answer worth anything, and both live there rather than here:
12
+ the commit it writes is the commit the packaged sources are, and a build that
13
+ cannot establish that fails instead of guessing.
14
+
15
+ Reading it back has three possible answers and only two of them are values. A
16
+ wheel carries the stamp. A source checkout — an editable install, the test
17
+ suite, ``uv run techtree`` — is not a built artifact and carries none, and says
18
+ so rather than reporting a commit nobody stamped.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import zipfile
24
+ from pathlib import Path
25
+ from typing import Annotated, Final, Literal
26
+
27
+ from pydantic import StringConstraints
28
+
29
+ from techtree.errors import ValidationError
30
+ from techtree.models.base import ProtocolModel
31
+ from techtree.release.document import (
32
+ packaged_release_root,
33
+ render_document,
34
+ )
35
+
36
+ __all__ = [
37
+ "BUILD_PROVENANCE_FILENAME",
38
+ "BUILD_PROVENANCE_SCHEMA_VERSION",
39
+ "COMMIT_PATTERN",
40
+ "BuildProvenance",
41
+ "Commit",
42
+ "packaged_build_provenance",
43
+ "parse_build_provenance",
44
+ "render_build_provenance",
45
+ "wheel_build_provenance",
46
+ ]
47
+
48
+ #: Bumped only if the stamped shape changes. The website, the plugin, and the
49
+ #: bootstrap checker all read it.
50
+ BUILD_PROVENANCE_SCHEMA_VERSION: Final = "techtree.build-provenance.v1"
51
+
52
+ #: The stamped file, wherever it is read from: the installed package, or a
53
+ #: wheel that has not been installed yet.
54
+ BUILD_PROVENANCE_FILENAME: Final = "build-provenance.json"
55
+
56
+ #: A full git commit, lowercase, never abbreviated. Spec section 9.3.2 pins
57
+ #: exact commits, and an abbreviation is not one.
58
+ COMMIT_PATTERN: Final = r"^[0-9a-f]{40}$"
59
+
60
+ type Commit = Annotated[str, StringConstraints(pattern=COMMIT_PATTERN)]
61
+
62
+ #: Where the stamp sits inside a wheel, which stores the package tree flat
63
+ #: under its import name.
64
+ _WHEEL_MEMBER: Final = f"techtree/resources/release/{BUILD_PROVENANCE_FILENAME}"
65
+
66
+
67
+ class BuildProvenance(ProtocolModel):
68
+ """The commit one built artifact was built from, and nothing else."""
69
+
70
+ schema_version: Literal["techtree.build-provenance.v1"]
71
+ source_commit: Commit
72
+
73
+
74
+ def render_build_provenance(provenance: BuildProvenance) -> bytes:
75
+ """Return the one byte spelling of a stamp.
76
+
77
+ The same commit produces the same bytes, which is what keeps two builds of
78
+ one commit byte-identical.
79
+ """
80
+ return render_document(provenance.model_dump(mode="json"))
81
+
82
+
83
+ def parse_build_provenance(raw: bytes) -> BuildProvenance:
84
+ """Load and validate a stamp from stored bytes."""
85
+ try:
86
+ return BuildProvenance.model_validate_json(raw)
87
+ except ValueError as error:
88
+ raise ValidationError(
89
+ f"this is not a valid build provenance stamp: {error}",
90
+ code="build_provenance_invalid",
91
+ ) from error
92
+
93
+
94
+ def packaged_build_provenance() -> BuildProvenance | None:
95
+ """Return what this build was stamped with, or None if it is not a build."""
96
+ stamp = packaged_release_root() / BUILD_PROVENANCE_FILENAME
97
+ if not stamp.is_file():
98
+ return None
99
+ return parse_build_provenance(stamp.read_bytes())
100
+
101
+
102
+ def wheel_build_provenance(wheel: Path) -> BuildProvenance | None:
103
+ """Return what a wheel on disk was stamped with, without installing it."""
104
+ with zipfile.ZipFile(wheel) as archive:
105
+ try:
106
+ raw = archive.read(_WHEEL_MEMBER)
107
+ except KeyError:
108
+ return None
109
+ return parse_build_provenance(raw)
@@ -0,0 +1 @@
1
+ {"agents":{"subject":{"harness":{"id":"hermes-agent","skills":[],"use_bundled_skill":false,"version":"0.19.0"},"model":{"credential_env":"PRIME_API_KEY","model_id":"qwen/qwen3.7-flash","provider":"prime","revision":null},"runtime":{"cpu":2,"image":"python@sha256:90744cff8f32887f075c47d747a173ff333e9e98801667af93c357fa9f5e28ff","image_platform_digests":{"linux/amd64":"sha256:78b39ef14d8e2b4d71f8dc304f1328c37df95fe0ef99477c2ae6bd3d03784553","linux/arm64":"sha256:20eadabc42589e6543b24a64ab305b9895e9fcf6dbb2cadb14812f394ecdbadf"},"memory_gb":4,"network_policy":"restricted","supported_platforms":["linux/amd64","linux/arm64"],"type":"docker"},"sampling":{"max_tokens":4096,"temperature":0},"trainable":false}},"budgets":{"maximum_input_tokens":900000,"maximum_model_calls":44,"maximum_output_tokens":16000,"maximum_usd":2.5},"context":{"outcome_contract_digest":null,"program_ref":null},"data_policy_digest":"sha256:6c532a43d595286a08260481890bbbffa16d1b4dd89465d1cc8395099d9ebcf9","environment":{"id":"single-agent"},"evaluation_backend":{"attestation":"participant","executor_identity":null,"kind":"local_techtree","provider_run_ref":null,"schema_version":"techtree.evaluation-backend.v1alpha1","workspace_ref":null},"evidence":{"runtime_evidence":"not_required","verifiers_episode":"required"},"execution":{"max_concurrent":4,"order":"parallel_variants","retry_limit":0,"timeout_seconds":600},"kind":"Campaign","metadata":{"id":"campaign_4e8e01a28126c92a906b5cf10b46ee21","purpose":"component_uplift","version":1},"mutation_contract":{"allowed_differences":["/agents/subject/harness/skills"],"kind":"skill_insertion","maximum_skills":1,"minimum_skills":1,"target_agent":"subject"},"schema_version":"techtree.campaign.v1alpha1","scoring":{"aggregation":"mean","minimum_absolute_delta":0,"primary_reward":"exact_match","require_candidate_above_baseline":true},"taskset":{"membership":{"membership_digest":"sha256:56f697fb182cc31691c2e1fff20471acbd549a06e37232227c9491c03aa8f9aa","mode":"committed","ordered_task_hashes":["sha256:af8a43bbaf38d332131d73ef9a5b995d9f330fdc37a777a12bef7c51c9fa0ed1","sha256:e9a51458548dcfb10572fd9f032f00abdb298425114f68acdc074faa79229232","sha256:bafa30550e9c0b09281b3b4b99cb467b9522babc9c2b9d97fee93abef26933b0","sha256:1e74b2f5d62a6b25288d1d8ea3f85b6a0daf4a7697fe36d3f54b8534bb67b40d","sha256:c05fd9db26808494349e9292ecde51a2c6b19e6d87e6f7d7080e2089829504f5","sha256:02725a29cc06f2c883ad17e11a7fc2a09bd2b214df08f3c5c79e4941b54e315e","sha256:206eb5646df5b41b67d28f0e42d370ceb3eb0d72a78cea97e5bbecb7e3841a9f","sha256:c600be21a92a8c5401eaf645b8591b09f34052afe2ab61898d3be2edf8027cad","sha256:b11c2add8339b16dbe85804f9b046e203a0b05db2b7fe9412a4dc45172c6555b","sha256:bd835a69bbeb90d2b1c529aef6bac0c47a8b83750967bf4b64373a90540a07a4","sha256:0a5bde37b05e74be39c62f4418d4812a123d627bc627591fb02dde124b246426","sha256:f15ce47698160cd33fe7a0eb5e40a4cc7977624dd2bdf481ffee69b6204f26b3","sha256:4fb4ddcab900a74eb7cb6ff6a730cc07b5a244535f40f6ddb6aa726cb2796596","sha256:84d83849562386b699e048a91c5671e0f55b49098c9dfac9711a0895766caef3","sha256:e08bb7c7a18a57a83d2ad8dba412c416a8746f708f54b8a29efc0355cabbdb45","sha256:775f95710c5b09fd79e5720554b764f8a1c938063e5743977efe9a4f9d6dc12b","sha256:832e70ff2ab29bfc83da704344417f0c9be2af2935247178ecd1cedc00477097","sha256:5ef9194a49f5ada8a059b52cee70090a53df2bd5b415bf9ed8533287f3020dfc","sha256:b195109ade431bf058e12c4ae1dff6c7bf05d4770be9af9ed27d8f588e95c910","sha256:67eae183c9a688ab5172104c8f8e6380a3c052d367e5bd05964618f5f575bcd8","sha256:cb5651849ccdc4309b9931918392831a2afc7d1b82e6a976a8777a23e73057aa","sha256:391c080f51aa41130367d01e43ff21f80ef915274dd19b2f1a4aaf76380f8f13","sha256:6c39dc6c8c62d0b471e3e40cc560d6cf105632c62e40de8ab21f00e1f3dcd8dc","sha256:f5d73f388d604fc4df9afe63c70ca81f22646faad2bae99e24b1be0b5b6e239a","sha256:c6bb62c1b970b25d39823748dd9cb087884cfb7360668f8c405d870af26e223e","sha256:23c6f7a31ede5aa224788e4408ae1462ca57c559175232d740de6a139b02a5b4","sha256:13fe52ddfdeab29466e3ed21db540a08d169e09d338e86852f96387f2272795d","sha256:702eb0abb56d4554d7e9b8d75f15cf3d7ecfc0c8c9f83cf1c6c3caa25c5865f5","sha256:aee7f9a4999dce081dbd0ce6ab65db37ffe9bb88d9b4a07b944f376fef0a14ad","sha256:4b6a3d718933e312c05adff201cd14ab1d58b0eb5c0a0d32faccedc09cb9af68","sha256:8b5ae57c98f3963e91dd611d4cfc37da07c80d4b3c0091ed7b4a76175376381a","sha256:98f86f8d3663dcf3d0f32b6b1f9ef44913bb144355ecbcd89b8e4c96337b6964","sha256:3efcd8d42d8adfce2038f5e5d8c88c3ec1d30d6ae3379c4727ea711b28e1929d","sha256:bd2815069c78bced9ef23d36770c941b27753dad15db3e6c3a90c197181202a1","sha256:08c8263a76b1d93beb586c8c39f0469721455cd08d517b15bcf827cf09ac0755","sha256:0777a89a3da39284a1765937c3ce1cea7af2ac27a98ffee32e86a2a1fccca9b5"]},"ref":{"config":{},"id":"procedure-transfer-v1","kind":"verifiers","package":{"digest":"sha256:f42a0d5e968478af40c703645d247ef043f115f6801d3b497eca5fab47261a7c","kind":"embedded","name":"procedure-transfer-v1","revision":"0.1.0"}},"selection":{"num_rollouts":1,"num_tasks":36,"shuffle":false},"validation_receipt_digest":"sha256:4944bd71caa1a295e03325b18a7af753d0d8fcf787189c89244209171cda1302"}}
@@ -0,0 +1,32 @@
1
+ {
2
+ "climbs": [
3
+ {
4
+ "digest": "sha256:a3a5e9c5f9b40d4f08fad54852377e201fd0d6dd4acfa4c565a0edfac324a236",
5
+ "path": "climbs/hello-world-climb.json",
6
+ "reference": "hello-world-climb@1"
7
+ }
8
+ ],
9
+ "objects": {
10
+ "sha256:4944bd71caa1a295e03325b18a7af753d0d8fcf787189c89244209171cda1302": {
11
+ "kind": "taskset_validation",
12
+ "media_type": "application/json",
13
+ "path": "taskset-validations/hello-world-climb.json"
14
+ },
15
+ "sha256:6c532a43d595286a08260481890bbbffa16d1b4dd89465d1cc8395099d9ebcf9": {
16
+ "kind": "data_policy",
17
+ "media_type": "application/json",
18
+ "path": "data-policies/hello-world-climb.json"
19
+ },
20
+ "sha256:991be5e42acbd48c6bd9aaf8e1f5a5a2478c0a0e4bf04ad4f2be84b702adee43": {
21
+ "kind": "validation_evidence",
22
+ "media_type": "application/json",
23
+ "path": "validation-evidence/hello-world-climb.json"
24
+ },
25
+ "sha256:ebf029abb266ca74c2def50eb23030511bab0e929c6bf4a68691f9b5afd554b1": {
26
+ "kind": "campaign",
27
+ "media_type": "application/json",
28
+ "path": "campaigns/hello-world-climb.json"
29
+ }
30
+ },
31
+ "schema_version": "techtree.catalog.v1alpha1"
32
+ }
@@ -0,0 +1 @@
1
+ {"campaign_spec_digest":"sha256:ebf029abb266ca74c2def50eb23030511bab0e929c6bf4a68691f9b5afd554b1","candidate_policy":{"constraints":{"format":"techtree-instruction-skill-v1","max_skills":1,"min_skills":1},"required_mutation":"skill_insertion","skill_visibility":"public"},"kind":"Climb","leaderboard":{"enabled":false,"evidence_required":"not_required"},"metadata":{"closes_at":null,"id":"climb_8475477d861d884f5dd2c046aeedb025","opens_at":null,"slug":"hello-world-climb","status":"development","summary":"A toy Skill-uplift Climb. It runs the synthetic BranchCode v1 task family twice — once without a Skill, once with one — so you can see what writing a procedure down changes. This is an introductory demonstration of the mechanism, not a measure of broad capability.","title":"Techtree Hello World","version":1},"publication":{"proof_grade":"development_only","public_trace_projection":"redacted","raw_episode_visibility":"prohibited","report_visibility":"public"},"schema_version":"techtree.climb.v1alpha1"}