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
techtree/runs/real.py
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
"""The executor that actually evaluates something. Spec section 6.17.
|
|
2
|
+
|
|
3
|
+
Everything the development executor invented, this one measures. What it does
|
|
4
|
+
*not* do is decide what the measurements mean: it returns a
|
|
5
|
+
:class:`~techtree.verifiers.models.RealExecutionResult` — two variants, their
|
|
6
|
+
raw evidence, and the engine's own normalized projection of it — and WP7 turns
|
|
7
|
+
that into receipts, a comparison and a report. Spec section 6.22 states the
|
|
8
|
+
boundary as a rule: WP6 does not invent final uplift.
|
|
9
|
+
|
|
10
|
+
The sequence in :meth:`RealVerifiersExecutor.execute` is section 6.17's,
|
|
11
|
+
unchanged, and its order is the whole safety argument. Every cheap refusal
|
|
12
|
+
happens before every expensive one. The staged inputs are verified before the
|
|
13
|
+
engine is resolved; the engine is resolved before the credential is required;
|
|
14
|
+
the credential is required before the taskset is validated; the taskset is
|
|
15
|
+
validated before a configuration is compiled; and both configurations survive a
|
|
16
|
+
model-free dry run against the real engine before a single container starts. A
|
|
17
|
+
run that is going to fail should fail while it is still free.
|
|
18
|
+
|
|
19
|
+
Two things this module owns that the specification's sketch leaves implicit.
|
|
20
|
+
|
|
21
|
+
*The skill mount is materialized at its content address.* The run stages the
|
|
22
|
+
candidate's skill tree as the artifact describes it; the compiler mounts a
|
|
23
|
+
skill from a directory named after its root digest, because the folder name is
|
|
24
|
+
part of what the subject sees and must therefore be a property of the skill
|
|
25
|
+
rather than of whoever prepared it. Reconciling the two is a verified copy
|
|
26
|
+
inside the run's own input tree, never a link to anything a participant could
|
|
27
|
+
still edit.
|
|
28
|
+
|
|
29
|
+
*A development-placeholder Campaign is refused here, not warned about.* Spec
|
|
30
|
+
section 16 requires that the fake executor stop being what a real Campaign
|
|
31
|
+
gets; the converse is that the real executor must not be what a placeholder
|
|
32
|
+
Campaign gets. One predicate answers both questions, and
|
|
33
|
+
:func:`campaign_is_executable` is it.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import contextlib
|
|
39
|
+
from collections.abc import Mapping, Sequence
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Final, Protocol
|
|
43
|
+
|
|
44
|
+
from techtree.canonical import sha256_digest_bytes, to_json_value
|
|
45
|
+
from techtree.doctor.execution_checks import check_live_campaign
|
|
46
|
+
from techtree.engines.bundle import default_engine_digest, read_engine_descriptor
|
|
47
|
+
from techtree.engines.registry import EngineRegistry
|
|
48
|
+
from techtree.engines.runner import EngineRunner
|
|
49
|
+
from techtree.errors import (
|
|
50
|
+
EngineError,
|
|
51
|
+
PrerequisiteError,
|
|
52
|
+
ValidationError,
|
|
53
|
+
VerificationError,
|
|
54
|
+
)
|
|
55
|
+
from techtree.fs import atomic_write_json, ensure_private_directory, open_exclusive
|
|
56
|
+
from techtree.models.base import Digest, JsonValue
|
|
57
|
+
from techtree.models.campaign import (
|
|
58
|
+
SUBJECT_AGENT,
|
|
59
|
+
AgentSpec,
|
|
60
|
+
CampaignSpec,
|
|
61
|
+
ModelSpec,
|
|
62
|
+
VariantSchedule,
|
|
63
|
+
)
|
|
64
|
+
from techtree.models.cli import CheckStatus
|
|
65
|
+
from techtree.models.engine import EngineDescriptor, EngineInstallation
|
|
66
|
+
from techtree.models.evaluation_backend import SUPPORTED_EVALUATION_BACKEND_KINDS
|
|
67
|
+
from techtree.models.experiment import ExperimentManifest
|
|
68
|
+
from techtree.models.run import ExecutorKind, RunPhase, RunRequest
|
|
69
|
+
from techtree.models.skill import SkillArtifact
|
|
70
|
+
from techtree.paths import TechtreePaths
|
|
71
|
+
from techtree.runs.artifacts import RunInputBundle
|
|
72
|
+
from techtree.runs.child_registry import ChildRegistry, EvaluationChild, execution_dir
|
|
73
|
+
from techtree.runs.executor import ExecutionContext
|
|
74
|
+
from techtree.runs.validation import TasksetValidationOutcome
|
|
75
|
+
from techtree.runs.variants import (
|
|
76
|
+
DEFAULT_POLL_INTERVAL_SECONDS,
|
|
77
|
+
VariantPair,
|
|
78
|
+
VariantPairOutcome,
|
|
79
|
+
VariantScheduler,
|
|
80
|
+
require_concurrency_budget,
|
|
81
|
+
)
|
|
82
|
+
from techtree.tasksets.service import TASKSET_DIRECTORY
|
|
83
|
+
from techtree.verifiers.budget import (
|
|
84
|
+
price_profile_for,
|
|
85
|
+
require_cost_bound,
|
|
86
|
+
require_executable_budget,
|
|
87
|
+
)
|
|
88
|
+
from techtree.verifiers.child import (
|
|
89
|
+
DEFAULT_GRACE_SECONDS,
|
|
90
|
+
VerifiersChild,
|
|
91
|
+
eval_argv,
|
|
92
|
+
)
|
|
93
|
+
from techtree.verifiers.compiler import (
|
|
94
|
+
compile_plans,
|
|
95
|
+
compile_variant_config,
|
|
96
|
+
skill_directory_name,
|
|
97
|
+
write_variant_config,
|
|
98
|
+
)
|
|
99
|
+
from techtree.verifiers.credentials import (
|
|
100
|
+
require_credentials,
|
|
101
|
+
scrubbed_child_environment,
|
|
102
|
+
)
|
|
103
|
+
from techtree.verifiers.image import resolve_subject_image
|
|
104
|
+
from techtree.verifiers.models import (
|
|
105
|
+
ChildProcessOutcome,
|
|
106
|
+
RealExecutionResult,
|
|
107
|
+
RunPaths,
|
|
108
|
+
SubjectImageResolution,
|
|
109
|
+
VariantExecutionPlan,
|
|
110
|
+
VariantExecutionResult,
|
|
111
|
+
VariantName,
|
|
112
|
+
)
|
|
113
|
+
from techtree.verifiers.outputs import (
|
|
114
|
+
DEFAULT_NORMALIZE_TIMEOUT_SECONDS,
|
|
115
|
+
build_variant_result,
|
|
116
|
+
)
|
|
117
|
+
from techtree.verifiers.verify import (
|
|
118
|
+
DEFAULT_DRY_RUN_TIMEOUT_SECONDS,
|
|
119
|
+
dry_run_variant_config,
|
|
120
|
+
verify_variant_execution,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
"REAL_EXECUTION_RESULT_FILENAME",
|
|
125
|
+
"REAL_EXECUTION_UNSUPPORTED",
|
|
126
|
+
"TASKSET_LOCK_FILENAME",
|
|
127
|
+
"VARIANT_NOT_USABLE",
|
|
128
|
+
"ChildFactory",
|
|
129
|
+
"RealVerifiersExecutor",
|
|
130
|
+
"campaign_is_executable",
|
|
131
|
+
"executor_kind_for",
|
|
132
|
+
"keep_evaluation_private",
|
|
133
|
+
"real_execution_result_path",
|
|
134
|
+
"require_bounded_campaign",
|
|
135
|
+
"require_live_campaign",
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
#: Stable error codes. Spec section 6.17's failure policy reports through these.
|
|
139
|
+
REAL_EXECUTION_UNSUPPORTED: Final = "real_execution_unsupported"
|
|
140
|
+
VARIANT_NOT_USABLE: Final = "variant_execution_not_usable"
|
|
141
|
+
|
|
142
|
+
#: The run's own copy of the membership the normalizer joins episodes onto.
|
|
143
|
+
#: Spec section 6.19.
|
|
144
|
+
TASKSET_LOCK_FILENAME: Final = "taskset-lock.json"
|
|
145
|
+
|
|
146
|
+
#: What WP6 hands WP7, written where WP7 will look for it. Spec section 6.19.
|
|
147
|
+
REAL_EXECUTION_RESULT_FILENAME: Final = "real-execution-result.json"
|
|
148
|
+
|
|
149
|
+
#: Owner read and write only, matching every other file Techtree writes.
|
|
150
|
+
_PRIVATE_FILE_MODE: Final = 0o600
|
|
151
|
+
|
|
152
|
+
#: Owner traversal only, matching every directory Techtree creates.
|
|
153
|
+
_PRIVATE_DIRECTORY_MODE: Final = 0o700
|
|
154
|
+
|
|
155
|
+
#: Both sides, always in comparison order.
|
|
156
|
+
_VARIANT_ORDER: Final[tuple[VariantName, ...]] = (
|
|
157
|
+
VariantName.BASELINE,
|
|
158
|
+
VariantName.CANDIDATE,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ChildFactory(Protocol):
|
|
163
|
+
"""How one variant's evaluation child is constructed.
|
|
164
|
+
|
|
165
|
+
:class:`~techtree.verifiers.child.VerifiersChild` is the implementation and
|
|
166
|
+
the default. The seam exists so that the executor's own sequence can be
|
|
167
|
+
exercised without a container or a provider, which is the only way to test
|
|
168
|
+
a code path whose real form spends money.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __call__(
|
|
172
|
+
self,
|
|
173
|
+
*,
|
|
174
|
+
variant: VariantName,
|
|
175
|
+
argv: Sequence[str],
|
|
176
|
+
cwd: Path,
|
|
177
|
+
env: Mapping[str, str],
|
|
178
|
+
stdout_path: Path,
|
|
179
|
+
stderr_path: Path,
|
|
180
|
+
supervision_record_path: Path,
|
|
181
|
+
) -> EvaluationChild:
|
|
182
|
+
"""Build one unstarted child."""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(frozen=True)
|
|
187
|
+
class _ResolvedEngine:
|
|
188
|
+
"""The engine this run will execute, and the facts read back from it."""
|
|
189
|
+
|
|
190
|
+
digest: Digest
|
|
191
|
+
installation: EngineInstallation
|
|
192
|
+
descriptor: EngineDescriptor
|
|
193
|
+
runner: EngineRunner
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Which executor a Campaign gets
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def campaign_is_executable(campaign: CampaignSpec) -> bool:
|
|
202
|
+
"""Whether this Campaign names a subject that may be executed for real.
|
|
203
|
+
|
|
204
|
+
One predicate, consulted both by the worker choosing an executor and by the
|
|
205
|
+
real executor refusing to start. The Campaign this build ships names the
|
|
206
|
+
ratified release subject (decisions 0025/0029), so the predicate answers
|
|
207
|
+
yes for the packaged bytes; a development-placeholder Campaign — any
|
|
208
|
+
Campaign whose subject cannot actually be paid for and executed — remains
|
|
209
|
+
a thing to compile and dry-run, never a thing to run.
|
|
210
|
+
"""
|
|
211
|
+
return check_live_campaign(campaign).status is CheckStatus.PASS
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def executor_kind_for(campaign: CampaignSpec) -> ExecutorKind:
|
|
215
|
+
"""Return the name of the executor this Campaign will be run by.
|
|
216
|
+
|
|
217
|
+
The same predicate that routes the worker, answered early enough to be
|
|
218
|
+
written down. A run records this when it is created, which is the moment a
|
|
219
|
+
person is shown what is about to happen, so the request says whether real
|
|
220
|
+
containers and a real provider are about to be used or whether this is the
|
|
221
|
+
development executor inventing numbers.
|
|
222
|
+
|
|
223
|
+
Recording it does not make it true — the worker re-reads the run's own
|
|
224
|
+
staged Campaign and asks this same question again, and the fake executor
|
|
225
|
+
refuses a request that names anything but itself. What recording it buys
|
|
226
|
+
is that nobody has to run a Campaign through a predicate to find out what a
|
|
227
|
+
finished run was.
|
|
228
|
+
"""
|
|
229
|
+
return "verifiers" if campaign_is_executable(campaign) else "fake"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def require_live_campaign(campaign: CampaignSpec) -> None:
|
|
233
|
+
"""Refuse a Campaign whose coordinates are development placeholders."""
|
|
234
|
+
check = check_live_campaign(campaign)
|
|
235
|
+
if check.status is CheckStatus.PASS:
|
|
236
|
+
return
|
|
237
|
+
raise PrerequisiteError(
|
|
238
|
+
f"this Campaign cannot be executed for real: {check.detail}",
|
|
239
|
+
code=REAL_EXECUTION_UNSUPPORTED,
|
|
240
|
+
details={"campaign_id": campaign.metadata.id},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def require_bounded_campaign(campaign: CampaignSpec) -> None:
|
|
245
|
+
"""Refuse to spend anything for a Campaign whose limits are not limits.
|
|
246
|
+
|
|
247
|
+
Both halves of decisions document 0029's layer A, asked in the one place
|
|
248
|
+
where a real run's first container is still one refusal away: every
|
|
249
|
+
declared execution limit is enforceable, and the most this comparison can
|
|
250
|
+
cost under those limits is not above what the Campaign says it may spend.
|
|
251
|
+
Asked here rather than when the run was created, because this is the
|
|
252
|
+
boundary between a run that has cost nothing and a run that is spending —
|
|
253
|
+
and it is the same boundary that already refuses a placeholder Campaign.
|
|
254
|
+
"""
|
|
255
|
+
require_executable_budget(campaign)
|
|
256
|
+
require_cost_bound(campaign, price_profile_for(campaign.subject.model.model_id))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def real_execution_result_path(run_root: Path) -> Path:
|
|
260
|
+
"""Return where one run records what WP6 hands WP7."""
|
|
261
|
+
return execution_dir(run_root) / REAL_EXECUTION_RESULT_FILENAME
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def keep_evaluation_private(run_paths: RunPaths) -> Path:
|
|
265
|
+
"""Make everything one run's engine produced readable by its owner only.
|
|
266
|
+
|
|
267
|
+
Spec section 6.19: raw Verifiers artifacts are mode ``0600`` and remain
|
|
268
|
+
local. Techtree writes its own capture files that way, but the engine
|
|
269
|
+
writes ``traces.jsonl``, ``eval.log``, the resolved configuration and the
|
|
270
|
+
taskset validator's output under the operator's ordinary umask — and
|
|
271
|
+
``traces.jsonl`` is the participant's complete subject transcripts.
|
|
272
|
+
Tightening them afterwards is the only moment available: the files belong
|
|
273
|
+
to the child while it is running, and a run that never reached them has
|
|
274
|
+
nothing to tighten.
|
|
275
|
+
|
|
276
|
+
Both trees the engine writes into are swept, not just the evaluation one.
|
|
277
|
+
``taskset/validation/`` is the validator's output and was left at the
|
|
278
|
+
operator's umask until WP11g S6 noticed; a task hash is not a transcript,
|
|
279
|
+
but "which of these two directories a stranger may read" is not a
|
|
280
|
+
distinction worth maintaining.
|
|
281
|
+
|
|
282
|
+
Directories are swept as well as files, because a directory the engine
|
|
283
|
+
created inside one of these trees carries the umask exactly as its
|
|
284
|
+
contents do.
|
|
285
|
+
|
|
286
|
+
Applied whether the run succeeded, failed or was cancelled: a partial
|
|
287
|
+
transcript is exactly as private as a complete one.
|
|
288
|
+
"""
|
|
289
|
+
root = run_paths.verifiers_dir
|
|
290
|
+
for tree in (root, run_paths.root / TASKSET_DIRECTORY):
|
|
291
|
+
if not tree.is_dir():
|
|
292
|
+
continue
|
|
293
|
+
_make_private(tree)
|
|
294
|
+
for path in tree.rglob("*"):
|
|
295
|
+
_make_private(path)
|
|
296
|
+
return root
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _make_private(path: Path) -> None:
|
|
300
|
+
"""Tighten one path the engine left behind, whatever kind it is."""
|
|
301
|
+
if path.is_symlink():
|
|
302
|
+
# Never chmod through a link: the target may not be the run's.
|
|
303
|
+
return
|
|
304
|
+
with contextlib.suppress(OSError):
|
|
305
|
+
if path.is_dir():
|
|
306
|
+
path.chmod(_PRIVATE_DIRECTORY_MODE)
|
|
307
|
+
elif path.is_file():
|
|
308
|
+
path.chmod(_PRIVATE_FILE_MODE)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
# The executor
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class RealVerifiersExecutor:
|
|
317
|
+
"""Executes both variants of a real Campaign through the pinned engine."""
|
|
318
|
+
|
|
319
|
+
def __init__(
|
|
320
|
+
self,
|
|
321
|
+
*,
|
|
322
|
+
paths: TechtreePaths,
|
|
323
|
+
engine_registry: EngineRegistry,
|
|
324
|
+
child_registry: ChildRegistry,
|
|
325
|
+
engine_digest: Digest | None = None,
|
|
326
|
+
child_factory: ChildFactory | None = None,
|
|
327
|
+
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL_SECONDS,
|
|
328
|
+
grace_seconds: float = DEFAULT_GRACE_SECONDS,
|
|
329
|
+
dry_run_timeout_seconds: float = DEFAULT_DRY_RUN_TIMEOUT_SECONDS,
|
|
330
|
+
normalize_timeout_seconds: float = DEFAULT_NORMALIZE_TIMEOUT_SECONDS,
|
|
331
|
+
) -> None:
|
|
332
|
+
self._paths = paths
|
|
333
|
+
self._registry = engine_registry
|
|
334
|
+
self._child_registry = child_registry
|
|
335
|
+
self._engine_digest = engine_digest or default_engine_digest()
|
|
336
|
+
self._child_factory: ChildFactory = child_factory or VerifiersChild
|
|
337
|
+
self._poll_interval = poll_interval_seconds
|
|
338
|
+
self._grace = grace_seconds
|
|
339
|
+
self._dry_run_timeout = dry_run_timeout_seconds
|
|
340
|
+
self._normalize_timeout = normalize_timeout_seconds
|
|
341
|
+
|
|
342
|
+
def execute(self, context: ExecutionContext) -> RealExecutionResult:
|
|
343
|
+
"""Execute one real Campaign's two variants. Spec section 6.17."""
|
|
344
|
+
request = context.request
|
|
345
|
+
run_id = request.run_id
|
|
346
|
+
run_paths = RunPaths.for_run(self._paths, run_id)
|
|
347
|
+
|
|
348
|
+
# 1-3. The run's own inputs, verified, and the rights it runs under.
|
|
349
|
+
inputs = context.artifact_store.load_inputs(run_id, request)
|
|
350
|
+
campaign = inputs.campaign
|
|
351
|
+
self._require_acknowledged_policy(request, campaign)
|
|
352
|
+
self._require_supported_backend(campaign)
|
|
353
|
+
require_live_campaign(campaign)
|
|
354
|
+
subject = self._subject(campaign)
|
|
355
|
+
|
|
356
|
+
# 4-5. The engine, and the credential the subject's calls are paid with.
|
|
357
|
+
engine = self._resolve_engine()
|
|
358
|
+
require_credentials(subject.model)
|
|
359
|
+
|
|
360
|
+
# 5a. What the Campaign may spend, and whether it can be held to it.
|
|
361
|
+
# After the credential rather than before it, because both refusals are
|
|
362
|
+
# free and this is the order the two are useful in: a machine that
|
|
363
|
+
# cannot pay for any run at all is told that first, and a Campaign
|
|
364
|
+
# whose limits are not limits is refused before the first container
|
|
365
|
+
# either way (decisions document 0029, layer A).
|
|
366
|
+
require_bounded_campaign(campaign)
|
|
367
|
+
|
|
368
|
+
# 6-7. Local taskset validation, and the membership it commits to.
|
|
369
|
+
validation = self._validate_taskset(context, inputs)
|
|
370
|
+
lock_path = self._write_taskset_lock(run_paths, validation)
|
|
371
|
+
|
|
372
|
+
# 8-11. Both configurations compiled and survived by the real engine,
|
|
373
|
+
# and what this machine's daemon holds for the container they pin —
|
|
374
|
+
# asked for both variants before either is launched, so that a missing
|
|
375
|
+
# or unexpected image costs nothing rather than half a comparison.
|
|
376
|
+
self._materialize_skill_mounts(inputs, run_paths)
|
|
377
|
+
pair = self._compile_pair(campaign, inputs, run_paths, engine, subject.model)
|
|
378
|
+
images = {
|
|
379
|
+
variant: resolve_subject_image(subject.runtime, variant)
|
|
380
|
+
for variant in _VARIANT_ORDER
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
# 12-15. Both children, started and watched.
|
|
385
|
+
outcome = self._run_children(
|
|
386
|
+
context, campaign=campaign, pair=pair, engine=engine, subject=subject
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# 16-18. The engine's own reading of what each child left behind.
|
|
390
|
+
results = self._normalize(
|
|
391
|
+
pair=pair,
|
|
392
|
+
outcome=outcome,
|
|
393
|
+
images=images,
|
|
394
|
+
inputs=inputs,
|
|
395
|
+
validation=validation,
|
|
396
|
+
engine=engine,
|
|
397
|
+
lock_path=lock_path,
|
|
398
|
+
primary_reward=campaign.scoring.primary_reward,
|
|
399
|
+
)
|
|
400
|
+
return self._record(run_paths, engine, outcome, results)
|
|
401
|
+
finally:
|
|
402
|
+
keep_evaluation_private(run_paths)
|
|
403
|
+
|
|
404
|
+
# -- preconditions ------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
def _require_acknowledged_policy(
|
|
407
|
+
self, request: RunRequest, campaign: CampaignSpec
|
|
408
|
+
) -> None:
|
|
409
|
+
"""Refuse to execute under rights nobody accepted for this Campaign."""
|
|
410
|
+
acknowledged = request.policy_acknowledgement.data_policy_digest
|
|
411
|
+
if acknowledged == campaign.data_policy_digest:
|
|
412
|
+
return
|
|
413
|
+
raise ValidationError(
|
|
414
|
+
"the DataPolicy this run acknowledged is not the one its Campaign "
|
|
415
|
+
"is governed by",
|
|
416
|
+
code=REAL_EXECUTION_UNSUPPORTED,
|
|
417
|
+
details={
|
|
418
|
+
"run_id": request.run_id,
|
|
419
|
+
"acknowledged": acknowledged,
|
|
420
|
+
"campaign": campaign.data_policy_digest,
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def _require_supported_backend(self, campaign: CampaignSpec) -> None:
|
|
425
|
+
"""Refuse a Campaign this build has no evaluation backend for."""
|
|
426
|
+
kind = campaign.evaluation_backend.kind
|
|
427
|
+
if kind in SUPPORTED_EVALUATION_BACKEND_KINDS:
|
|
428
|
+
return
|
|
429
|
+
raise ValidationError(
|
|
430
|
+
f"this Campaign is evaluated by {kind.value}, which this build "
|
|
431
|
+
"does not run",
|
|
432
|
+
code=REAL_EXECUTION_UNSUPPORTED,
|
|
433
|
+
details={"evaluation_backend": kind.value},
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def _subject(self, campaign: CampaignSpec) -> AgentSpec:
|
|
437
|
+
"""Return the evaluated agent, or refuse a Campaign that names none."""
|
|
438
|
+
subject = campaign.agents.get(SUBJECT_AGENT)
|
|
439
|
+
if subject is None:
|
|
440
|
+
raise ValidationError(
|
|
441
|
+
f"this Campaign defines no {SUBJECT_AGENT!r} agent to execute",
|
|
442
|
+
code=REAL_EXECUTION_UNSUPPORTED,
|
|
443
|
+
details={"campaign_id": campaign.metadata.id},
|
|
444
|
+
)
|
|
445
|
+
return subject
|
|
446
|
+
|
|
447
|
+
def _resolve_engine(self) -> _ResolvedEngine:
|
|
448
|
+
"""Resolve the pinned engine and require it to be verified in place."""
|
|
449
|
+
status = self._registry.status(self._engine_digest)
|
|
450
|
+
if not status.installed or not status.verified:
|
|
451
|
+
raise EngineError(
|
|
452
|
+
"the pinned evaluation engine is not installed and verified on "
|
|
453
|
+
"this machine",
|
|
454
|
+
code="engine_not_verified",
|
|
455
|
+
details={
|
|
456
|
+
"engine_digest": self._engine_digest,
|
|
457
|
+
"installed": status.installed,
|
|
458
|
+
"verified": status.verified,
|
|
459
|
+
},
|
|
460
|
+
)
|
|
461
|
+
installation = self._registry.installation(self._engine_digest)
|
|
462
|
+
if installation is None:
|
|
463
|
+
raise EngineError(
|
|
464
|
+
"the pinned evaluation engine records no installation",
|
|
465
|
+
code="engine_not_verified",
|
|
466
|
+
details={"engine_digest": self._engine_digest},
|
|
467
|
+
)
|
|
468
|
+
return _ResolvedEngine(
|
|
469
|
+
digest=self._engine_digest,
|
|
470
|
+
installation=installation,
|
|
471
|
+
descriptor=read_engine_descriptor(self._registry.path(self._engine_digest)),
|
|
472
|
+
runner=EngineRunner(self._registry, self._engine_digest),
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# -- taskset ------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
def _validate_taskset(
|
|
478
|
+
self, context: ExecutionContext, inputs: RunInputBundle
|
|
479
|
+
) -> TasksetValidationOutcome:
|
|
480
|
+
"""Validate the taskset locally and require it to be the committed one."""
|
|
481
|
+
run_id = context.request.run_id
|
|
482
|
+
context.run_store.append(run_id, phase=RunPhase.VALIDATING_TASKSET)
|
|
483
|
+
validation = context.validation_provider.validate(run_id=run_id, inputs=inputs)
|
|
484
|
+
context.artifact_store.write_validation_marker(
|
|
485
|
+
run_id, validation.marker_document()
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if validation.receipt.status != "valid":
|
|
489
|
+
raise VerificationError(
|
|
490
|
+
"the taskset validation this run was given reports "
|
|
491
|
+
f"{validation.receipt.status}, so nothing may be scored on it",
|
|
492
|
+
code=VARIANT_NOT_USABLE,
|
|
493
|
+
details={"run_id": run_id, "status": validation.receipt.status},
|
|
494
|
+
)
|
|
495
|
+
if validation.lock.ordered_task_hashes != inputs.ordered_task_hashes:
|
|
496
|
+
raise VerificationError(
|
|
497
|
+
"the validated taskset does not hold the tasks the Campaign commits to",
|
|
498
|
+
code=VARIANT_NOT_USABLE,
|
|
499
|
+
details={"run_id": run_id},
|
|
500
|
+
)
|
|
501
|
+
return validation
|
|
502
|
+
|
|
503
|
+
def _write_taskset_lock(
|
|
504
|
+
self, run_paths: RunPaths, validation: TasksetValidationOutcome
|
|
505
|
+
) -> Path:
|
|
506
|
+
"""Write the run's own copy of the membership the normalizer joins on."""
|
|
507
|
+
path = run_paths.inputs_dir / TASKSET_LOCK_FILENAME
|
|
508
|
+
if path.is_file():
|
|
509
|
+
return path
|
|
510
|
+
ensure_private_directory(path.parent)
|
|
511
|
+
with open_exclusive(path) as handle:
|
|
512
|
+
handle.write(validation.lock.model_dump_json().encode("utf-8"))
|
|
513
|
+
return path
|
|
514
|
+
|
|
515
|
+
# -- inputs the subject actually reads ----------------------------------
|
|
516
|
+
|
|
517
|
+
def _materialize_skill_mounts(
|
|
518
|
+
self, inputs: RunInputBundle, run_paths: RunPaths
|
|
519
|
+
) -> None:
|
|
520
|
+
"""Place each declared skill at the content address the compiler mounts.
|
|
521
|
+
|
|
522
|
+
The bytes are re-verified against the artifact as they are copied, so
|
|
523
|
+
what the subject reads is provably the tree the run's manifests name
|
|
524
|
+
rather than whatever is sitting in the staged directory by then.
|
|
525
|
+
|
|
526
|
+
A Skill insertion places one tree; a Skill replacement places two,
|
|
527
|
+
because its baseline carries the Skill being revised. Which is which
|
|
528
|
+
never has to be decided here: a skill is found by the content address
|
|
529
|
+
the variant declares, among the skills the run owns, so a variant
|
|
530
|
+
asking for anything the run did not stage stops the run.
|
|
531
|
+
"""
|
|
532
|
+
owned = {
|
|
533
|
+
staged.artifact.root_digest: staged for staged in inputs.declared_skills
|
|
534
|
+
}
|
|
535
|
+
for manifest in (inputs.baseline, inputs.candidate):
|
|
536
|
+
subject = manifest.configuration.agents.get(SUBJECT_AGENT)
|
|
537
|
+
if subject is None:
|
|
538
|
+
continue
|
|
539
|
+
for reference in subject.harness.skills:
|
|
540
|
+
staged = owned.get(reference.digest)
|
|
541
|
+
if staged is None:
|
|
542
|
+
raise VerificationError(
|
|
543
|
+
"a variant declares a skill this run does not own",
|
|
544
|
+
code=VARIANT_NOT_USABLE,
|
|
545
|
+
details={
|
|
546
|
+
"declared": reference.digest,
|
|
547
|
+
"staged": ", ".join(sorted(owned)),
|
|
548
|
+
},
|
|
549
|
+
)
|
|
550
|
+
self._copy_skill_tree(
|
|
551
|
+
skill=staged.artifact,
|
|
552
|
+
source=staged.files,
|
|
553
|
+
destination=run_paths.skill_files_dir
|
|
554
|
+
/ skill_directory_name(reference.digest),
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def _copy_skill_tree(
|
|
558
|
+
self, *, skill: SkillArtifact, source: Path, destination: Path
|
|
559
|
+
) -> None:
|
|
560
|
+
"""Copy one skill's files under a content-addressed directory name."""
|
|
561
|
+
for entry in skill.files:
|
|
562
|
+
target = destination / entry.path
|
|
563
|
+
if target.is_file():
|
|
564
|
+
continue
|
|
565
|
+
ensure_private_directory(target.parent)
|
|
566
|
+
data = (source / entry.path).read_bytes()
|
|
567
|
+
if len(data) != entry.size or sha256_digest_bytes(data) != entry.digest:
|
|
568
|
+
raise VerificationError(
|
|
569
|
+
f"this run's copy of the skill file {entry.path} is not "
|
|
570
|
+
"what the artifact says it is, so it is not what the "
|
|
571
|
+
"subject may be given",
|
|
572
|
+
code=VARIANT_NOT_USABLE,
|
|
573
|
+
details={"path": entry.path},
|
|
574
|
+
)
|
|
575
|
+
with open_exclusive(target) as handle:
|
|
576
|
+
handle.write(data)
|
|
577
|
+
|
|
578
|
+
# -- compilation --------------------------------------------------------
|
|
579
|
+
|
|
580
|
+
def _compile_pair(
|
|
581
|
+
self,
|
|
582
|
+
campaign: CampaignSpec,
|
|
583
|
+
inputs: RunInputBundle,
|
|
584
|
+
run_paths: RunPaths,
|
|
585
|
+
engine: _ResolvedEngine,
|
|
586
|
+
model: ModelSpec,
|
|
587
|
+
) -> VariantPair:
|
|
588
|
+
"""Compile, write and dry-run both variants' configurations.
|
|
589
|
+
|
|
590
|
+
``compile_plans`` divides the Campaign's concurrency and returns the
|
|
591
|
+
plans; the configuration itself is recompiled per variant from the
|
|
592
|
+
permits the plan settled on. Compilation is a pure function of the
|
|
593
|
+
manifest, the run paths and that number, so the two agree by
|
|
594
|
+
construction rather than by assertion — and the recompilation is what
|
|
595
|
+
gives the dry run the exact document that was written.
|
|
596
|
+
"""
|
|
597
|
+
baseline_plan, candidate_plan = compile_plans(
|
|
598
|
+
campaign=campaign,
|
|
599
|
+
baseline=inputs.baseline,
|
|
600
|
+
candidate=inputs.candidate,
|
|
601
|
+
run_paths=run_paths,
|
|
602
|
+
)
|
|
603
|
+
pair = VariantPair(baseline=baseline_plan, candidate=candidate_plan)
|
|
604
|
+
require_concurrency_budget(
|
|
605
|
+
pair, max_concurrent=campaign.execution.max_concurrent
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
manifests = {
|
|
609
|
+
VariantName.BASELINE: inputs.baseline,
|
|
610
|
+
VariantName.CANDIDATE: inputs.candidate,
|
|
611
|
+
}
|
|
612
|
+
for variant in _VARIANT_ORDER:
|
|
613
|
+
plan = pair.plan(variant)
|
|
614
|
+
compiled = compile_variant_config(
|
|
615
|
+
campaign=campaign,
|
|
616
|
+
experiment=manifests[variant],
|
|
617
|
+
run_paths=run_paths,
|
|
618
|
+
variant=variant,
|
|
619
|
+
variant_max_concurrent=plan.max_concurrent,
|
|
620
|
+
)
|
|
621
|
+
input_path = run_paths.variant_input_config(variant)
|
|
622
|
+
write_variant_config(compiled, input_path)
|
|
623
|
+
|
|
624
|
+
outcome = dry_run_variant_config(
|
|
625
|
+
engine_runner=engine.runner,
|
|
626
|
+
variant=variant,
|
|
627
|
+
compiled=compiled,
|
|
628
|
+
input_config_path=input_path,
|
|
629
|
+
dry_run_dir=run_paths.variant_dry_run_dir(variant),
|
|
630
|
+
model=model,
|
|
631
|
+
timeout=self._dry_run_timeout,
|
|
632
|
+
)
|
|
633
|
+
if not outcome.ok:
|
|
634
|
+
failures: list[JsonValue] = [
|
|
635
|
+
to_json_value(check) for check in outcome.failures
|
|
636
|
+
]
|
|
637
|
+
raise ValidationError(
|
|
638
|
+
f"the {variant.value} configuration did not survive the "
|
|
639
|
+
"engine's own validation, so nothing was executed",
|
|
640
|
+
code=VARIANT_NOT_USABLE,
|
|
641
|
+
details={"variant": variant.value, "checks": failures},
|
|
642
|
+
)
|
|
643
|
+
return pair
|
|
644
|
+
|
|
645
|
+
# -- execution ----------------------------------------------------------
|
|
646
|
+
|
|
647
|
+
def _run_children(
|
|
648
|
+
self,
|
|
649
|
+
context: ExecutionContext,
|
|
650
|
+
*,
|
|
651
|
+
campaign: CampaignSpec,
|
|
652
|
+
pair: VariantPair,
|
|
653
|
+
engine: _ResolvedEngine,
|
|
654
|
+
subject: AgentSpec,
|
|
655
|
+
) -> VariantPairOutcome:
|
|
656
|
+
"""Start and watch both variants under the Campaign's own schedule."""
|
|
657
|
+
run_id = context.request.run_id
|
|
658
|
+
run_paths = RunPaths.for_run(self._paths, run_id)
|
|
659
|
+
environment = scrubbed_child_environment(
|
|
660
|
+
model=subject.model, engine=engine.installation
|
|
661
|
+
)
|
|
662
|
+
executable = self._registry.executable(engine.digest, "eval")
|
|
663
|
+
children = {
|
|
664
|
+
variant: self._build_child(
|
|
665
|
+
variant=variant,
|
|
666
|
+
plan=pair.plan(variant),
|
|
667
|
+
executable=executable,
|
|
668
|
+
environment=environment,
|
|
669
|
+
run_paths=run_paths,
|
|
670
|
+
)
|
|
671
|
+
for variant in _VARIANT_ORDER
|
|
672
|
+
}
|
|
673
|
+
scheduler = VariantScheduler(
|
|
674
|
+
run_store=context.run_store,
|
|
675
|
+
child_registry=self._child_registry,
|
|
676
|
+
poll_interval_seconds=self._poll_interval,
|
|
677
|
+
grace_seconds=self._grace,
|
|
678
|
+
)
|
|
679
|
+
run = (
|
|
680
|
+
scheduler.execute_parallel
|
|
681
|
+
if campaign.execution.order is VariantSchedule.PARALLEL
|
|
682
|
+
else scheduler.execute_sequential
|
|
683
|
+
)
|
|
684
|
+
return run(
|
|
685
|
+
run_id=run_id,
|
|
686
|
+
run_root=run_paths.root,
|
|
687
|
+
pair=pair,
|
|
688
|
+
baseline_child=children[VariantName.BASELINE],
|
|
689
|
+
candidate_child=children[VariantName.CANDIDATE],
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
def _build_child(
|
|
693
|
+
self,
|
|
694
|
+
*,
|
|
695
|
+
variant: VariantName,
|
|
696
|
+
plan: VariantExecutionPlan,
|
|
697
|
+
executable: Path,
|
|
698
|
+
environment: Mapping[str, str],
|
|
699
|
+
run_paths: RunPaths,
|
|
700
|
+
) -> EvaluationChild:
|
|
701
|
+
"""Construct one unstarted evaluation child."""
|
|
702
|
+
return self._child_factory(
|
|
703
|
+
variant=variant,
|
|
704
|
+
argv=eval_argv(
|
|
705
|
+
eval_executable=executable,
|
|
706
|
+
input_config_path=Path(plan.verifiers_input_config_path),
|
|
707
|
+
),
|
|
708
|
+
cwd=Path(plan.verifiers_output_dir),
|
|
709
|
+
env=environment,
|
|
710
|
+
stdout_path=run_paths.variant_stdout_log(variant),
|
|
711
|
+
stderr_path=run_paths.variant_stderr_log(variant),
|
|
712
|
+
supervision_record_path=run_paths.variant_supervision_record(variant),
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# -- the engine's reading of the evidence -------------------------------
|
|
716
|
+
|
|
717
|
+
def _normalize(
|
|
718
|
+
self,
|
|
719
|
+
*,
|
|
720
|
+
pair: VariantPair,
|
|
721
|
+
outcome: VariantPairOutcome,
|
|
722
|
+
images: Mapping[VariantName, SubjectImageResolution],
|
|
723
|
+
inputs: RunInputBundle,
|
|
724
|
+
validation: TasksetValidationOutcome,
|
|
725
|
+
engine: _ResolvedEngine,
|
|
726
|
+
lock_path: Path,
|
|
727
|
+
primary_reward: str,
|
|
728
|
+
) -> dict[VariantName, VariantExecutionResult]:
|
|
729
|
+
"""Normalize and verify both variants, or fail the whole execution."""
|
|
730
|
+
outcomes: dict[VariantName, ChildProcessOutcome] = {
|
|
731
|
+
VariantName.BASELINE: outcome.baseline,
|
|
732
|
+
VariantName.CANDIDATE: outcome.candidate,
|
|
733
|
+
}
|
|
734
|
+
manifests: dict[VariantName, ExperimentManifest] = {
|
|
735
|
+
VariantName.BASELINE: inputs.baseline,
|
|
736
|
+
VariantName.CANDIDATE: inputs.candidate,
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
results: dict[VariantName, VariantExecutionResult] = {}
|
|
740
|
+
for variant in _VARIANT_ORDER:
|
|
741
|
+
result = build_variant_result(
|
|
742
|
+
plan=pair.plan(variant),
|
|
743
|
+
outcome=outcomes[variant],
|
|
744
|
+
image_resolution=images[variant],
|
|
745
|
+
engine_registry=self._registry,
|
|
746
|
+
engine_digest=engine.digest,
|
|
747
|
+
engine_runner=engine.runner,
|
|
748
|
+
taskset_lock_path=lock_path,
|
|
749
|
+
timeout=self._normalize_timeout,
|
|
750
|
+
)
|
|
751
|
+
checks = verify_variant_execution(
|
|
752
|
+
result=result,
|
|
753
|
+
experiment=manifests[variant],
|
|
754
|
+
taskset_lock=validation.lock,
|
|
755
|
+
primary_reward=primary_reward,
|
|
756
|
+
engine=engine.descriptor,
|
|
757
|
+
)
|
|
758
|
+
failed = [check for check in checks if check.status == "failed"]
|
|
759
|
+
if failed:
|
|
760
|
+
detail: list[JsonValue] = [to_json_value(check) for check in failed]
|
|
761
|
+
raise VerificationError(
|
|
762
|
+
f"the {variant.value} variant's execution is not "
|
|
763
|
+
"scientifically usable",
|
|
764
|
+
code=VARIANT_NOT_USABLE,
|
|
765
|
+
details={"variant": variant.value, "checks": detail},
|
|
766
|
+
)
|
|
767
|
+
results[variant] = result
|
|
768
|
+
return results
|
|
769
|
+
|
|
770
|
+
def _record(
|
|
771
|
+
self,
|
|
772
|
+
run_paths: RunPaths,
|
|
773
|
+
engine: _ResolvedEngine,
|
|
774
|
+
outcome: VariantPairOutcome,
|
|
775
|
+
results: dict[VariantName, VariantExecutionResult],
|
|
776
|
+
) -> RealExecutionResult:
|
|
777
|
+
"""Assemble what WP6 hands WP7, and write it beside the run."""
|
|
778
|
+
result = RealExecutionResult(
|
|
779
|
+
execution_backend="verifiers",
|
|
780
|
+
engine_digest=engine.digest,
|
|
781
|
+
verifiers_revision=engine.descriptor.verifiers_revision,
|
|
782
|
+
schedule=outcome.schedule,
|
|
783
|
+
baseline=results[VariantName.BASELINE],
|
|
784
|
+
candidate=results[VariantName.CANDIDATE],
|
|
785
|
+
)
|
|
786
|
+
path = real_execution_result_path(run_paths.root)
|
|
787
|
+
ensure_private_directory(path.parent)
|
|
788
|
+
atomic_write_json(path, to_json_value(result))
|
|
789
|
+
return result
|