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,435 @@
|
|
|
1
|
+
"""What has to be true before a real evaluation starts. Spec section 6.18.
|
|
2
|
+
|
|
3
|
+
:mod:`techtree.doctor.checks` answers "is this machine usable at all". This
|
|
4
|
+
module answers a narrower and more expensive question: could the *next* real
|
|
5
|
+
Campaign run actually execute here. The two are separate because the answers
|
|
6
|
+
have different consequences. Somebody browsing Climbs should not be told that
|
|
7
|
+
Docker is broken as though the tool were unusable; somebody about to spend money
|
|
8
|
+
on model calls should be stopped before a container is provisioned.
|
|
9
|
+
|
|
10
|
+
Three distinctions run through the whole module.
|
|
11
|
+
|
|
12
|
+
*The subject platform is not the host platform.* An evaluated subject runs in a
|
|
13
|
+
container on whatever the daemon serves, which on macOS is a Linux VM. Both are
|
|
14
|
+
reported in one ``<os>/<arch>`` vocabulary so a Campaign's declared
|
|
15
|
+
``supported_platforms`` can be compared against the thing that will actually run
|
|
16
|
+
it, rather than against the laptop.
|
|
17
|
+
|
|
18
|
+
*Evaluation authentication is not host authentication.* The credential checked
|
|
19
|
+
here buys tokens for the evaluated subject. It has nothing to do with whatever
|
|
20
|
+
the operator's own Hermes is signed in with, and it fails late and expensively
|
|
21
|
+
if it is missing: the pinned client resolves an absent key to the literal string
|
|
22
|
+
``"EMPTY"`` and only discovers the problem at the first model call, after Docker
|
|
23
|
+
is up (``docs/verifiers-eval.md``). It is also diagnosed for the environment a
|
|
24
|
+
detached run would be given rather than for the terminal these checks were
|
|
25
|
+
typed into, because those differ in the one way that matters and a readiness
|
|
26
|
+
answer that cannot survive the difference is worse than no answer at all.
|
|
27
|
+
|
|
28
|
+
*Pulling an image is a setup operation, not a check.* A check that silently
|
|
29
|
+
downloaded gigabytes would be a check that lied about being read-only.
|
|
30
|
+
:func:`check_subject_image` inspects what is present locally and says plainly
|
|
31
|
+
what to run when it is not.
|
|
32
|
+
|
|
33
|
+
Nothing here reads or returns a credential value; see
|
|
34
|
+
:mod:`techtree.verifiers.credentials` for why that is a hard boundary rather
|
|
35
|
+
than a habit.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import os
|
|
41
|
+
import shutil
|
|
42
|
+
import subprocess
|
|
43
|
+
from typing import Final
|
|
44
|
+
|
|
45
|
+
from techtree.doctor.checks import (
|
|
46
|
+
DAEMON_TIMEOUT_SECONDS,
|
|
47
|
+
VERSION_TIMEOUT_SECONDS,
|
|
48
|
+
detect_host_platform,
|
|
49
|
+
)
|
|
50
|
+
from techtree.engines.bundle import read_engine_descriptor
|
|
51
|
+
from techtree.engines.registry import EngineRegistry
|
|
52
|
+
from techtree.errors import ValidationError
|
|
53
|
+
from techtree.models.base import Digest, JsonValue
|
|
54
|
+
from techtree.models.campaign import (
|
|
55
|
+
SUBJECT_AGENT,
|
|
56
|
+
CampaignSpec,
|
|
57
|
+
ModelSpec,
|
|
58
|
+
RuntimeSpec,
|
|
59
|
+
)
|
|
60
|
+
from techtree.models.cli import CheckStatus, DoctorCheck
|
|
61
|
+
from techtree.runs.launcher import worker_visible_environment
|
|
62
|
+
from techtree.verifiers.child import EVAL_EXECUTABLE
|
|
63
|
+
from techtree.verifiers.credentials import credential_status
|
|
64
|
+
from techtree.verifiers.image import resolve_subject_image
|
|
65
|
+
from techtree.verifiers.models import VariantName
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"DEVELOPMENT_PLACEHOLDER_MARKERS",
|
|
69
|
+
"IMAGE_INSPECT_TIMEOUT_SECONDS",
|
|
70
|
+
"check_docker_platform",
|
|
71
|
+
"check_engine_eval",
|
|
72
|
+
"check_live_campaign",
|
|
73
|
+
"check_prime_auth",
|
|
74
|
+
"check_subject_image",
|
|
75
|
+
"execution_checks",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
#: Inspecting a local image is a metadata read, but the daemon may still be
|
|
79
|
+
#: waking up, so it gets the daemon's allowance rather than a version probe's.
|
|
80
|
+
IMAGE_INSPECT_TIMEOUT_SECONDS: Final = DAEMON_TIMEOUT_SECONDS
|
|
81
|
+
|
|
82
|
+
#: Substrings that mark a Campaign as a development fixture rather than
|
|
83
|
+
#: something that may be executed for real. The shipped Campaign carries none
|
|
84
|
+
#: of them any more — decisions document 0007 R5 pinned its container by
|
|
85
|
+
#: content and decisions document 0025 named its subject model — but a
|
|
86
|
+
#: Campaign is not always the shipped one, and every field is still scanned,
|
|
87
|
+
#: because which fields happen to carry a placeholder is not what the check is
|
|
88
|
+
#: about.
|
|
89
|
+
DEVELOPMENT_PLACEHOLDER_MARKERS: Final[tuple[str, ...]] = (
|
|
90
|
+
"development-placeholder",
|
|
91
|
+
"not-executed",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
#: The platform strings a subject container can be served on.
|
|
95
|
+
_SUPPORTED_SUBJECT_PLATFORMS: Final[tuple[str, ...]] = ("linux/arm64", "linux/amd64")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_docker_platform() -> DoctorCheck:
|
|
99
|
+
"""Confirm the daemon is reachable and serving a supported subject platform."""
|
|
100
|
+
metadata: dict[str, JsonValue] = {"host_platform": detect_host_platform()}
|
|
101
|
+
|
|
102
|
+
if shutil.which("docker") is None:
|
|
103
|
+
return DoctorCheck(
|
|
104
|
+
id="execution_docker_platform",
|
|
105
|
+
label="Docker subject platform",
|
|
106
|
+
status=CheckStatus.FAIL,
|
|
107
|
+
detail=(
|
|
108
|
+
"docker was not found on PATH; an evaluated subject runs in a "
|
|
109
|
+
"container on this host"
|
|
110
|
+
),
|
|
111
|
+
blocking=True,
|
|
112
|
+
metadata=metadata,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
served = _docker_platform()
|
|
116
|
+
if served is None:
|
|
117
|
+
return DoctorCheck(
|
|
118
|
+
id="execution_docker_platform",
|
|
119
|
+
label="Docker subject platform",
|
|
120
|
+
status=CheckStatus.FAIL,
|
|
121
|
+
detail=(
|
|
122
|
+
"the Docker daemon did not answer; start Docker before running "
|
|
123
|
+
"an evaluation"
|
|
124
|
+
),
|
|
125
|
+
blocking=True,
|
|
126
|
+
metadata=metadata,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
metadata["docker_platform"] = served
|
|
130
|
+
if served not in _SUPPORTED_SUBJECT_PLATFORMS:
|
|
131
|
+
return DoctorCheck(
|
|
132
|
+
id="execution_docker_platform",
|
|
133
|
+
label="Docker subject platform",
|
|
134
|
+
status=CheckStatus.FAIL,
|
|
135
|
+
detail=(
|
|
136
|
+
f"the Docker daemon serves {served}; an evaluated subject needs "
|
|
137
|
+
f"one of {', '.join(_SUPPORTED_SUBJECT_PLATFORMS)}"
|
|
138
|
+
),
|
|
139
|
+
blocking=True,
|
|
140
|
+
metadata=metadata,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return DoctorCheck(
|
|
144
|
+
id="execution_docker_platform",
|
|
145
|
+
label="Docker subject platform",
|
|
146
|
+
status=CheckStatus.PASS,
|
|
147
|
+
detail=f"the Docker daemon is reachable and serving {served}",
|
|
148
|
+
blocking=False,
|
|
149
|
+
metadata=metadata,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def check_engine_eval(
|
|
154
|
+
engine_registry: EngineRegistry, digest: Digest | None = None
|
|
155
|
+
) -> DoctorCheck:
|
|
156
|
+
"""Confirm the engine's own ``eval`` exists and holds the expected revision.
|
|
157
|
+
|
|
158
|
+
The console script is addressed by absolute path inside the engine's virtual
|
|
159
|
+
environment. Checking that the file is there is checking the one thing a
|
|
160
|
+
real run cannot recover from: a ``PATH`` lookup would find *something*
|
|
161
|
+
called ``eval`` on almost any machine.
|
|
162
|
+
"""
|
|
163
|
+
resolved = digest if digest is not None else engine_registry.active_digest()
|
|
164
|
+
metadata: dict[str, JsonValue] = {"engine_digest": resolved}
|
|
165
|
+
|
|
166
|
+
if resolved is None:
|
|
167
|
+
return DoctorCheck(
|
|
168
|
+
id="execution_engine_eval",
|
|
169
|
+
label="Engine eval entrypoint",
|
|
170
|
+
status=CheckStatus.FAIL,
|
|
171
|
+
detail=(
|
|
172
|
+
"no managed evaluation engine is active; install one before "
|
|
173
|
+
"running an evaluation"
|
|
174
|
+
),
|
|
175
|
+
blocking=True,
|
|
176
|
+
metadata=metadata,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
status = engine_registry.status(resolved)
|
|
180
|
+
if not status.installed:
|
|
181
|
+
return DoctorCheck(
|
|
182
|
+
id="execution_engine_eval",
|
|
183
|
+
label="Engine eval entrypoint",
|
|
184
|
+
status=CheckStatus.FAIL,
|
|
185
|
+
detail=f"engine {resolved} is not installed",
|
|
186
|
+
blocking=True,
|
|
187
|
+
metadata=metadata,
|
|
188
|
+
)
|
|
189
|
+
if not status.verified:
|
|
190
|
+
return DoctorCheck(
|
|
191
|
+
id="execution_engine_eval",
|
|
192
|
+
label="Engine eval entrypoint",
|
|
193
|
+
status=CheckStatus.FAIL,
|
|
194
|
+
detail=(
|
|
195
|
+
f"engine {resolved} is installed but has not been verified; a "
|
|
196
|
+
"real run executes only a verified engine"
|
|
197
|
+
),
|
|
198
|
+
blocking=True,
|
|
199
|
+
metadata=metadata,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
executable = engine_registry.executable(resolved, EVAL_EXECUTABLE)
|
|
203
|
+
metadata["eval_executable"] = str(executable)
|
|
204
|
+
if not executable.is_file():
|
|
205
|
+
return DoctorCheck(
|
|
206
|
+
id="execution_engine_eval",
|
|
207
|
+
label="Engine eval entrypoint",
|
|
208
|
+
status=CheckStatus.FAIL,
|
|
209
|
+
detail=(
|
|
210
|
+
f"engine {resolved} has no {EVAL_EXECUTABLE} entrypoint at "
|
|
211
|
+
f"{executable}; install the engine again"
|
|
212
|
+
),
|
|
213
|
+
blocking=True,
|
|
214
|
+
metadata=metadata,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
descriptor = read_engine_descriptor(engine_registry.path(resolved))
|
|
218
|
+
metadata["verifiers_version"] = descriptor.verifiers_version
|
|
219
|
+
metadata["verifiers_revision"] = descriptor.verifiers_revision
|
|
220
|
+
return DoctorCheck(
|
|
221
|
+
id="execution_engine_eval",
|
|
222
|
+
label="Engine eval entrypoint",
|
|
223
|
+
status=CheckStatus.PASS,
|
|
224
|
+
detail=(
|
|
225
|
+
f"{executable} is present, holding Verifiers "
|
|
226
|
+
f"{descriptor.verifiers_version} at {descriptor.verifiers_revision}"
|
|
227
|
+
),
|
|
228
|
+
blocking=False,
|
|
229
|
+
metadata=metadata,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def check_prime_auth(model: ModelSpec) -> DoctorCheck:
|
|
234
|
+
"""Confirm a run could authenticate, without reading anything.
|
|
235
|
+
|
|
236
|
+
The question is asked about the environment a detached run would be given,
|
|
237
|
+
not about the terminal this check was typed into. Those differ in exactly
|
|
238
|
+
one way that matters: a credential exported in a terminal is not among the
|
|
239
|
+
variables a run inherits, so a check that consulted its own environment
|
|
240
|
+
could report "ready" about a run that stops at its first model call. When
|
|
241
|
+
the export is there and unusable the check says so, because a person who
|
|
242
|
+
exported it has every reason to expect it to count.
|
|
243
|
+
"""
|
|
244
|
+
status = credential_status(model, environ=worker_visible_environment())
|
|
245
|
+
exported = bool(os.environ.get(model.credential_env))
|
|
246
|
+
detail = status.detail
|
|
247
|
+
if not status.available and exported:
|
|
248
|
+
detail = (
|
|
249
|
+
f"{model.credential_env} is set in this terminal, but a run works in "
|
|
250
|
+
"a separate background process that is not given this terminal's "
|
|
251
|
+
f"variables, so nothing exported here can reach it: {detail}"
|
|
252
|
+
)
|
|
253
|
+
return DoctorCheck(
|
|
254
|
+
id="execution_model_credential",
|
|
255
|
+
label="Evaluation model credential",
|
|
256
|
+
status=CheckStatus.PASS if status.available else CheckStatus.FAIL,
|
|
257
|
+
detail=detail,
|
|
258
|
+
blocking=not status.available,
|
|
259
|
+
metadata={
|
|
260
|
+
"provider": status.provider,
|
|
261
|
+
"model_id": model.model_id,
|
|
262
|
+
"credential_env": status.credential_env,
|
|
263
|
+
"source": status.source,
|
|
264
|
+
"exported_in_this_terminal": exported,
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def check_subject_image(runtime: RuntimeSpec) -> DoctorCheck:
|
|
270
|
+
"""Report whether the pinned subject image is on this machine, as pinned.
|
|
271
|
+
|
|
272
|
+
Absent is a failure with an argument vector attached, not a prompt to pull.
|
|
273
|
+
Downloading an image is an explicit setup operation the operator asks for
|
|
274
|
+
(spec section 6.18); a check that did it silently would spend somebody's
|
|
275
|
+
bandwidth to make its own answer come out right.
|
|
276
|
+
|
|
277
|
+
Present-but-different is a failure too. The Campaign pins the image by
|
|
278
|
+
content and pins one manifest digest per platform it supports, so a daemon
|
|
279
|
+
holding something else, or serving the pin on a platform the Campaign never
|
|
280
|
+
recorded, cannot run this Campaign — and finding that out here is much
|
|
281
|
+
cheaper than finding it out from a comparison that has already been paid
|
|
282
|
+
for.
|
|
283
|
+
"""
|
|
284
|
+
metadata: dict[str, JsonValue] = {"image": runtime.image}
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
resolution = resolve_subject_image(runtime, VariantName.BASELINE)
|
|
288
|
+
except ValidationError as refusal:
|
|
289
|
+
return DoctorCheck(
|
|
290
|
+
id="execution_subject_image",
|
|
291
|
+
label="Subject runtime image",
|
|
292
|
+
status=CheckStatus.FAIL,
|
|
293
|
+
detail=refusal.message,
|
|
294
|
+
blocking=True,
|
|
295
|
+
metadata={**metadata, "pull": f"docker pull {runtime.image}"},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
metadata["image_platform"] = resolution.platform
|
|
299
|
+
metadata["image_platform_digest"] = runtime.image_platform_digests[
|
|
300
|
+
resolution.platform
|
|
301
|
+
]
|
|
302
|
+
return DoctorCheck(
|
|
303
|
+
id="execution_subject_image",
|
|
304
|
+
label="Subject runtime image",
|
|
305
|
+
status=CheckStatus.PASS,
|
|
306
|
+
detail=(
|
|
307
|
+
f"the daemon holds the pinned subject image and serves it as "
|
|
308
|
+
f"{resolution.platform}, which the Campaign pins a manifest digest "
|
|
309
|
+
"for"
|
|
310
|
+
),
|
|
311
|
+
blocking=False,
|
|
312
|
+
metadata=metadata,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def check_live_campaign(campaign: CampaignSpec) -> DoctorCheck:
|
|
317
|
+
"""Refuse a Campaign whose coordinates are development placeholders.
|
|
318
|
+
|
|
319
|
+
A development fixture names a model that exists to be compiled and dry-run,
|
|
320
|
+
never to be executed. Catching that here is the difference between a clear
|
|
321
|
+
refusal and a container that starts, authenticates against nothing, and
|
|
322
|
+
fails at the first model call.
|
|
323
|
+
"""
|
|
324
|
+
subject = campaign.agents.get(SUBJECT_AGENT)
|
|
325
|
+
metadata: dict[str, JsonValue] = {"campaign_id": campaign.metadata.id}
|
|
326
|
+
|
|
327
|
+
if subject is None:
|
|
328
|
+
return DoctorCheck(
|
|
329
|
+
id="execution_live_campaign",
|
|
330
|
+
label="Campaign is executable",
|
|
331
|
+
status=CheckStatus.FAIL,
|
|
332
|
+
detail=f"the Campaign defines no {SUBJECT_AGENT!r} agent to execute",
|
|
333
|
+
blocking=True,
|
|
334
|
+
metadata=metadata,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
metadata["model_id"] = subject.model.model_id
|
|
338
|
+
metadata["provider"] = subject.model.provider
|
|
339
|
+
metadata["image"] = subject.runtime.image
|
|
340
|
+
|
|
341
|
+
placeholders = sorted(
|
|
342
|
+
{
|
|
343
|
+
f"{field}={value}"
|
|
344
|
+
for field, value in (
|
|
345
|
+
("provider", subject.model.provider),
|
|
346
|
+
("model_id", subject.model.model_id),
|
|
347
|
+
("image", subject.runtime.image),
|
|
348
|
+
)
|
|
349
|
+
for marker in DEVELOPMENT_PLACEHOLDER_MARKERS
|
|
350
|
+
if marker in value
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
if placeholders:
|
|
354
|
+
return DoctorCheck(
|
|
355
|
+
id="execution_live_campaign",
|
|
356
|
+
label="Campaign is executable",
|
|
357
|
+
status=CheckStatus.FAIL,
|
|
358
|
+
detail=(
|
|
359
|
+
"this Campaign carries development placeholders and must not be "
|
|
360
|
+
f"executed for real: {', '.join(placeholders)}"
|
|
361
|
+
),
|
|
362
|
+
blocking=True,
|
|
363
|
+
metadata=metadata,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return DoctorCheck(
|
|
367
|
+
id="execution_live_campaign",
|
|
368
|
+
label="Campaign is executable",
|
|
369
|
+
status=CheckStatus.PASS,
|
|
370
|
+
detail=(
|
|
371
|
+
f"the Campaign names a real subject: {subject.model.model_id} on "
|
|
372
|
+
f"{subject.runtime.image}"
|
|
373
|
+
),
|
|
374
|
+
blocking=False,
|
|
375
|
+
metadata=metadata,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def execution_checks(
|
|
380
|
+
*,
|
|
381
|
+
engine_registry: EngineRegistry,
|
|
382
|
+
campaign: CampaignSpec | None = None,
|
|
383
|
+
engine_digest: Digest | None = None,
|
|
384
|
+
) -> list[DoctorCheck]:
|
|
385
|
+
"""Run every execution check, in a deterministic order.
|
|
386
|
+
|
|
387
|
+
Without a Campaign only the machine-level questions can be asked, which is
|
|
388
|
+
the honest answer for ``techtree doctor --for-evaluation`` run before a
|
|
389
|
+
Climb has been chosen.
|
|
390
|
+
"""
|
|
391
|
+
checks = [
|
|
392
|
+
check_docker_platform(),
|
|
393
|
+
check_engine_eval(engine_registry, engine_digest),
|
|
394
|
+
]
|
|
395
|
+
if campaign is None:
|
|
396
|
+
return checks
|
|
397
|
+
|
|
398
|
+
checks.append(check_live_campaign(campaign))
|
|
399
|
+
subject = campaign.agents.get(SUBJECT_AGENT)
|
|
400
|
+
if subject is not None:
|
|
401
|
+
checks.append(check_prime_auth(subject.model))
|
|
402
|
+
checks.append(check_subject_image(subject.runtime))
|
|
403
|
+
return checks
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _docker_platform() -> str | None:
|
|
407
|
+
"""Return the ``<os>/<arch>`` the daemon serves, or None if it did not answer."""
|
|
408
|
+
return _run(
|
|
409
|
+
["docker", "version", "--format", "{{.Server.Os}}/{{.Server.Arch}}"],
|
|
410
|
+
timeout=DAEMON_TIMEOUT_SECONDS,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _run(argv: list[str], *, timeout: float = VERSION_TIMEOUT_SECONDS) -> str | None:
|
|
415
|
+
"""Run one argument vector and return its first line of output, or None."""
|
|
416
|
+
executable = shutil.which(argv[0])
|
|
417
|
+
if executable is None:
|
|
418
|
+
return None
|
|
419
|
+
try:
|
|
420
|
+
completed = subprocess.run(
|
|
421
|
+
[executable, *argv[1:]],
|
|
422
|
+
capture_output=True,
|
|
423
|
+
text=True,
|
|
424
|
+
timeout=timeout,
|
|
425
|
+
check=False,
|
|
426
|
+
stdin=subprocess.DEVNULL,
|
|
427
|
+
)
|
|
428
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
429
|
+
return None
|
|
430
|
+
if completed.returncode != 0:
|
|
431
|
+
return None
|
|
432
|
+
for line in completed.stdout.splitlines():
|
|
433
|
+
if line.strip():
|
|
434
|
+
return line.strip()
|
|
435
|
+
return None
|