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.
- techtree/__init__.py +35 -0
- techtree/__main__.py +14 -0
- techtree/canonical.py +239 -0
- techtree/catalog/__init__.py +25 -0
- techtree/catalog/repository.py +400 -0
- techtree/catalog/service.py +419 -0
- techtree/cli/__init__.py +1 -0
- techtree/cli/app.py +416 -0
- techtree/cli/commands/__init__.py +1 -0
- techtree/cli/commands/climb.py +1223 -0
- techtree/cli/commands/doctor.py +147 -0
- techtree/cli/commands/engine.py +207 -0
- techtree/cli/commands/proof.py +556 -0
- techtree/cli/commands/publish.py +447 -0
- techtree/cli/commands/release.py +303 -0
- techtree/cli/commands/run.py +1067 -0
- techtree/cli/commands/setup.py +181 -0
- techtree/cli/commands/skill.py +221 -0
- techtree/cli/commands/uplift.py +698 -0
- techtree/cli/commands/withdraw.py +212 -0
- techtree/cli/confirm.py +47 -0
- techtree/cli/context.py +96 -0
- techtree/cli/invoke.py +220 -0
- techtree/cli/output.py +280 -0
- techtree/constants.py +138 -0
- techtree/crypto.py +128 -0
- techtree/doctor/__init__.py +1 -0
- techtree/doctor/checks.py +675 -0
- techtree/doctor/execution_checks.py +435 -0
- techtree/doctor/service.py +326 -0
- techtree/drafts/__init__.py +32 -0
- techtree/drafts/source.py +146 -0
- techtree/drafts/store.py +992 -0
- techtree/engines/__init__.py +1 -0
- techtree/engines/bundle.py +251 -0
- techtree/engines/installer.py +679 -0
- techtree/engines/registry.py +235 -0
- techtree/engines/runner.py +170 -0
- techtree/errors.py +262 -0
- techtree/fs.py +234 -0
- techtree/harness.py +108 -0
- techtree/identity/__init__.py +41 -0
- techtree/identity/models.py +113 -0
- techtree/identity/service.py +199 -0
- techtree/identity/store.py +263 -0
- techtree/ids.py +85 -0
- techtree/manifests/__init__.py +39 -0
- techtree/manifests/builder.py +433 -0
- techtree/manifests/compare.py +376 -0
- techtree/models/__init__.py +282 -0
- techtree/models/base.py +201 -0
- techtree/models/campaign.py +484 -0
- techtree/models/catalog.py +227 -0
- techtree/models/cli.py +151 -0
- techtree/models/climb.py +254 -0
- techtree/models/data_policy.py +130 -0
- techtree/models/engine.py +156 -0
- techtree/models/episode_receipt.py +130 -0
- techtree/models/evaluation_backend.py +113 -0
- techtree/models/experiment.py +154 -0
- techtree/models/run.py +214 -0
- techtree/models/skill.py +156 -0
- techtree/models/uplift_report.py +158 -0
- techtree/models/validation.py +299 -0
- techtree/paths.py +116 -0
- techtree/presentation/__init__.py +31 -0
- techtree/presentation/build.py +1242 -0
- techtree/presentation/compact.py +246 -0
- techtree/presentation/evidence.py +169 -0
- techtree/presentation/models.py +358 -0
- techtree/presentation/rich.py +312 -0
- techtree/presentation/sanitize.py +156 -0
- techtree/publication/__init__.py +44 -0
- techtree/publication/address.py +180 -0
- techtree/publication/coordinates.py +26 -0
- techtree/publication/journal.py +212 -0
- techtree/publication/keccak.py +183 -0
- techtree/publication/models.py +209 -0
- techtree/publication/offer.py +35 -0
- techtree/publication/service.py +618 -0
- techtree/publication/transport.py +296 -0
- techtree/publication/verify.py +242 -0
- techtree/publication/withdraw.py +156 -0
- techtree/py.typed +0 -0
- techtree/receipts/__init__.py +52 -0
- techtree/receipts/bundle.py +578 -0
- techtree/receipts/compare.py +1065 -0
- techtree/receipts/episode.py +672 -0
- techtree/receipts/execution.py +630 -0
- techtree/receipts/observed.py +474 -0
- techtree/receipts/set.py +336 -0
- techtree/receipts/uplift.py +655 -0
- techtree/receipts/verify.py +1055 -0
- techtree/release/__init__.py +9 -0
- techtree/release/bootstrap.py +509 -0
- techtree/release/checks.py +376 -0
- techtree/release/document.py +125 -0
- techtree/release/generate.py +221 -0
- techtree/release/models.py +293 -0
- techtree/release/provenance.py +109 -0
- techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
- techtree/resources/catalog/catalog.json +32 -0
- techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
- techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
- techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
- techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
- techtree/resources/engines/default/engine.json +20 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
- techtree/resources/engines/default/pyproject.toml +23 -0
- techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
- techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
- techtree/resources/engines/default/tools/normalize_validation.py +222 -0
- techtree/resources/engines/default/uv.lock +1758 -0
- techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
- techtree/resources/release/build-provenance.json +4 -0
- techtree/resources/release/release-core.json +24 -0
- techtree/runs/__init__.py +31 -0
- techtree/runs/artifacts.py +750 -0
- techtree/runs/child_registry.py +228 -0
- techtree/runs/events.py +478 -0
- techtree/runs/executor.py +140 -0
- techtree/runs/fake.py +741 -0
- techtree/runs/launcher.py +253 -0
- techtree/runs/machine.py +489 -0
- techtree/runs/real.py +789 -0
- techtree/runs/service.py +616 -0
- techtree/runs/store.py +555 -0
- techtree/runs/validation.py +259 -0
- techtree/runs/variants.py +684 -0
- techtree/settings.py +143 -0
- techtree/skills/__init__.py +14 -0
- techtree/skills/archive.py +282 -0
- techtree/skills/policy.py +62 -0
- techtree/skills/scanner.py +394 -0
- techtree/skills/service.py +752 -0
- techtree/skills/starter.py +434 -0
- techtree/tasksets/__init__.py +1 -0
- techtree/tasksets/membership.py +269 -0
- techtree/tasksets/provider.py +207 -0
- techtree/tasksets/resolver.py +311 -0
- techtree/tasksets/service.py +484 -0
- techtree/tasksets/verifiers_cli.py +538 -0
- techtree/uplift/__init__.py +20 -0
- techtree/uplift/context.py +544 -0
- techtree/uplift/derive.py +203 -0
- techtree/uplift/public_tasks.py +151 -0
- techtree/uplift/service.py +719 -0
- techtree/uplift/source.py +160 -0
- techtree/verifiers/__init__.py +31 -0
- techtree/verifiers/budget.py +219 -0
- techtree/verifiers/child.py +633 -0
- techtree/verifiers/compiler.py +432 -0
- techtree/verifiers/config.py +365 -0
- techtree/verifiers/credentials.py +321 -0
- techtree/verifiers/image.py +126 -0
- techtree/verifiers/models.py +527 -0
- techtree/verifiers/outputs.py +368 -0
- techtree/verifiers/progress.py +192 -0
- techtree/verifiers/supervisor.py +341 -0
- techtree/verifiers/verify.py +782 -0
- techtree/version.py +39 -0
- techtree/worker/__init__.py +18 -0
- techtree/worker/execute.py +487 -0
- techtree/worker/main.py +57 -0
- techtree-0.1.0.dist-info/METADATA +344 -0
- techtree-0.1.0.dist-info/RECORD +174 -0
- techtree-0.1.0.dist-info/WHEEL +4 -0
- techtree-0.1.0.dist-info/entry_points.txt +3 -0
- techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Whether one release still agrees with itself. Spec sections 9.5 and 9.7.
|
|
2
|
+
|
|
3
|
+
A ReleaseCore is a set of claims about things that live elsewhere: an engine
|
|
4
|
+
bundle, a catalog index, an installed package, a Climb, two Skills. Each
|
|
5
|
+
claim is checked here against the thing it names, one check per claim, so that
|
|
6
|
+
a failure says which coordinate drifted rather than that "the release is
|
|
7
|
+
broken".
|
|
8
|
+
|
|
9
|
+
Two verdicts are not enough. A check that could not run is not a check that
|
|
10
|
+
passed, and reporting it as one is how a release ships with something nobody
|
|
11
|
+
looked at. So a check is ``passed``, ``failed``, or ``skipped``, and a
|
|
12
|
+
verification is verified when nothing failed — with every skip still visible in
|
|
13
|
+
the output, named and explained.
|
|
14
|
+
|
|
15
|
+
Two coordinates cannot be settled from inside an installed CLI at all: the two
|
|
16
|
+
founder Skill digests, whose bytes live with the plugin and the website, and
|
|
17
|
+
the address the starter Skill is published at, which a verification that
|
|
18
|
+
contacts nothing must not go and fetch. Those are reported as skips with their
|
|
19
|
+
values in the detail, so a reader sees the coordinate and sees that nobody
|
|
20
|
+
here checked it.
|
|
21
|
+
|
|
22
|
+
Every coordinate a release names is a real one (decisions 0026), so there is
|
|
23
|
+
nothing here that treats a value as provisional: a document that parsed is a
|
|
24
|
+
document whose every coordinate was chosen, and each one is compared against
|
|
25
|
+
the thing it names.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections.abc import Mapping
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from typing import Final, Literal, Self
|
|
33
|
+
|
|
34
|
+
from pydantic import model_validator
|
|
35
|
+
|
|
36
|
+
from techtree.catalog.repository import EmbeddedCatalogRepository
|
|
37
|
+
from techtree.engines.bundle import engine_bundle_digest
|
|
38
|
+
from techtree.errors import ValidationError
|
|
39
|
+
from techtree.models.base import Digest, NonEmptyString, ProtocolModel
|
|
40
|
+
from techtree.release.document import (
|
|
41
|
+
document_digest,
|
|
42
|
+
is_canonical_document,
|
|
43
|
+
parse_release_core,
|
|
44
|
+
)
|
|
45
|
+
from techtree.release.generate import ReleaseSources
|
|
46
|
+
from techtree.release.models import ReleaseCore
|
|
47
|
+
from techtree.version import package_version
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"RELEASE_COORDINATE_MISMATCH",
|
|
51
|
+
"RELEASE_CORE_DIGEST_MISMATCH",
|
|
52
|
+
"RELEASE_CORE_INVALID",
|
|
53
|
+
"RELEASE_CORE_NOT_CANONICAL",
|
|
54
|
+
"ReleaseCheck",
|
|
55
|
+
"ReleaseFacts",
|
|
56
|
+
"ReleaseVerification",
|
|
57
|
+
"local_release_facts",
|
|
58
|
+
"verify_release_core",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
#: Stable failure codes. A caller branches on these, never on wording.
|
|
62
|
+
RELEASE_CORE_INVALID: Final = "release_core_invalid"
|
|
63
|
+
RELEASE_CORE_NOT_CANONICAL: Final = "release_core_not_canonical"
|
|
64
|
+
RELEASE_CORE_DIGEST_MISMATCH: Final = "release_core_digest_mismatch"
|
|
65
|
+
RELEASE_COORDINATE_MISMATCH: Final = "release_coordinate_mismatch"
|
|
66
|
+
|
|
67
|
+
#: Why a check could not be run. Never a failure and never a pass.
|
|
68
|
+
RELEASE_CHECK_NOT_APPLICABLE: Final = "release_check_not_applicable"
|
|
69
|
+
|
|
70
|
+
type ReleaseCheckStatus = Literal["passed", "failed", "skipped"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ReleaseCheck(ProtocolModel):
|
|
74
|
+
"""One named claim, and what comparing it against reality found."""
|
|
75
|
+
|
|
76
|
+
id: NonEmptyString
|
|
77
|
+
status: ReleaseCheckStatus
|
|
78
|
+
code: NonEmptyString
|
|
79
|
+
detail: NonEmptyString
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ReleaseVerification(ProtocolModel):
|
|
83
|
+
"""Every check a release verification ran, and its single verdict."""
|
|
84
|
+
|
|
85
|
+
verified: bool
|
|
86
|
+
checks: list[ReleaseCheck]
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def failures(self) -> list[ReleaseCheck]:
|
|
90
|
+
"""Return every check that failed."""
|
|
91
|
+
return [check for check in self.checks if check.status == "failed"]
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def skipped(self) -> list[ReleaseCheck]:
|
|
95
|
+
"""Return every check that could not be run."""
|
|
96
|
+
return [check for check in self.checks if check.status == "skipped"]
|
|
97
|
+
|
|
98
|
+
@model_validator(mode="after")
|
|
99
|
+
def _check_verdict_follows_the_checks(self) -> Self:
|
|
100
|
+
"""Refuse a verdict the checks do not support."""
|
|
101
|
+
if self.verified != (not self.failures):
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"a release verifies exactly when no check failed; this one "
|
|
104
|
+
f"claims verified={self.verified} with {len(self.failures)} "
|
|
105
|
+
"failed checks"
|
|
106
|
+
)
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class ReleaseFacts:
|
|
112
|
+
"""What this machine and this source tree actually contain.
|
|
113
|
+
|
|
114
|
+
Plain values rather than live objects, so that a verification is a pure
|
|
115
|
+
comparison and a test can break exactly one fact at a time.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
package_version: str
|
|
119
|
+
protocol_version: str
|
|
120
|
+
engine_digest: Digest
|
|
121
|
+
catalog_digest: Digest
|
|
122
|
+
climb_references: tuple[str, ...]
|
|
123
|
+
subject_hermes_versions: Mapping[str, str]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def local_release_facts(sources: ReleaseSources) -> ReleaseFacts:
|
|
127
|
+
"""Read the facts a release is checked against out of one source tree."""
|
|
128
|
+
catalog = EmbeddedCatalogRepository(sources.catalog_root)
|
|
129
|
+
references = tuple(catalog.list_climb_references())
|
|
130
|
+
harnesses: dict[str, str] = {}
|
|
131
|
+
for reference in references:
|
|
132
|
+
climb = catalog.load_climb(reference)
|
|
133
|
+
campaign = catalog.load_campaign(climb.campaign_spec_digest)
|
|
134
|
+
harnesses[reference] = campaign.subject.harness.version
|
|
135
|
+
|
|
136
|
+
return ReleaseFacts(
|
|
137
|
+
package_version=package_version(),
|
|
138
|
+
protocol_version=sources.protocol_version,
|
|
139
|
+
engine_digest=engine_bundle_digest(sources.engine_root),
|
|
140
|
+
catalog_digest=document_digest(sources.catalog_index_bytes()),
|
|
141
|
+
climb_references=references,
|
|
142
|
+
subject_hermes_versions=harnesses,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def verify_release_core(
|
|
147
|
+
raw: bytes,
|
|
148
|
+
facts: ReleaseFacts,
|
|
149
|
+
*,
|
|
150
|
+
expected_digest: Digest | None = None,
|
|
151
|
+
) -> ReleaseVerification:
|
|
152
|
+
"""Check a stored ReleaseCore against the build it claims to describe."""
|
|
153
|
+
checks = [_document_check(raw)]
|
|
154
|
+
if checks[0].status == "failed":
|
|
155
|
+
# Nothing below can be compared against a document that is not one.
|
|
156
|
+
return _verification(checks)
|
|
157
|
+
|
|
158
|
+
core = parse_release_core(raw)
|
|
159
|
+
checks.extend(
|
|
160
|
+
[
|
|
161
|
+
_canonical_bytes_check(raw),
|
|
162
|
+
_digest_check(raw, expected_digest),
|
|
163
|
+
_cli_version_check(core, facts),
|
|
164
|
+
_protocol_version_check(core, facts),
|
|
165
|
+
_engine_digest_check(core, facts),
|
|
166
|
+
_catalog_digest_check(core, facts),
|
|
167
|
+
_intro_climb_check(core, facts),
|
|
168
|
+
_subject_hermes_check(core, facts),
|
|
169
|
+
*_founder_skill_checks(core),
|
|
170
|
+
_starter_skill_source_check(core),
|
|
171
|
+
]
|
|
172
|
+
)
|
|
173
|
+
return _verification(checks)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# The checks
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _document_check(raw: bytes) -> ReleaseCheck:
|
|
182
|
+
try:
|
|
183
|
+
core = parse_release_core(raw)
|
|
184
|
+
except ValidationError as error:
|
|
185
|
+
return _failed("release_core_document", RELEASE_CORE_INVALID, str(error))
|
|
186
|
+
|
|
187
|
+
return _passed(
|
|
188
|
+
"release_core_document",
|
|
189
|
+
f"the ReleaseCore is valid: release {core.release_id}, with every "
|
|
190
|
+
"coordinate concrete.",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _canonical_bytes_check(raw: bytes) -> ReleaseCheck:
|
|
195
|
+
if is_canonical_document(raw):
|
|
196
|
+
return _passed(
|
|
197
|
+
"release_core_canonical_bytes",
|
|
198
|
+
"the stored bytes are in the one published spelling.",
|
|
199
|
+
)
|
|
200
|
+
return _failed(
|
|
201
|
+
"release_core_canonical_bytes",
|
|
202
|
+
RELEASE_CORE_NOT_CANONICAL,
|
|
203
|
+
"the stored bytes are not in the published spelling, so their digest "
|
|
204
|
+
"is not the digest the generator would produce; regenerate rather "
|
|
205
|
+
"than edit this file.",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _digest_check(raw: bytes, expected: Digest | None) -> ReleaseCheck:
|
|
210
|
+
actual = document_digest(raw)
|
|
211
|
+
if expected is None:
|
|
212
|
+
return _skipped(
|
|
213
|
+
"release_core_digest",
|
|
214
|
+
f"no expected digest was given; these bytes are {actual}.",
|
|
215
|
+
)
|
|
216
|
+
if actual == expected:
|
|
217
|
+
return _passed(
|
|
218
|
+
"release_core_digest",
|
|
219
|
+
f"the embedded ReleaseCore is {actual}, as expected.",
|
|
220
|
+
)
|
|
221
|
+
return _failed(
|
|
222
|
+
"release_core_digest",
|
|
223
|
+
RELEASE_CORE_DIGEST_MISMATCH,
|
|
224
|
+
f"the embedded ReleaseCore is {actual}, not the expected {expected}.",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _cli_version_check(core: ReleaseCore, facts: ReleaseFacts) -> ReleaseCheck:
|
|
229
|
+
if core.cli_version == facts.package_version:
|
|
230
|
+
return _passed(
|
|
231
|
+
"cli_version",
|
|
232
|
+
f"the installed package is {facts.package_version}, the version "
|
|
233
|
+
"this release names.",
|
|
234
|
+
)
|
|
235
|
+
return _failed(
|
|
236
|
+
"cli_version",
|
|
237
|
+
RELEASE_COORDINATE_MISMATCH,
|
|
238
|
+
f"this release names CLI version {core.cli_version}, but the "
|
|
239
|
+
f"installed package is {facts.package_version}.",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _protocol_version_check(core: ReleaseCore, facts: ReleaseFacts) -> ReleaseCheck:
|
|
244
|
+
return _equality(
|
|
245
|
+
"protocol_version",
|
|
246
|
+
claimed=core.protocol_version,
|
|
247
|
+
actual=facts.protocol_version,
|
|
248
|
+
subject="the protocol version",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _engine_digest_check(core: ReleaseCore, facts: ReleaseFacts) -> ReleaseCheck:
|
|
253
|
+
return _equality(
|
|
254
|
+
"engine_digest",
|
|
255
|
+
claimed=core.engine_digest,
|
|
256
|
+
actual=facts.engine_digest,
|
|
257
|
+
subject="the managed engine bundle",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _catalog_digest_check(core: ReleaseCore, facts: ReleaseFacts) -> ReleaseCheck:
|
|
262
|
+
return _equality(
|
|
263
|
+
"catalog_digest",
|
|
264
|
+
claimed=core.catalog_digest,
|
|
265
|
+
actual=facts.catalog_digest,
|
|
266
|
+
subject="the catalog index",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _intro_climb_check(core: ReleaseCore, facts: ReleaseFacts) -> ReleaseCheck:
|
|
271
|
+
if core.intro_climb_reference in facts.climb_references:
|
|
272
|
+
return _passed(
|
|
273
|
+
"intro_climb_reference",
|
|
274
|
+
f"this build ships {core.intro_climb_reference}, the introductory "
|
|
275
|
+
"Climb the release names.",
|
|
276
|
+
)
|
|
277
|
+
return _failed(
|
|
278
|
+
"intro_climb_reference",
|
|
279
|
+
RELEASE_COORDINATE_MISMATCH,
|
|
280
|
+
f"this release leads with {core.intro_climb_reference}, which this "
|
|
281
|
+
f"build does not ship; it ships {list(facts.climb_references)}.",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _subject_hermes_check(core: ReleaseCore, facts: ReleaseFacts) -> ReleaseCheck:
|
|
286
|
+
pinned = facts.subject_hermes_versions.get(core.intro_climb_reference)
|
|
287
|
+
if pinned is None:
|
|
288
|
+
return _failed(
|
|
289
|
+
"subject_hermes_version",
|
|
290
|
+
RELEASE_COORDINATE_MISMATCH,
|
|
291
|
+
"the subject harness version cannot be compared because this "
|
|
292
|
+
f"build ships no Climb called {core.intro_climb_reference}.",
|
|
293
|
+
)
|
|
294
|
+
return _equality(
|
|
295
|
+
"subject_hermes_version",
|
|
296
|
+
claimed=core.subject_hermes_version,
|
|
297
|
+
actual=pinned,
|
|
298
|
+
subject="the subject harness the introductory Campaign pins",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _founder_skill_checks(core: ReleaseCore) -> list[ReleaseCheck]:
|
|
303
|
+
"""Report each founder Skill digest, which this package cannot resolve.
|
|
304
|
+
|
|
305
|
+
The CLI ships no Skill bytes — the starter Skill is served by the website
|
|
306
|
+
and the operator Skill is bundled by the plugin — so each digest is checked
|
|
307
|
+
where those bytes are, not here (spec section 9.6).
|
|
308
|
+
"""
|
|
309
|
+
return [
|
|
310
|
+
_skipped(
|
|
311
|
+
field,
|
|
312
|
+
f"this release binds {field.removesuffix('_digest')} to "
|
|
313
|
+
f"{getattr(core, field)}; the CLI package carries no Skill bytes, "
|
|
314
|
+
"so the plugin and the website verify it against the Skill they "
|
|
315
|
+
"serve.",
|
|
316
|
+
)
|
|
317
|
+
for field in ("starter_skill_digest", "skill_improver_digest")
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _starter_skill_source_check(core: ReleaseCore) -> ReleaseCheck:
|
|
322
|
+
"""Report where the starter Skill is published, without going to look.
|
|
323
|
+
|
|
324
|
+
``release verify`` contacts nothing, which is the property that lets a host
|
|
325
|
+
agent run it on a machine that has just been installed and may not be
|
|
326
|
+
online. So a bound address is reported rather than resolved: what the bytes
|
|
327
|
+
at the other end have to be is already settled by the digest beside it, and
|
|
328
|
+
that comparison happens where the fetch does (spec section 10.5).
|
|
329
|
+
"""
|
|
330
|
+
return _skipped(
|
|
331
|
+
"starter_skill_object_url",
|
|
332
|
+
f"this release publishes the starter Skill at "
|
|
333
|
+
f"{core.starter_skill_object_url}; nothing is fetched here, and what "
|
|
334
|
+
"is served is checked against the digest when it is obtained.",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# Building checks
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _equality(
|
|
344
|
+
identifier: str, *, claimed: str, actual: str, subject: str
|
|
345
|
+
) -> ReleaseCheck:
|
|
346
|
+
if claimed == actual:
|
|
347
|
+
return _passed(identifier, f"{subject} is {actual}, as the release says.")
|
|
348
|
+
return _failed(
|
|
349
|
+
identifier,
|
|
350
|
+
RELEASE_COORDINATE_MISMATCH,
|
|
351
|
+
f"the release says {subject} is {claimed}; it is {actual}.",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _passed(identifier: str, detail: str) -> ReleaseCheck:
|
|
356
|
+
return ReleaseCheck(id=identifier, status="passed", code="ok", detail=detail)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _failed(identifier: str, code: str, detail: str) -> ReleaseCheck:
|
|
360
|
+
return ReleaseCheck(id=identifier, status="failed", code=code, detail=detail)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _skipped(identifier: str, detail: str) -> ReleaseCheck:
|
|
364
|
+
return ReleaseCheck(
|
|
365
|
+
id=identifier,
|
|
366
|
+
status="skipped",
|
|
367
|
+
code=RELEASE_CHECK_NOT_APPLICABLE,
|
|
368
|
+
detail=detail,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _verification(checks: list[ReleaseCheck]) -> ReleaseVerification:
|
|
373
|
+
return ReleaseVerification(
|
|
374
|
+
verified=not any(check.status == "failed" for check in checks),
|
|
375
|
+
checks=checks,
|
|
376
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""The bytes of a release document, and the digest taken over them.
|
|
2
|
+
|
|
3
|
+
Spec sections 9.3.1 and 9.5.
|
|
4
|
+
|
|
5
|
+
Every other digest in Techtree is taken over the RFC 8785 canonical form of a
|
|
6
|
+
parsed object, because the object is what two independent builds have to agree
|
|
7
|
+
about. A ReleaseCore is different in one respect that decides this module: the
|
|
8
|
+
specification requires *identical bytes* in the CLI package, in the plugin
|
|
9
|
+
repository, and in the website wrapper, and two of those three consumers are
|
|
10
|
+
not Python. So the ReleaseCore digest is the SHA-256 of the file, exactly as
|
|
11
|
+
the file is stored, which is the same construction the website already uses for
|
|
12
|
+
the catalog index and is checkable with ``shasum`` in any repository.
|
|
13
|
+
|
|
14
|
+
That only works if the file has one spelling, so this module owns it: keys
|
|
15
|
+
sorted, two-space indent, no ASCII escaping, one trailing newline. It is the
|
|
16
|
+
house style of every other generated JSON document in the tree, and
|
|
17
|
+
:func:`is_canonical_document` lets a verification prove a stored file is
|
|
18
|
+
still in it — a hand-edit that changes nothing but whitespace still changes the
|
|
19
|
+
digest, and that has to be reported as tampering rather than tolerated.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
from importlib.resources import files as resource_files
|
|
27
|
+
from importlib.resources.abc import Traversable
|
|
28
|
+
from typing import Final
|
|
29
|
+
|
|
30
|
+
from techtree.canonical import sha256_digest_bytes
|
|
31
|
+
from techtree.errors import ValidationError
|
|
32
|
+
from techtree.models.base import Digest, JsonValue
|
|
33
|
+
from techtree.release.models import ReleaseCore, ReleaseInputs
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"RELEASE_CORE_FILENAME",
|
|
37
|
+
"RELEASE_RESOURCE_DIRECTORY",
|
|
38
|
+
"document_digest",
|
|
39
|
+
"is_canonical_document",
|
|
40
|
+
"packaged_release_core_bytes",
|
|
41
|
+
"packaged_release_root",
|
|
42
|
+
"parse_release_core",
|
|
43
|
+
"parse_release_inputs",
|
|
44
|
+
"render_document",
|
|
45
|
+
"render_release_core",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
#: The file, wherever it is stored. The plugin repository and the website use
|
|
49
|
+
#: the same name, so a person moving bytes between repositories never has to
|
|
50
|
+
#: translate.
|
|
51
|
+
RELEASE_CORE_FILENAME: Final = "release-core.json"
|
|
52
|
+
|
|
53
|
+
#: Where the shipped copy lives inside the installed distribution.
|
|
54
|
+
RELEASE_RESOURCE_DIRECTORY: Final = "release"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render_document(payload: Mapping[str, JsonValue]) -> bytes:
|
|
58
|
+
"""Return the one byte spelling of a release document."""
|
|
59
|
+
text = json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False)
|
|
60
|
+
return f"{text}\n".encode()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_canonical_document(raw: bytes) -> bool:
|
|
64
|
+
"""Return whether these bytes are already in the one stored spelling."""
|
|
65
|
+
try:
|
|
66
|
+
payload = json.loads(raw)
|
|
67
|
+
except ValueError:
|
|
68
|
+
return False
|
|
69
|
+
if not isinstance(payload, dict):
|
|
70
|
+
return False
|
|
71
|
+
return render_document(payload) == raw
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def document_digest(raw: bytes) -> Digest:
|
|
75
|
+
"""Return the digest a release document is published under."""
|
|
76
|
+
return sha256_digest_bytes(raw)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def render_release_core(core: ReleaseCore) -> bytes:
|
|
80
|
+
"""Return the stored bytes of a ReleaseCore."""
|
|
81
|
+
return render_document(core.model_dump(mode="json"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_release_core(raw: bytes) -> ReleaseCore:
|
|
85
|
+
"""Load and validate a ReleaseCore from stored bytes."""
|
|
86
|
+
try:
|
|
87
|
+
return ReleaseCore.model_validate_json(raw)
|
|
88
|
+
except ValueError as error:
|
|
89
|
+
raise ValidationError(
|
|
90
|
+
f"this is not a valid ReleaseCore: {error}",
|
|
91
|
+
code="release_core_invalid",
|
|
92
|
+
) from error
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_release_inputs(raw: bytes) -> ReleaseInputs:
|
|
96
|
+
"""Load and validate the founder-owned release inputs."""
|
|
97
|
+
try:
|
|
98
|
+
return ReleaseInputs.model_validate_json(raw)
|
|
99
|
+
except ValueError as error:
|
|
100
|
+
raise ValidationError(
|
|
101
|
+
f"this is not a valid release input file: {error}",
|
|
102
|
+
code="release_inputs_invalid",
|
|
103
|
+
) from error
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def packaged_release_root() -> Traversable:
|
|
107
|
+
"""Return the release directory shipped inside the installed package."""
|
|
108
|
+
return resource_files("techtree") / "resources" / RELEASE_RESOURCE_DIRECTORY
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def packaged_release_core_bytes() -> bytes:
|
|
112
|
+
"""Return the exact ReleaseCore bytes this build ships.
|
|
113
|
+
|
|
114
|
+
A build with no ReleaseCore is not a release, so the absence is a typed
|
|
115
|
+
failure rather than an empty answer.
|
|
116
|
+
"""
|
|
117
|
+
document = packaged_release_root() / RELEASE_CORE_FILENAME
|
|
118
|
+
if not document.is_file():
|
|
119
|
+
raise ValidationError(
|
|
120
|
+
"this build ships no ReleaseCore; it was not produced by the "
|
|
121
|
+
"release generator",
|
|
122
|
+
code="release_core_missing",
|
|
123
|
+
details={"file": RELEASE_CORE_FILENAME},
|
|
124
|
+
)
|
|
125
|
+
return document.read_bytes()
|