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,9 @@
|
|
|
1
|
+
"""The release document that binds this build to one Climb v0.1 release.
|
|
2
|
+
|
|
3
|
+
Spec sections 6.6, 9.3-9.5 and 9.7. Everything here is about *coordinates*:
|
|
4
|
+
which CLI version, which source commit, which engine, which catalog, which
|
|
5
|
+
Skills. Nothing in this package runs a Climb, calls a model, or writes to the
|
|
6
|
+
network, and nothing outside it is allowed to invent a release coordinate.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""Checking a website bootstrap document from the side that produced it.
|
|
2
|
+
|
|
3
|
+
Spec sections 9.3.2, 9.4 and 9.7.
|
|
4
|
+
|
|
5
|
+
The website publishes a ``BootstrapRelease``: the wrapper that tells a new
|
|
6
|
+
operator which CLI version to install, which plugin commit to install, and
|
|
7
|
+
which Climb to start with. It is generated after this repository's ReleaseCore
|
|
8
|
+
exists, it is served as exact bytes, and the website refuses to import one that
|
|
9
|
+
does not hold together.
|
|
10
|
+
|
|
11
|
+
This module checks the same document from the producing end, and it does that
|
|
12
|
+
for two different reasons that are worth keeping apart.
|
|
13
|
+
|
|
14
|
+
*Does the wrapper still name this release?* The bootstrap and the ReleaseCore
|
|
15
|
+
repeat four coordinates — the CLI version, the minimum host Hermes version, the
|
|
16
|
+
introductory Climb, and the starter Skill. Repeated values drift, and when they
|
|
17
|
+
do the website tells operators to install one thing while the CLI believes
|
|
18
|
+
another. Each repeat is compared individually here so a failure names the
|
|
19
|
+
coordinate.
|
|
20
|
+
|
|
21
|
+
The wrapper also states one coordinate the release document deliberately does
|
|
22
|
+
not carry: which source commit the published wheel was built from. Decisions
|
|
23
|
+
0026 puts that fact where it can be known — stamped into the wheel by the build
|
|
24
|
+
— so the comparison here is against the wheel itself, not against a claim the
|
|
25
|
+
release repeats about itself.
|
|
26
|
+
|
|
27
|
+
The starter Skill is the coordinate with the most to lose. Spec sections 4.1
|
|
28
|
+
and 10.5 make the wrapper the thing that says where the public Skill object is
|
|
29
|
+
served from, and the release document the thing that says which bytes count.
|
|
30
|
+
The wrapper carries both halves of the object: ``file_digest``, the bytes the
|
|
31
|
+
address returns, and ``tree_digest``, the one-file Skill the CLI builds out of
|
|
32
|
+
them and verifies before it runs anything. The address must be keyed by the
|
|
33
|
+
file digest, because that is what the website serves it under, and the tree
|
|
34
|
+
digest must be the one this release measured.
|
|
35
|
+
|
|
36
|
+
*Would the website accept these bytes at all?* The shape rules are the
|
|
37
|
+
website's, not this repository's, and they stay the website's: it re-checks
|
|
38
|
+
every one of them at import. Restating them here is a pre-flight, so that a
|
|
39
|
+
release that cannot be published is caught while it is still being assembled
|
|
40
|
+
rather than at deploy time.
|
|
41
|
+
|
|
42
|
+
One thing is deliberately not checked: the plugin commit. A ReleaseCore does
|
|
43
|
+
not contain one (spec section 6.6) precisely so that the plugin can embed the
|
|
44
|
+
ReleaseCore without a cycle, so nothing here can have an opinion about which
|
|
45
|
+
commit the wrapper pins — only that it pins a full, immutable one.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
import re
|
|
52
|
+
from collections.abc import Mapping
|
|
53
|
+
from datetime import datetime
|
|
54
|
+
from typing import Final
|
|
55
|
+
|
|
56
|
+
from techtree.models.base import DIGEST_PATTERN, JsonValue
|
|
57
|
+
from techtree.release.checks import (
|
|
58
|
+
ReleaseCheck,
|
|
59
|
+
ReleaseVerification,
|
|
60
|
+
)
|
|
61
|
+
from techtree.release.models import (
|
|
62
|
+
OBJECT_URL_PATTERN,
|
|
63
|
+
ReleaseCore,
|
|
64
|
+
object_url_digest,
|
|
65
|
+
)
|
|
66
|
+
from techtree.release.provenance import COMMIT_PATTERN, BuildProvenance
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"BOOTSTRAP_RELEASE_INVALID",
|
|
70
|
+
"BOOTSTRAP_RELEASE_MISMATCH",
|
|
71
|
+
"BOOTSTRAP_SCHEMA_VERSION",
|
|
72
|
+
"verify_bootstrap_document",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
#: The schema version the website's importer accepts, and nothing else.
|
|
76
|
+
BOOTSTRAP_SCHEMA_VERSION: Final = "techtree.bootstrap.v1alpha1"
|
|
77
|
+
|
|
78
|
+
BOOTSTRAP_RELEASE_INVALID: Final = "bootstrap_release_invalid"
|
|
79
|
+
BOOTSTRAP_RELEASE_MISMATCH: Final = "bootstrap_release_mismatch"
|
|
80
|
+
|
|
81
|
+
#: How the published install command names an interpreter to the installer.
|
|
82
|
+
#: The value beside it is never written here — it is read from the document
|
|
83
|
+
#: being checked (decision 0034).
|
|
84
|
+
_INTERPRETER_FLAG: Final = "--python"
|
|
85
|
+
|
|
86
|
+
_COMMIT_RE = re.compile(COMMIT_PATTERN)
|
|
87
|
+
_OBJECT_URL_RE = re.compile(OBJECT_URL_PATTERN)
|
|
88
|
+
_DIGEST_RE = re.compile(DIGEST_PATTERN)
|
|
89
|
+
|
|
90
|
+
#: Every field the website importer requires, and the kind it requires. Listed
|
|
91
|
+
#: as paths so a failure can name the field the way the document spells it.
|
|
92
|
+
_REQUIRED_FIELDS: Final[tuple[tuple[tuple[str, ...], str], ...]] = (
|
|
93
|
+
(("channel",), "string"),
|
|
94
|
+
# The website's own declaration of whether it is serving a development
|
|
95
|
+
# bootstrap or a published one (decisions 0026 section 3). It says nothing
|
|
96
|
+
# about the release document, which is concrete either way.
|
|
97
|
+
(("placeholder_release",), "boolean"),
|
|
98
|
+
(("published_at",), "timestamp"),
|
|
99
|
+
(("minimums", "hermes_version"), "string"),
|
|
100
|
+
(("cli", "distribution"), "string"),
|
|
101
|
+
(("cli", "version"), "string"),
|
|
102
|
+
(("cli", "install_argv"), "argv"),
|
|
103
|
+
(("cli", "source_revision"), "commit"),
|
|
104
|
+
(("hermes_plugin", "plugin_id"), "string"),
|
|
105
|
+
(("hermes_plugin", "revision"), "commit"),
|
|
106
|
+
(("hermes_plugin", "install_argv"), "argv"),
|
|
107
|
+
(("hermes_plugin", "doctor_argv"), "argv"),
|
|
108
|
+
(("introductory_climb", "reference"), "string"),
|
|
109
|
+
(("introductory_climb", "host_prompt"), "string"),
|
|
110
|
+
(("starter_skill", "name"), "string"),
|
|
111
|
+
(("starter_skill", "object_url"), "object URL"),
|
|
112
|
+
(("starter_skill", "file_digest"), "digest"),
|
|
113
|
+
(("starter_skill", "tree_digest"), "digest"),
|
|
114
|
+
(("starter_skill", "media_type"), "string"),
|
|
115
|
+
(("starter_skill", "size"), "byte count"),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
#: The kinds whose name does not take "a". Spelled out rather than derived,
|
|
119
|
+
#: because the list is short and a vowel rule would be wrong for "URL".
|
|
120
|
+
_IRREGULAR_ARTICLES: Final[Mapping[str, str]] = {
|
|
121
|
+
"argv": "an argument array",
|
|
122
|
+
"object URL": "an object URL",
|
|
123
|
+
"byte count": "a positive byte count",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def verify_bootstrap_document(
|
|
128
|
+
core: ReleaseCore, raw: bytes, *, wheel: BuildProvenance, wheel_sha256: str
|
|
129
|
+
) -> ReleaseVerification:
|
|
130
|
+
"""Check one bootstrap document against the release it should wrap.
|
|
131
|
+
|
|
132
|
+
``wheel`` is the provenance stamped into the CLI wheel this bootstrap
|
|
133
|
+
publishes. It is required rather than optional: the document names a source
|
|
134
|
+
commit, and the only thing that can confirm it is the artifact itself.
|
|
135
|
+
|
|
136
|
+
``wheel_sha256`` is that same file's SHA-256, in lowercase hex without a
|
|
137
|
+
prefix, computed by the caller from the bytes it holds. The stamp cannot
|
|
138
|
+
carry it — decision 0026: an artifact never names its own identity — so the
|
|
139
|
+
one thing that can confirm the digest the document publishes is a fresh
|
|
140
|
+
hash of the file. Without this the gate would bind a wheel by name and by
|
|
141
|
+
commit while never checking the number a participant actually installs
|
|
142
|
+
against.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
document = json.loads(raw)
|
|
146
|
+
except ValueError as error:
|
|
147
|
+
return _one(
|
|
148
|
+
_failed(
|
|
149
|
+
"bootstrap_document",
|
|
150
|
+
BOOTSTRAP_RELEASE_INVALID,
|
|
151
|
+
f"the bootstrap document is not JSON: {error}",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
if not isinstance(document, dict):
|
|
155
|
+
return _one(
|
|
156
|
+
_failed(
|
|
157
|
+
"bootstrap_document",
|
|
158
|
+
BOOTSTRAP_RELEASE_INVALID,
|
|
159
|
+
"the bootstrap document must be a JSON object.",
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
schema = _schema_version_check(document)
|
|
164
|
+
if schema.status == "failed":
|
|
165
|
+
return _one(schema)
|
|
166
|
+
|
|
167
|
+
contract = _importer_contract_check(document)
|
|
168
|
+
checks = [schema, contract]
|
|
169
|
+
if contract.status == "failed":
|
|
170
|
+
# Every comparison below reads a field the contract just found missing
|
|
171
|
+
# or mis-typed, so running them would only repeat one failure.
|
|
172
|
+
return ReleaseVerification(verified=False, checks=checks)
|
|
173
|
+
|
|
174
|
+
checks.extend(
|
|
175
|
+
[
|
|
176
|
+
_coordinate_check(
|
|
177
|
+
"bootstrap_cli_version",
|
|
178
|
+
document,
|
|
179
|
+
("cli", "version"),
|
|
180
|
+
core.cli_version,
|
|
181
|
+
"the CLI version",
|
|
182
|
+
),
|
|
183
|
+
_wheel_commit_check(document, wheel),
|
|
184
|
+
_wheel_digest_check(document, wheel_sha256),
|
|
185
|
+
_coordinate_check(
|
|
186
|
+
"bootstrap_hermes_minimum",
|
|
187
|
+
document,
|
|
188
|
+
("minimums", "hermes_version"),
|
|
189
|
+
core.minimum_host_hermes_version,
|
|
190
|
+
"the minimum host Hermes version",
|
|
191
|
+
),
|
|
192
|
+
_coordinate_check(
|
|
193
|
+
"bootstrap_intro_climb",
|
|
194
|
+
document,
|
|
195
|
+
("introductory_climb", "reference"),
|
|
196
|
+
core.intro_climb_reference,
|
|
197
|
+
"the introductory Climb",
|
|
198
|
+
),
|
|
199
|
+
_coordinate_check(
|
|
200
|
+
"bootstrap_starter_skill_object_url",
|
|
201
|
+
document,
|
|
202
|
+
("starter_skill", "object_url"),
|
|
203
|
+
core.starter_skill_object_url,
|
|
204
|
+
"the starter Skill object URL",
|
|
205
|
+
),
|
|
206
|
+
_coordinate_check(
|
|
207
|
+
"bootstrap_starter_skill_tree_digest",
|
|
208
|
+
document,
|
|
209
|
+
("starter_skill", "tree_digest"),
|
|
210
|
+
core.starter_skill_digest,
|
|
211
|
+
"the starter Skill tree digest",
|
|
212
|
+
),
|
|
213
|
+
_starter_skill_address_check(document),
|
|
214
|
+
_cli_install_argv_check(core, document),
|
|
215
|
+
_plugin_install_argv_check(document),
|
|
216
|
+
]
|
|
217
|
+
)
|
|
218
|
+
return ReleaseVerification(
|
|
219
|
+
verified=not any(check.status == "failed" for check in checks),
|
|
220
|
+
checks=checks,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# The checks
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _schema_version_check(document: dict[str, JsonValue]) -> ReleaseCheck:
|
|
230
|
+
found = document.get("schema_version")
|
|
231
|
+
if found == BOOTSTRAP_SCHEMA_VERSION:
|
|
232
|
+
return _passed(
|
|
233
|
+
"bootstrap_schema_version",
|
|
234
|
+
f"the bootstrap document is a {BOOTSTRAP_SCHEMA_VERSION}.",
|
|
235
|
+
)
|
|
236
|
+
return _failed(
|
|
237
|
+
"bootstrap_schema_version",
|
|
238
|
+
BOOTSTRAP_RELEASE_INVALID,
|
|
239
|
+
f"the bootstrap document declares {found!r}; the website imports only "
|
|
240
|
+
f"{BOOTSTRAP_SCHEMA_VERSION}.",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _importer_contract_check(document: dict[str, JsonValue]) -> ReleaseCheck:
|
|
245
|
+
"""Report every field the website would refuse the document over."""
|
|
246
|
+
problems = [
|
|
247
|
+
f"{'.'.join(path)} must be {_article(kind)}"
|
|
248
|
+
for path, kind in _REQUIRED_FIELDS
|
|
249
|
+
if not _holds(_lookup(document, path), kind)
|
|
250
|
+
]
|
|
251
|
+
if problems:
|
|
252
|
+
return _failed(
|
|
253
|
+
"bootstrap_importer_contract",
|
|
254
|
+
BOOTSTRAP_RELEASE_INVALID,
|
|
255
|
+
"the website would refuse this document: " + "; ".join(problems) + ".",
|
|
256
|
+
)
|
|
257
|
+
return _passed(
|
|
258
|
+
"bootstrap_importer_contract",
|
|
259
|
+
f"all {len(_REQUIRED_FIELDS)} fields the website requires are present "
|
|
260
|
+
"and well formed.",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _wheel_commit_check(
|
|
265
|
+
document: dict[str, JsonValue], wheel: BuildProvenance
|
|
266
|
+
) -> ReleaseCheck:
|
|
267
|
+
"""The commit the wrapper publishes must be the wheel's own stamp.
|
|
268
|
+
|
|
269
|
+
The release document says nothing about which commit built which artifact
|
|
270
|
+
(decisions 0026), so this is the one coordinate here that is compared
|
|
271
|
+
against the artifact rather than against the release: the wheel was
|
|
272
|
+
stamped while it was built, and the wrapper repeats that stamp to
|
|
273
|
+
operators.
|
|
274
|
+
"""
|
|
275
|
+
published = _lookup(document, ("cli", "source_revision"))
|
|
276
|
+
if published == wheel.source_commit:
|
|
277
|
+
return _passed(
|
|
278
|
+
"bootstrap_cli_source_revision",
|
|
279
|
+
f"the published CLI was built from {wheel.source_commit}, which is "
|
|
280
|
+
"what the wheel is stamped with.",
|
|
281
|
+
)
|
|
282
|
+
return _failed(
|
|
283
|
+
"bootstrap_cli_source_revision",
|
|
284
|
+
BOOTSTRAP_RELEASE_MISMATCH,
|
|
285
|
+
f"the bootstrap document says the published CLI was built from "
|
|
286
|
+
f"{published!r}, and the wheel is stamped {wheel.source_commit!r}.",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _wheel_digest_check(
|
|
291
|
+
document: dict[str, JsonValue], wheel_sha256: str
|
|
292
|
+
) -> ReleaseCheck:
|
|
293
|
+
"""The digest the document publishes must be this wheel's own."""
|
|
294
|
+
published = _lookup(document, ("cli", "wheel_sha256"))
|
|
295
|
+
expected = f"sha256:{wheel_sha256}"
|
|
296
|
+
if published == expected:
|
|
297
|
+
return _passed(
|
|
298
|
+
"bootstrap_cli_wheel_sha256",
|
|
299
|
+
f"the published CLI wheel hashes to {expected}, which is what the "
|
|
300
|
+
"bootstrap document names.",
|
|
301
|
+
)
|
|
302
|
+
return _failed(
|
|
303
|
+
"bootstrap_cli_wheel_sha256",
|
|
304
|
+
BOOTSTRAP_RELEASE_MISMATCH,
|
|
305
|
+
f"the bootstrap document publishes the wheel digest {published!r}, and "
|
|
306
|
+
f"the wheel handed to this check hashes to {expected!r}.",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _starter_skill_address_check(document: dict[str, JsonValue]) -> ReleaseCheck:
|
|
311
|
+
"""The published address must be keyed by the bytes it returns.
|
|
312
|
+
|
|
313
|
+
The website files public objects under the digest of the file it serves,
|
|
314
|
+
and refuses an address keyed by anything else. A fetcher checks what
|
|
315
|
+
arrives against the address before it has built anything, so an address
|
|
316
|
+
keyed by the tree digest — or by nothing at all — would leave the first
|
|
317
|
+
check with nothing to compare against.
|
|
318
|
+
"""
|
|
319
|
+
url = _lookup(document, ("starter_skill", "object_url"))
|
|
320
|
+
file_digest = _lookup(document, ("starter_skill", "file_digest"))
|
|
321
|
+
if not isinstance(url, str) or not isinstance(file_digest, str):
|
|
322
|
+
raise AssertionError("both fields are validated before they are read")
|
|
323
|
+
|
|
324
|
+
keyed = object_url_digest(url)
|
|
325
|
+
if keyed == file_digest:
|
|
326
|
+
return _passed(
|
|
327
|
+
"bootstrap_starter_skill_address",
|
|
328
|
+
f"the starter Skill is published at the digest of its own bytes, "
|
|
329
|
+
f"{file_digest}.",
|
|
330
|
+
)
|
|
331
|
+
return _failed(
|
|
332
|
+
"bootstrap_starter_skill_address",
|
|
333
|
+
BOOTSTRAP_RELEASE_MISMATCH,
|
|
334
|
+
f"the starter Skill address is keyed by {keyed} and the document says "
|
|
335
|
+
f"the bytes it returns are {file_digest}; a fetcher would check what "
|
|
336
|
+
"arrives against the wrong digest.",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _coordinate_check(
|
|
341
|
+
identifier: str,
|
|
342
|
+
document: dict[str, JsonValue],
|
|
343
|
+
path: tuple[str, ...],
|
|
344
|
+
expected: str,
|
|
345
|
+
subject: str,
|
|
346
|
+
) -> ReleaseCheck:
|
|
347
|
+
found = _lookup(document, path)
|
|
348
|
+
if found == expected:
|
|
349
|
+
return _passed(identifier, f"{subject} agrees: {expected}.")
|
|
350
|
+
return _failed(
|
|
351
|
+
identifier,
|
|
352
|
+
BOOTSTRAP_RELEASE_MISMATCH,
|
|
353
|
+
f"{subject} is {found!r} in the bootstrap document and {expected!r} in "
|
|
354
|
+
"the ReleaseCore.",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _cli_install_argv_check(
|
|
359
|
+
core: ReleaseCore, document: dict[str, JsonValue]
|
|
360
|
+
) -> ReleaseCheck:
|
|
361
|
+
"""The published install command must install the published version, and
|
|
362
|
+
install it on the interpreter this same document requires.
|
|
363
|
+
|
|
364
|
+
Left to choose, the installer takes whatever Python the machine already
|
|
365
|
+
treats as its default — which can be a version Techtree does not support.
|
|
366
|
+
The install then succeeds and the first thing the operator sees is Doctor
|
|
367
|
+
saying the interpreter is wrong, after running the exact command this
|
|
368
|
+
project published (decision 0034). So the command pins the interpreter.
|
|
369
|
+
|
|
370
|
+
Which interpreter is not restated here. It is read from the document's own
|
|
371
|
+
requirements, because a second written-out copy of one number is how a
|
|
372
|
+
document ends up telling someone to install on an interpreter it also
|
|
373
|
+
calls unsupported. That makes the two halves one fact, and this check is
|
|
374
|
+
what keeps them one fact.
|
|
375
|
+
"""
|
|
376
|
+
argv = _string_list(_lookup(document, ("cli", "install_argv")))
|
|
377
|
+
distribution = _lookup(document, ("cli", "distribution"))
|
|
378
|
+
interpreter = _lookup(document, ("minimums", "python"))
|
|
379
|
+
pin = f"{distribution}=={core.cli_version}"
|
|
380
|
+
|
|
381
|
+
if pin not in argv:
|
|
382
|
+
return _failed(
|
|
383
|
+
"bootstrap_cli_install_argv",
|
|
384
|
+
BOOTSTRAP_RELEASE_MISMATCH,
|
|
385
|
+
f"the published install command does not pin {pin}; it is {argv}.",
|
|
386
|
+
)
|
|
387
|
+
if not _pins_interpreter(argv, interpreter):
|
|
388
|
+
return _failed(
|
|
389
|
+
"bootstrap_cli_install_argv",
|
|
390
|
+
BOOTSTRAP_RELEASE_MISMATCH,
|
|
391
|
+
"the published install command does not pin the interpreter this "
|
|
392
|
+
f"document requires: minimums.python is {interpreter!r} and the "
|
|
393
|
+
f"command is {argv}.",
|
|
394
|
+
)
|
|
395
|
+
return _passed(
|
|
396
|
+
"bootstrap_cli_install_argv",
|
|
397
|
+
f"the published install command pins {pin} and installs it on the "
|
|
398
|
+
f"Python {interpreter} this document requires.",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _pins_interpreter(argv: list[str], interpreter: JsonValue) -> bool:
|
|
403
|
+
"""Return whether the command names that interpreter to the installer."""
|
|
404
|
+
if not isinstance(interpreter, str) or not interpreter.strip():
|
|
405
|
+
return False
|
|
406
|
+
wanted = (_INTERPRETER_FLAG, interpreter)
|
|
407
|
+
return any(
|
|
408
|
+
tuple(argv[position : position + 2]) == wanted
|
|
409
|
+
for position in range(len(argv) - 1)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _plugin_install_argv_check(document: dict[str, JsonValue]) -> ReleaseCheck:
|
|
414
|
+
"""The published plugin command must install the commit it names."""
|
|
415
|
+
revision = _lookup(document, ("hermes_plugin", "revision"))
|
|
416
|
+
argv = _string_list(_lookup(document, ("hermes_plugin", "install_argv")))
|
|
417
|
+
if revision in argv:
|
|
418
|
+
return _passed(
|
|
419
|
+
"bootstrap_plugin_install_argv",
|
|
420
|
+
f"the published plugin command installs the exact commit {revision}.",
|
|
421
|
+
)
|
|
422
|
+
return _failed(
|
|
423
|
+
"bootstrap_plugin_install_argv",
|
|
424
|
+
BOOTSTRAP_RELEASE_MISMATCH,
|
|
425
|
+
f"the published plugin command does not name the commit {revision} "
|
|
426
|
+
f"the document pins; it is {argv}.",
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ---------------------------------------------------------------------------
|
|
431
|
+
# Reading the document
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _lookup(document: dict[str, JsonValue], path: tuple[str, ...]) -> JsonValue:
|
|
436
|
+
"""Return the value at a path, or None when any step is absent."""
|
|
437
|
+
current: JsonValue = document
|
|
438
|
+
for step in path:
|
|
439
|
+
if not isinstance(current, dict) or step not in current:
|
|
440
|
+
return None
|
|
441
|
+
current = current[step]
|
|
442
|
+
return current
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _holds(value: JsonValue, kind: str) -> bool:
|
|
446
|
+
"""Return whether a value is of the kind the website requires."""
|
|
447
|
+
match kind:
|
|
448
|
+
case "string":
|
|
449
|
+
return isinstance(value, str) and bool(value.strip())
|
|
450
|
+
case "boolean":
|
|
451
|
+
return isinstance(value, bool)
|
|
452
|
+
case "timestamp":
|
|
453
|
+
return isinstance(value, str) and _is_instant(value)
|
|
454
|
+
case "commit":
|
|
455
|
+
return isinstance(value, str) and _COMMIT_RE.fullmatch(value) is not None
|
|
456
|
+
case "digest":
|
|
457
|
+
return isinstance(value, str) and _DIGEST_RE.fullmatch(value) is not None
|
|
458
|
+
case "object URL":
|
|
459
|
+
return (
|
|
460
|
+
isinstance(value, str) and _OBJECT_URL_RE.fullmatch(value) is not None
|
|
461
|
+
)
|
|
462
|
+
case "argv":
|
|
463
|
+
return _is_argv(value)
|
|
464
|
+
case "byte count":
|
|
465
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
466
|
+
raise AssertionError(f"unknown field kind {kind!r}")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _is_instant(value: str) -> bool:
|
|
470
|
+
try:
|
|
471
|
+
datetime.fromisoformat(value)
|
|
472
|
+
except ValueError:
|
|
473
|
+
return False
|
|
474
|
+
return True
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _is_argv(value: JsonValue) -> bool:
|
|
478
|
+
"""Return whether a value is an argument array rather than a command line.
|
|
479
|
+
|
|
480
|
+
A string here would be a shell command, and the whole point of publishing
|
|
481
|
+
argument arrays is that nothing the website serves is ever handed to a
|
|
482
|
+
shell (spec section 9.12).
|
|
483
|
+
"""
|
|
484
|
+
if not isinstance(value, list) or not value:
|
|
485
|
+
return False
|
|
486
|
+
return all(isinstance(item, str) and bool(item.strip()) for item in value)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _string_list(value: JsonValue) -> list[str]:
|
|
490
|
+
"""Return an already-validated argument array as strings."""
|
|
491
|
+
if not isinstance(value, list):
|
|
492
|
+
raise AssertionError("argument arrays are validated before they are read")
|
|
493
|
+
return [item for item in value if isinstance(item, str)]
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _article(kind: str) -> str:
|
|
497
|
+
return _IRREGULAR_ARTICLES.get(kind, f"a {kind}")
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _passed(identifier: str, detail: str) -> ReleaseCheck:
|
|
501
|
+
return ReleaseCheck(id=identifier, status="passed", code="ok", detail=detail)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _failed(identifier: str, code: str, detail: str) -> ReleaseCheck:
|
|
505
|
+
return ReleaseCheck(id=identifier, status="failed", code=code, detail=detail)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _one(check: ReleaseCheck) -> ReleaseVerification:
|
|
509
|
+
return ReleaseVerification(verified=check.status != "failed", checks=[check])
|