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,326 @@
|
|
|
1
|
+
"""Running the checks and deciding what to do about them. Spec section 13.2.
|
|
2
|
+
|
|
3
|
+
The service owns three decisions the CLI must not make for itself: the order
|
|
4
|
+
checks run in, which failures actually block, and which repairs are worth
|
|
5
|
+
offering. Keeping them here is what lets the Doctor command be nine lines of
|
|
6
|
+
plumbing with no opinion in it.
|
|
7
|
+
|
|
8
|
+
Repairs are chosen by priority and capped at three. Some problems have a
|
|
9
|
+
concrete argument vector that fixes them — a permissions change, an engine
|
|
10
|
+
install. Others cannot be fixed by anything Techtree is allowed to run, and for
|
|
11
|
+
those the action's argv is the re-check and the reason says what a person has
|
|
12
|
+
to do first. That is deliberate: a next action is a promise that running the
|
|
13
|
+
vector is safe and sensible, so it never carries a placeholder to be filled in
|
|
14
|
+
or an installer piped out of the network.
|
|
15
|
+
|
|
16
|
+
When nothing needs repair the caller is not left without a next step. The
|
|
17
|
+
useful thing to do on a healthy host is to look at what there is to climb.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Callable, Iterable
|
|
23
|
+
from typing import Final
|
|
24
|
+
|
|
25
|
+
from techtree.doctor.checks import (
|
|
26
|
+
check_active_engine,
|
|
27
|
+
check_docker_cli,
|
|
28
|
+
check_docker_daemon,
|
|
29
|
+
check_hermes_cli,
|
|
30
|
+
check_hermes_plugin,
|
|
31
|
+
check_host_platform,
|
|
32
|
+
check_python_version,
|
|
33
|
+
check_techtree_home,
|
|
34
|
+
check_uv_cli,
|
|
35
|
+
detect_host_platform,
|
|
36
|
+
)
|
|
37
|
+
from techtree.doctor.execution_checks import execution_checks
|
|
38
|
+
from techtree.engines.registry import EngineRegistry
|
|
39
|
+
from techtree.models.base import NonEmptyString, ProtocolModel
|
|
40
|
+
from techtree.models.campaign import CampaignSpec
|
|
41
|
+
from techtree.models.cli import MAX_NEXT_ACTIONS, CheckStatus, DoctorCheck, NextAction
|
|
42
|
+
from techtree.paths import TechtreePaths
|
|
43
|
+
from techtree.settings import Settings
|
|
44
|
+
from techtree.version import version_info
|
|
45
|
+
|
|
46
|
+
__all__ = ["DoctorReport", "DoctorService"]
|
|
47
|
+
|
|
48
|
+
#: Where uv's own installation instructions live. It goes in a reason, never in
|
|
49
|
+
#: an argument vector: Techtree does not offer to pipe an installer into a
|
|
50
|
+
#: shell on someone's behalf.
|
|
51
|
+
UV_INSTALL_DOCUMENTATION: Final = (
|
|
52
|
+
"https://docs.astral.sh/uv/getting-started/installation/"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DoctorReport(ProtocolModel):
|
|
57
|
+
"""Everything one Doctor run found.
|
|
58
|
+
|
|
59
|
+
The two platform strings are separate on purpose. ``host_platform`` is
|
|
60
|
+
where Techtree runs; ``docker_platform`` is where an evaluated subject
|
|
61
|
+
would run, and is absent whenever the daemon could not say.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
techtree_home: NonEmptyString
|
|
65
|
+
host_platform: NonEmptyString | None
|
|
66
|
+
docker_platform: NonEmptyString | None
|
|
67
|
+
versions: dict[str, NonEmptyString]
|
|
68
|
+
checks: list[DoctorCheck]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DoctorService:
|
|
72
|
+
"""Runs environment checks and turns them into repairs."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, paths: TechtreePaths, settings: Settings) -> None:
|
|
75
|
+
self._paths = paths
|
|
76
|
+
self._settings = settings
|
|
77
|
+
|
|
78
|
+
def run(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
for_evaluation: bool = False,
|
|
82
|
+
campaign: CampaignSpec | None = None,
|
|
83
|
+
) -> list[DoctorCheck]:
|
|
84
|
+
"""Run checks in deterministic order.
|
|
85
|
+
|
|
86
|
+
``for_evaluation`` adds the execution checks of spec section 6.18 and
|
|
87
|
+
makes them blocking. An ordinary Doctor is a question about the machine
|
|
88
|
+
and answers it with warnings; the evaluation Doctor is the gate in front
|
|
89
|
+
of a run that provisions containers and spends money on model calls, so
|
|
90
|
+
the same facts stop it rather than merely being noted.
|
|
91
|
+
|
|
92
|
+
A ``campaign`` narrows the question from "could anything run here" to
|
|
93
|
+
"could this run here", which is the only form in which the subject's
|
|
94
|
+
credential and image can be checked at all.
|
|
95
|
+
"""
|
|
96
|
+
host_platform = detect_host_platform()
|
|
97
|
+
checks = [
|
|
98
|
+
check_python_version(),
|
|
99
|
+
check_host_platform(host_platform),
|
|
100
|
+
check_techtree_home(self._paths),
|
|
101
|
+
check_uv_cli(),
|
|
102
|
+
check_docker_cli(),
|
|
103
|
+
check_docker_daemon(),
|
|
104
|
+
check_hermes_cli(),
|
|
105
|
+
check_hermes_plugin(),
|
|
106
|
+
check_active_engine(self._paths, self._settings),
|
|
107
|
+
]
|
|
108
|
+
if not for_evaluation:
|
|
109
|
+
return checks
|
|
110
|
+
registry = EngineRegistry(self._paths, self._settings)
|
|
111
|
+
checks.extend(execution_checks(engine_registry=registry, campaign=campaign))
|
|
112
|
+
return checks
|
|
113
|
+
|
|
114
|
+
def report(self, checks: list[DoctorCheck]) -> DoctorReport:
|
|
115
|
+
"""Project the checks into the command's response payload."""
|
|
116
|
+
return DoctorReport(
|
|
117
|
+
techtree_home=str(self._paths.root),
|
|
118
|
+
host_platform=_metadata_string(checks, "host_platform", "host_platform"),
|
|
119
|
+
docker_platform=_metadata_string(
|
|
120
|
+
checks, "docker_daemon", "docker_platform"
|
|
121
|
+
),
|
|
122
|
+
versions=version_info(),
|
|
123
|
+
checks=list(checks),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def blocking_failures(self, checks: list[DoctorCheck]) -> list[DoctorCheck]:
|
|
127
|
+
"""Return blocking failures."""
|
|
128
|
+
return [check for check in checks if check.blocking]
|
|
129
|
+
|
|
130
|
+
def warnings(self, checks: list[DoctorCheck]) -> list[DoctorCheck]:
|
|
131
|
+
"""Return warnings."""
|
|
132
|
+
return [
|
|
133
|
+
check
|
|
134
|
+
for check in checks
|
|
135
|
+
if check.status is CheckStatus.WARN
|
|
136
|
+
or (check.status is CheckStatus.FAIL and not check.blocking)
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
def next_actions(self, checks: list[DoctorCheck]) -> list[NextAction]:
|
|
140
|
+
"""Create no more than three repair actions."""
|
|
141
|
+
by_id = {check.id: check for check in checks}
|
|
142
|
+
actions: list[NextAction] = []
|
|
143
|
+
offered: set[str] = set()
|
|
144
|
+
|
|
145
|
+
for check_id, build in _REPAIRS:
|
|
146
|
+
check = by_id.get(check_id)
|
|
147
|
+
if check is None or check.status in (CheckStatus.PASS, CheckStatus.SKIP):
|
|
148
|
+
continue
|
|
149
|
+
action = build(self._paths)
|
|
150
|
+
# Two checks can share one repair — a missing Docker CLI and an
|
|
151
|
+
# unreachable daemon are both fixed by installing and starting it.
|
|
152
|
+
if action is None or action.id in offered:
|
|
153
|
+
continue
|
|
154
|
+
actions.append(action)
|
|
155
|
+
offered.add(action.id)
|
|
156
|
+
if len(actions) == MAX_NEXT_ACTIONS:
|
|
157
|
+
return actions
|
|
158
|
+
|
|
159
|
+
if actions:
|
|
160
|
+
return actions
|
|
161
|
+
# Nothing offered a repair. That means either the host is fine, or the
|
|
162
|
+
# only things wrong with it are things Techtree cannot fix — a Campaign
|
|
163
|
+
# that is not meant to be executed yet, for instance. Saying "this host
|
|
164
|
+
# is ready" in the second case would be a lie told cheerfully.
|
|
165
|
+
return [_browse_climbs(ready=not any(check.blocking for check in checks))]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _metadata_string(
|
|
169
|
+
checks: Iterable[DoctorCheck], check_id: str, key: str
|
|
170
|
+
) -> str | None:
|
|
171
|
+
"""Read one string out of a check's metadata, if the check reported it."""
|
|
172
|
+
for check in checks:
|
|
173
|
+
if check.id != check_id:
|
|
174
|
+
continue
|
|
175
|
+
value = check.metadata.get(key)
|
|
176
|
+
return value if isinstance(value, str) else None
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _fix_home_permissions(paths: TechtreePaths) -> NextAction:
|
|
181
|
+
return NextAction(
|
|
182
|
+
id="fix_techtree_home_permissions",
|
|
183
|
+
label="Make the Techtree home directory private and writable",
|
|
184
|
+
reason="Techtree stores drafts, runs, and engines there for one user only.",
|
|
185
|
+
cli=["chmod", "700", str(paths.root)],
|
|
186
|
+
hermes_tool=None,
|
|
187
|
+
hermes_args=None,
|
|
188
|
+
requires_user_confirmation=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _install_supported_python(_: TechtreePaths) -> NextAction:
|
|
193
|
+
return NextAction(
|
|
194
|
+
id="install_supported_python",
|
|
195
|
+
label="Install a supported Python and re-run Doctor",
|
|
196
|
+
reason="Techtree requires Python 3.12 or 3.13.",
|
|
197
|
+
cli=["uv", "python", "install", "3.12"],
|
|
198
|
+
hermes_tool=None,
|
|
199
|
+
hermes_args=None,
|
|
200
|
+
requires_user_confirmation=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _install_engine(_: TechtreePaths) -> NextAction:
|
|
205
|
+
return NextAction(
|
|
206
|
+
id="install_engine",
|
|
207
|
+
label="Install the managed evaluation engine",
|
|
208
|
+
reason="Preparing or starting a Climb needs an installed, active engine.",
|
|
209
|
+
cli=["techtree", "engine", "install"],
|
|
210
|
+
hermes_tool=None,
|
|
211
|
+
hermes_args=None,
|
|
212
|
+
requires_user_confirmation=False,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _recheck_after_installing_uv(_: TechtreePaths) -> NextAction:
|
|
217
|
+
return NextAction(
|
|
218
|
+
id="recheck_after_installing_uv",
|
|
219
|
+
label="Install uv, then re-run Doctor",
|
|
220
|
+
reason=(
|
|
221
|
+
"The managed evaluation engine is installed with uv. Installation "
|
|
222
|
+
f"instructions: {UV_INSTALL_DOCUMENTATION}"
|
|
223
|
+
),
|
|
224
|
+
cli=["techtree", "doctor"],
|
|
225
|
+
hermes_tool=None,
|
|
226
|
+
hermes_args=None,
|
|
227
|
+
requires_user_confirmation=False,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _recheck_after_starting_docker(_: TechtreePaths) -> NextAction:
|
|
232
|
+
return NextAction(
|
|
233
|
+
id="recheck_after_starting_docker",
|
|
234
|
+
label="Start Docker, then re-run Doctor",
|
|
235
|
+
reason="An evaluated subject runs in a container on this host.",
|
|
236
|
+
cli=["techtree", "doctor"],
|
|
237
|
+
hermes_tool=None,
|
|
238
|
+
hermes_args=None,
|
|
239
|
+
requires_user_confirmation=False,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _unsupported_host(_: TechtreePaths) -> None:
|
|
244
|
+
"""No repair exists for the wrong machine, so no action is offered."""
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _sign_in_to_prime(_: TechtreePaths) -> NextAction:
|
|
249
|
+
return NextAction(
|
|
250
|
+
id="sign_in_to_prime",
|
|
251
|
+
label="Sign in to Prime, then re-run this check",
|
|
252
|
+
reason=(
|
|
253
|
+
"The evaluated subject's model calls are paid for by a credential "
|
|
254
|
+
"a run reads for itself, which is why signing in to Prime works "
|
|
255
|
+
"and exporting the credential in a terminal does not. It is "
|
|
256
|
+
"separate from whatever your own agent is signed in with. After "
|
|
257
|
+
"signing in, run techtree doctor --for-evaluation again."
|
|
258
|
+
),
|
|
259
|
+
cli=["prime", "login"],
|
|
260
|
+
hermes_tool=None,
|
|
261
|
+
hermes_args=None,
|
|
262
|
+
requires_user_confirmation=True,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _pull_subject_image(_: TechtreePaths) -> NextAction:
|
|
267
|
+
return NextAction(
|
|
268
|
+
id="pull_subject_image",
|
|
269
|
+
label="Pull the subject's container image, then re-run this check",
|
|
270
|
+
reason=(
|
|
271
|
+
"The evaluated subject runs in the image the Campaign pins. "
|
|
272
|
+
"Downloading it is a deliberate setup step, so Techtree reports it "
|
|
273
|
+
"rather than doing it during a check."
|
|
274
|
+
),
|
|
275
|
+
cli=["techtree", "doctor", "--for-evaluation"],
|
|
276
|
+
hermes_tool=None,
|
|
277
|
+
hermes_args=None,
|
|
278
|
+
requires_user_confirmation=False,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _not_runnable_yet(_: TechtreePaths) -> None:
|
|
283
|
+
"""A Campaign that is not meant to be executed has no repair."""
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _browse_climbs(*, ready: bool) -> NextAction:
|
|
288
|
+
return NextAction(
|
|
289
|
+
id="list_climbs",
|
|
290
|
+
label="Browse the available Climbs",
|
|
291
|
+
reason=(
|
|
292
|
+
"This host is ready."
|
|
293
|
+
if ready
|
|
294
|
+
else (
|
|
295
|
+
"Nothing left to fix automatically; each failed check says what "
|
|
296
|
+
"it needs."
|
|
297
|
+
)
|
|
298
|
+
),
|
|
299
|
+
cli=["techtree", "climb", "list"],
|
|
300
|
+
hermes_tool=None,
|
|
301
|
+
hermes_args=None,
|
|
302
|
+
requires_user_confirmation=False,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
#: Check identifier to repair builder, most important first. A builder returns
|
|
307
|
+
#: None when the problem is real and nothing runnable would fix it.
|
|
308
|
+
type _Repair = Callable[[TechtreePaths], NextAction | None]
|
|
309
|
+
|
|
310
|
+
_REPAIRS: Final[tuple[tuple[str, _Repair], ...]] = (
|
|
311
|
+
("techtree_home", _fix_home_permissions),
|
|
312
|
+
("python_version", _install_supported_python),
|
|
313
|
+
("host_platform", _unsupported_host),
|
|
314
|
+
("uv", _recheck_after_installing_uv),
|
|
315
|
+
("active_engine", _install_engine),
|
|
316
|
+
("docker_cli", _recheck_after_starting_docker),
|
|
317
|
+
("docker_daemon", _recheck_after_starting_docker),
|
|
318
|
+
# Spec section 6.18. These only appear under --for-evaluation, and they sit
|
|
319
|
+
# last because a host that fails an ordinary check fails these too, and the
|
|
320
|
+
# ordinary repair is the one worth offering first.
|
|
321
|
+
("execution_docker_platform", _recheck_after_starting_docker),
|
|
322
|
+
("execution_engine_eval", _install_engine),
|
|
323
|
+
("execution_model_credential", _sign_in_to_prime),
|
|
324
|
+
("execution_subject_image", _pull_subject_image),
|
|
325
|
+
("execution_live_campaign", _not_runnable_yet),
|
|
326
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Immutable submission drafts and the one-time handover that starts one.
|
|
2
|
+
|
|
3
|
+
Spec PR6 §6.7.
|
|
4
|
+
|
|
5
|
+
A draft is the complete, reviewable statement of what a participant is about to
|
|
6
|
+
run: the public graph it came from, the two experiment variants, the proof that
|
|
7
|
+
those variants differ only where permitted, the snapshotted candidate skill,
|
|
8
|
+
and the rights the participant is being asked to accept. Once written it is
|
|
9
|
+
never edited. A change of mind is a new draft.
|
|
10
|
+
|
|
11
|
+
Two modules divide the work:
|
|
12
|
+
|
|
13
|
+
:mod:`techtree.drafts.source`
|
|
14
|
+
What a draft is prepared against: a Campaign, its DataPolicy, its
|
|
15
|
+
publisher validation receipt, and the public Climb that invited it when
|
|
16
|
+
there is one. A Skill replacement (spec section 7.19) is derived locally
|
|
17
|
+
and no Climb wraps it, so the Climb is the one part that may be absent.
|
|
18
|
+
|
|
19
|
+
:mod:`techtree.drafts.store`
|
|
20
|
+
Placement, verification, and the start claim. Where each file lives under
|
|
21
|
+
``drafts/draft_<id>/``, how the whole graph is re-verified offline without
|
|
22
|
+
consulting the catalog, and the exactly-once handover to a run.
|
|
23
|
+
|
|
24
|
+
Preparing a draft is not accepting its data policy. Decisions document 0019
|
|
25
|
+
section 2 puts the acceptance where a person can act on it: the draft is
|
|
26
|
+
reviewed, the review is answered, and the run records that it was — nothing
|
|
27
|
+
here may be read as consent.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""What a draft is prepared against, with or without a public Climb.
|
|
2
|
+
|
|
3
|
+
Every draft this build produced before WP7d came from a
|
|
4
|
+
:class:`~techtree.models.climb.ResolvedClimb`: a public invitation wrapping a
|
|
5
|
+
Campaign. Spec section 7.19 asks for one that does not. A Skill-replacement
|
|
6
|
+
Campaign is derived locally from a run that already finished, and no Climb
|
|
7
|
+
wraps it — :class:`~techtree.models.climb.CandidatePolicy` can only require
|
|
8
|
+
``skill_insertion``, so a ``ResolvedClimb`` holding a replacement Campaign is
|
|
9
|
+
unrepresentable rather than merely unusual.
|
|
10
|
+
|
|
11
|
+
:class:`CampaignSource` is what the kernel carries instead. It holds the three
|
|
12
|
+
objects a run's science actually needs — the Campaign, the DataPolicy it runs
|
|
13
|
+
under, and the publisher's taskset validation receipt — and the public Climb
|
|
14
|
+
beside them when there is one. Preparation, the draft store, the run's staged
|
|
15
|
+
inputs, and the report stage all read it, so a local replacement travels the
|
|
16
|
+
same path as a public submission rather than a parallel one.
|
|
17
|
+
|
|
18
|
+
Two properties are deliberate.
|
|
19
|
+
|
|
20
|
+
*The Climb's own edge checks stay in one place.* When a source comes from a
|
|
21
|
+
Climb it comes through :meth:`CampaignSource.from_climb`, which takes an
|
|
22
|
+
already-validated ``ResolvedClimb``; when it is read back from disk the Climb
|
|
23
|
+
is reassembled into a ``ResolvedClimb`` first, so its validator runs on the
|
|
24
|
+
bytes. Nothing here re-implements those checks, and a Climb-free source is not
|
|
25
|
+
given a weaker version of them: it has no Climb to check.
|
|
26
|
+
|
|
27
|
+
*A source is a container, not an authority.* The Campaign-to-DataPolicy and
|
|
28
|
+
Campaign-to-receipt edges are verified where the bytes are — in
|
|
29
|
+
:meth:`~techtree.drafts.store.DraftStore.verify_snapshot` and in
|
|
30
|
+
:class:`~techtree.runs.artifacts.RunArtifactStore` — because those are the
|
|
31
|
+
places that know which digest a *request* said to expect.
|
|
32
|
+
|
|
33
|
+
:class:`StagedSkill` is the other half of the same generalization. An insertion
|
|
34
|
+
stages one Skill, the candidate's. A replacement stages two, because the
|
|
35
|
+
baseline variant carries the Skill being revised and the subject has to be
|
|
36
|
+
given its files, not just its digest.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
from techtree.canonical import digest_object
|
|
45
|
+
from techtree.models.base import Digest
|
|
46
|
+
from techtree.models.campaign import CampaignSpec, MutationKind, PublicContext
|
|
47
|
+
from techtree.models.climb import ClimbManifest, ResolvedClimb
|
|
48
|
+
from techtree.models.data_policy import DataPolicy
|
|
49
|
+
from techtree.models.skill import SkillArtifact
|
|
50
|
+
from techtree.models.validation import TasksetValidationReceipt
|
|
51
|
+
|
|
52
|
+
__all__ = ["CampaignSource", "StagedSkill"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class StagedSkill:
|
|
57
|
+
"""One Skill as a draft or a run owns it: the artifact and its bytes."""
|
|
58
|
+
|
|
59
|
+
artifact: SkillArtifact
|
|
60
|
+
archive: Path
|
|
61
|
+
files: Path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class CampaignSource:
|
|
66
|
+
"""The Campaign graph a draft is prepared against.
|
|
67
|
+
|
|
68
|
+
``climb`` and ``climb_digest`` are present together or absent together: a
|
|
69
|
+
Climb-free source is a local Campaign, which spec section 7.19 derives for
|
|
70
|
+
a Skill replacement and which no public invitation wraps.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
campaign: CampaignSpec
|
|
74
|
+
campaign_digest: Digest
|
|
75
|
+
data_policy: DataPolicy
|
|
76
|
+
data_policy_digest: Digest
|
|
77
|
+
publisher_validation: TasksetValidationReceipt
|
|
78
|
+
publisher_validation_digest: Digest
|
|
79
|
+
climb: ClimbManifest | None
|
|
80
|
+
climb_digest: Digest | None
|
|
81
|
+
|
|
82
|
+
def __post_init__(self) -> None:
|
|
83
|
+
"""Reject a half-present Climb, which no reader could interpret."""
|
|
84
|
+
if (self.climb is None) != (self.climb_digest is None):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"a campaign source names a public Climb and the digest it was "
|
|
87
|
+
"loaded under together, or neither"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_climb(cls, resolved: ResolvedClimb) -> CampaignSource:
|
|
92
|
+
"""Return the source one resolved public Climb describes."""
|
|
93
|
+
return cls(
|
|
94
|
+
campaign=resolved.campaign,
|
|
95
|
+
campaign_digest=resolved.campaign_digest,
|
|
96
|
+
data_policy=resolved.data_policy,
|
|
97
|
+
data_policy_digest=resolved.data_policy_digest,
|
|
98
|
+
publisher_validation=resolved.publisher_validation,
|
|
99
|
+
publisher_validation_digest=resolved.publisher_validation_digest,
|
|
100
|
+
climb=resolved.climb,
|
|
101
|
+
climb_digest=resolved.climb_digest,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def local(
|
|
106
|
+
cls,
|
|
107
|
+
*,
|
|
108
|
+
campaign: CampaignSpec,
|
|
109
|
+
data_policy: DataPolicy,
|
|
110
|
+
publisher_validation: TasksetValidationReceipt,
|
|
111
|
+
) -> CampaignSource:
|
|
112
|
+
"""Return a source for a locally derived Campaign that no Climb wraps."""
|
|
113
|
+
return cls(
|
|
114
|
+
campaign=campaign,
|
|
115
|
+
campaign_digest=digest_object(campaign),
|
|
116
|
+
data_policy=data_policy,
|
|
117
|
+
data_policy_digest=digest_object(data_policy),
|
|
118
|
+
publisher_validation=publisher_validation,
|
|
119
|
+
publisher_validation_digest=digest_object(publisher_validation),
|
|
120
|
+
climb=None,
|
|
121
|
+
climb_digest=None,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def public_context(self) -> PublicContext | None:
|
|
126
|
+
"""Return the public context artifacts built from this source carry."""
|
|
127
|
+
if self.climb_digest is None:
|
|
128
|
+
return None
|
|
129
|
+
return PublicContext(kind="climb", climb_digest=self.climb_digest)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def title(self) -> str:
|
|
133
|
+
"""Return what to call this comparison in a result a person reads.
|
|
134
|
+
|
|
135
|
+
A public Climb has a title written for readers. A locally derived
|
|
136
|
+
Campaign has only its own identity, and saying so is better than
|
|
137
|
+
inventing a name for it.
|
|
138
|
+
"""
|
|
139
|
+
if self.climb is not None:
|
|
140
|
+
return self.climb.metadata.title
|
|
141
|
+
return f"{self.campaign.metadata.id} v{self.campaign.metadata.version}"
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def replaces_a_skill(self) -> bool:
|
|
145
|
+
"""Return whether this Campaign's baseline carries the Skill being revised."""
|
|
146
|
+
return self.campaign.mutation_contract.kind is MutationKind.SKILL_REPLACEMENT
|