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,752 @@
|
|
|
1
|
+
"""Turning a directory into a prepared submission. Spec PR6 §6.8.
|
|
2
|
+
|
|
3
|
+
This is the only place where a participant's working directory becomes a
|
|
4
|
+
scientific input, and it is written on the assumption that everything about
|
|
5
|
+
that directory is provisional until it has been copied and hashed.
|
|
6
|
+
|
|
7
|
+
The order of operations is the safety property. Nothing is written where a
|
|
8
|
+
later command could find it until every check has passed: the Climb is
|
|
9
|
+
resolved, its status and this machine's compatibility are confirmed, the skill
|
|
10
|
+
is scanned, the files are copied into a staging directory and re-verified
|
|
11
|
+
against what was scanned, both manifests are derived from the Campaign, the
|
|
12
|
+
comparison is required to be controlled, the draft is built and digested, and
|
|
13
|
+
only then is the whole tree renamed into place by the draft store. A failure
|
|
14
|
+
anywhere leaves the drafts directory exactly as it was.
|
|
15
|
+
|
|
16
|
+
Three things this module deliberately does not do.
|
|
17
|
+
|
|
18
|
+
It does not re-implement the scanner. PR5 already decides what a skill may
|
|
19
|
+
contain and what a credential looks like; duplicating that logic here would
|
|
20
|
+
create two answers to one question.
|
|
21
|
+
|
|
22
|
+
It does not call a model, start a process, or open a socket. Preparation is
|
|
23
|
+
pure local computation, and the integration test asserts it.
|
|
24
|
+
|
|
25
|
+
It does not ask anyone to accept anything. The draft states the rights that
|
|
26
|
+
will have to be accepted — decisions document 0003 A5's
|
|
27
|
+
``PolicyAcceptanceRequirement`` — and the acceptance itself happens at start,
|
|
28
|
+
after the review of what the run would do has been shown, recorded as a
|
|
29
|
+
deliberate act.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
import uuid
|
|
36
|
+
from collections.abc import Callable
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
from pathlib import Path, PurePosixPath
|
|
40
|
+
from typing import Final, assert_never
|
|
41
|
+
|
|
42
|
+
from techtree.canonical import (
|
|
43
|
+
canonical_json_bytes,
|
|
44
|
+
digest_object,
|
|
45
|
+
sha256_digest_bytes,
|
|
46
|
+
)
|
|
47
|
+
from techtree.catalog.service import CatalogService
|
|
48
|
+
from techtree.constants import SKILL_SCHEMA_VERSION, SUBMISSION_DRAFT_SCHEMA_VERSION
|
|
49
|
+
from techtree.drafts.source import CampaignSource, StagedSkill
|
|
50
|
+
from techtree.drafts.store import DraftStore, utc_now
|
|
51
|
+
from techtree.errors import (
|
|
52
|
+
PolicyError,
|
|
53
|
+
PrerequisiteError,
|
|
54
|
+
ValidationError,
|
|
55
|
+
VerificationError,
|
|
56
|
+
)
|
|
57
|
+
from techtree.fs import atomic_write_bytes, ensure_private_directory, remove_tree
|
|
58
|
+
from techtree.ids import new_id
|
|
59
|
+
from techtree.manifests.builder import (
|
|
60
|
+
build_baseline_manifest,
|
|
61
|
+
build_candidate_manifest,
|
|
62
|
+
skill_content_digest,
|
|
63
|
+
)
|
|
64
|
+
from techtree.manifests.compare import assert_controlled_comparison, compare_manifests
|
|
65
|
+
from techtree.models.base import Digest
|
|
66
|
+
from techtree.models.campaign import CampaignSpec, PublicContext, VariantSchedule
|
|
67
|
+
from techtree.models.climb import ResolvedClimb
|
|
68
|
+
from techtree.models.data_policy import DataPolicy
|
|
69
|
+
from techtree.models.experiment import ExperimentManifest, ManifestComparison
|
|
70
|
+
from techtree.models.skill import (
|
|
71
|
+
PolicyAcceptanceRequirement,
|
|
72
|
+
SkillArtifact,
|
|
73
|
+
SkillFile,
|
|
74
|
+
SubmissionDraft,
|
|
75
|
+
)
|
|
76
|
+
from techtree.models.uplift_report import UpliftReport
|
|
77
|
+
from techtree.models.validation import TasksetValidationReceipt, ValidationEvidence
|
|
78
|
+
from techtree.paths import TechtreePaths
|
|
79
|
+
from techtree.runs.real import executor_kind_for
|
|
80
|
+
from techtree.skills.archive import build_deterministic_tar
|
|
81
|
+
from techtree.skills.policy import SkillPolicy, default_instruction_skill_policy
|
|
82
|
+
from techtree.skills.scanner import ScannedFile, SkillScanResult, scan_skill
|
|
83
|
+
from techtree.uplift.derive import (
|
|
84
|
+
derive_replacement_manifests,
|
|
85
|
+
derive_skill_replacement_campaign,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"PreparedDraft",
|
|
90
|
+
"ResolvedClimbBundle",
|
|
91
|
+
"SkillPreparationService",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
#: Stable error codes. Spec PR6 §6.10.
|
|
95
|
+
_CLIMB_NOT_PREPARABLE: Final = "climb_not_preparable"
|
|
96
|
+
_CANDIDATE_POLICY_VIOLATION: Final = "candidate_policy_violation"
|
|
97
|
+
_SKILL_INVALID: Final = "skill_invalid"
|
|
98
|
+
_SKILL_SNAPSHOT_FAILED: Final = "skill_snapshot_failed"
|
|
99
|
+
_PUBLISHER_EVIDENCE_MISSING: Final = "publisher_validation_evidence_missing"
|
|
100
|
+
|
|
101
|
+
#: Which Climbs accept submissions. A closed Climb is still readable; it is
|
|
102
|
+
#: simply not something a new candidate can be entered into.
|
|
103
|
+
_PREPARABLE_STATUSES: Final[frozenset[str]] = frozenset({"open", "development"})
|
|
104
|
+
|
|
105
|
+
#: Every variant runs the whole taskset once per rollout, and there are two
|
|
106
|
+
#: variants.
|
|
107
|
+
_VARIANTS: Final = 2
|
|
108
|
+
|
|
109
|
+
#: What a candidate label may be made of. Deliberately narrow: the label is
|
|
110
|
+
#: carried in a public artifact, so it holds a name and nothing that could be
|
|
111
|
+
#: read as a path, a flag, or markup.
|
|
112
|
+
_LABEL_PATTERN: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$")
|
|
113
|
+
|
|
114
|
+
#: Staging lives beside the drafts so the draft store's final placement is a
|
|
115
|
+
#: rename within one directory.
|
|
116
|
+
_STAGING_PREFIX: Final = ".prepare-"
|
|
117
|
+
|
|
118
|
+
_SKILL_DIR: Final = "skill"
|
|
119
|
+
_ARTIFACT_FILE: Final = "artifact.json"
|
|
120
|
+
_BUNDLE_FILE: Final = "bundle.tar"
|
|
121
|
+
_FILES_DIR: Final = "files"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class ResolvedClimbBundle:
|
|
126
|
+
"""A resolved Climb together with the evidence a draft has to carry."""
|
|
127
|
+
|
|
128
|
+
resolved: ResolvedClimb
|
|
129
|
+
validation_evidence: ValidationEvidence
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class PreparedDraft:
|
|
134
|
+
"""What preparation produced, and what it was prepared against."""
|
|
135
|
+
|
|
136
|
+
draft: SubmissionDraft
|
|
137
|
+
draft_digest: Digest
|
|
138
|
+
manifest_comparison: ManifestComparison
|
|
139
|
+
source: CampaignSource
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SkillPreparationService:
|
|
143
|
+
"""Builds one complete, immutable, offline-verifiable submission draft."""
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
*,
|
|
148
|
+
paths: TechtreePaths,
|
|
149
|
+
catalog: CatalogService,
|
|
150
|
+
draft_store: DraftStore,
|
|
151
|
+
skill_policy: SkillPolicy | None = None,
|
|
152
|
+
clock: Callable[[], datetime] = utc_now,
|
|
153
|
+
) -> None:
|
|
154
|
+
self._paths = paths
|
|
155
|
+
self._catalog = catalog
|
|
156
|
+
self._drafts = draft_store
|
|
157
|
+
self._policy = skill_policy or default_instruction_skill_policy()
|
|
158
|
+
self._clock = clock
|
|
159
|
+
|
|
160
|
+
def prepare(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
climb_reference: str,
|
|
164
|
+
skill_path: Path,
|
|
165
|
+
candidate_label: str | None = None,
|
|
166
|
+
) -> PreparedDraft:
|
|
167
|
+
"""Resolve, snapshot, derive, compare, and persist one draft."""
|
|
168
|
+
created_at = self._clock()
|
|
169
|
+
|
|
170
|
+
bundle = self._resolve(climb_reference)
|
|
171
|
+
resolved = bundle.resolved
|
|
172
|
+
self._require_preparable(resolved)
|
|
173
|
+
|
|
174
|
+
scan = self._scan(skill_path)
|
|
175
|
+
self._validate_candidate_policy(resolved, scan)
|
|
176
|
+
|
|
177
|
+
ensure_private_directory(self._paths.drafts_dir)
|
|
178
|
+
staging = self._paths.drafts_dir / f"{_STAGING_PREFIX}{uuid.uuid4().hex}"
|
|
179
|
+
|
|
180
|
+
source = CampaignSource.from_climb(resolved)
|
|
181
|
+
try:
|
|
182
|
+
ensure_private_directory(staging)
|
|
183
|
+
staged = self._snapshot_skill(
|
|
184
|
+
skill_dir=staging / _SKILL_DIR,
|
|
185
|
+
scan=scan,
|
|
186
|
+
candidate_label=candidate_label,
|
|
187
|
+
)
|
|
188
|
+
public_context = PublicContext(
|
|
189
|
+
kind="climb", climb_digest=resolved.climb_digest
|
|
190
|
+
)
|
|
191
|
+
baseline = build_baseline_manifest(
|
|
192
|
+
campaign=resolved.campaign,
|
|
193
|
+
campaign_digest=resolved.campaign_digest,
|
|
194
|
+
public_context=public_context,
|
|
195
|
+
created_at=created_at,
|
|
196
|
+
)
|
|
197
|
+
candidate = build_candidate_manifest(
|
|
198
|
+
campaign=resolved.campaign,
|
|
199
|
+
campaign_digest=resolved.campaign_digest,
|
|
200
|
+
skill=staged.artifact,
|
|
201
|
+
public_context=public_context,
|
|
202
|
+
created_at=created_at,
|
|
203
|
+
)
|
|
204
|
+
comparison = self._require_controlled(
|
|
205
|
+
baseline, candidate, resolved.campaign
|
|
206
|
+
)
|
|
207
|
+
draft = self._build_draft(
|
|
208
|
+
source=source,
|
|
209
|
+
skill=staged.artifact,
|
|
210
|
+
baseline=baseline,
|
|
211
|
+
candidate=candidate,
|
|
212
|
+
policy=self._build_policy_requirement(resolved.data_policy),
|
|
213
|
+
created_at=created_at,
|
|
214
|
+
warnings=self._warnings(resolved),
|
|
215
|
+
)
|
|
216
|
+
draft_digest = digest_object(draft)
|
|
217
|
+
|
|
218
|
+
self._drafts.create(
|
|
219
|
+
draft=draft,
|
|
220
|
+
baseline=baseline,
|
|
221
|
+
candidate=candidate,
|
|
222
|
+
comparison=comparison,
|
|
223
|
+
source=source,
|
|
224
|
+
validation_evidence=bundle.validation_evidence,
|
|
225
|
+
staged_candidate_skill=staged,
|
|
226
|
+
)
|
|
227
|
+
finally:
|
|
228
|
+
# The store renames its own staging tree into place; this one is
|
|
229
|
+
# always scratch, so it always goes away.
|
|
230
|
+
remove_tree(staging)
|
|
231
|
+
|
|
232
|
+
return PreparedDraft(
|
|
233
|
+
draft=draft,
|
|
234
|
+
draft_digest=draft_digest,
|
|
235
|
+
manifest_comparison=comparison,
|
|
236
|
+
source=source,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def prepare_replacement(
|
|
240
|
+
self,
|
|
241
|
+
*,
|
|
242
|
+
source_campaign: CampaignSpec,
|
|
243
|
+
data_policy: DataPolicy,
|
|
244
|
+
publisher_validation: TasksetValidationReceipt,
|
|
245
|
+
validation_evidence: ValidationEvidence,
|
|
246
|
+
source_report: UpliftReport,
|
|
247
|
+
baseline_skill: StagedSkill,
|
|
248
|
+
candidate_skill_path: Path,
|
|
249
|
+
candidate_label: str | None = None,
|
|
250
|
+
) -> PreparedDraft:
|
|
251
|
+
"""Prepare a Skill v1 against Skill v2 draft. Spec sections 7.19, 7.20.
|
|
252
|
+
|
|
253
|
+
Everything a public submission goes through happens here too, in the
|
|
254
|
+
same order and through the same store: the new Skill is scanned by the
|
|
255
|
+
same scanner, snapshotted by the same snapshotter, and compared by the
|
|
256
|
+
same comparison. Only two things differ, and both come from the
|
|
257
|
+
Campaign rather than from a caller.
|
|
258
|
+
|
|
259
|
+
*The Campaign is derived, not resolved.* Spec section 7.19 fixes every
|
|
260
|
+
scientific field to the source run's and changes exactly the mutation
|
|
261
|
+
contract and the Skill the baseline carries, so no public Climb wraps
|
|
262
|
+
it and the draft names no public context.
|
|
263
|
+
|
|
264
|
+
*The baseline carries a Skill.* ``baseline_skill`` is Skill v1 exactly
|
|
265
|
+
as it was evaluated, taken from the source run's own verified inputs
|
|
266
|
+
rather than rescanned from a directory that may have changed since,
|
|
267
|
+
and it is snapshotted beside the candidate because the subject has to
|
|
268
|
+
be handed its files.
|
|
269
|
+
"""
|
|
270
|
+
created_at = self._clock()
|
|
271
|
+
scan = self._scan(candidate_skill_path)
|
|
272
|
+
self._require_candidate_files(scan)
|
|
273
|
+
|
|
274
|
+
ensure_private_directory(self._paths.drafts_dir)
|
|
275
|
+
staging = self._paths.drafts_dir / f"{_STAGING_PREFIX}{uuid.uuid4().hex}"
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
ensure_private_directory(staging)
|
|
279
|
+
staged = self._snapshot_skill(
|
|
280
|
+
skill_dir=staging / _SKILL_DIR,
|
|
281
|
+
scan=scan,
|
|
282
|
+
candidate_label=candidate_label,
|
|
283
|
+
parent_skill_digest=baseline_skill.artifact.root_digest,
|
|
284
|
+
)
|
|
285
|
+
campaign = derive_skill_replacement_campaign(
|
|
286
|
+
source_campaign=source_campaign,
|
|
287
|
+
source_run=source_report,
|
|
288
|
+
baseline_skill=baseline_skill.artifact,
|
|
289
|
+
candidate_skill=staged.artifact,
|
|
290
|
+
)
|
|
291
|
+
source = CampaignSource.local(
|
|
292
|
+
campaign=campaign,
|
|
293
|
+
data_policy=data_policy,
|
|
294
|
+
publisher_validation=publisher_validation,
|
|
295
|
+
)
|
|
296
|
+
baseline, candidate, comparison = derive_replacement_manifests(
|
|
297
|
+
campaign=campaign,
|
|
298
|
+
candidate_skill=staged.artifact,
|
|
299
|
+
campaign_digest=source.campaign_digest,
|
|
300
|
+
created_at=created_at,
|
|
301
|
+
)
|
|
302
|
+
draft = self._build_draft(
|
|
303
|
+
source=source,
|
|
304
|
+
skill=staged.artifact,
|
|
305
|
+
baseline=baseline,
|
|
306
|
+
candidate=candidate,
|
|
307
|
+
policy=self._build_policy_requirement(data_policy),
|
|
308
|
+
created_at=created_at,
|
|
309
|
+
warnings=self._replacement_warnings(),
|
|
310
|
+
)
|
|
311
|
+
draft_digest = digest_object(draft)
|
|
312
|
+
|
|
313
|
+
self._drafts.create(
|
|
314
|
+
draft=draft,
|
|
315
|
+
baseline=baseline,
|
|
316
|
+
candidate=candidate,
|
|
317
|
+
comparison=comparison,
|
|
318
|
+
source=source,
|
|
319
|
+
validation_evidence=validation_evidence,
|
|
320
|
+
staged_candidate_skill=staged,
|
|
321
|
+
staged_baseline_skill=baseline_skill,
|
|
322
|
+
)
|
|
323
|
+
finally:
|
|
324
|
+
remove_tree(staging)
|
|
325
|
+
|
|
326
|
+
return PreparedDraft(
|
|
327
|
+
draft=draft,
|
|
328
|
+
draft_digest=draft_digest,
|
|
329
|
+
manifest_comparison=comparison,
|
|
330
|
+
source=source,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# -- Resolution and eligibility ----------------------------------------
|
|
334
|
+
|
|
335
|
+
def _resolve(self, climb_reference: str) -> ResolvedClimbBundle:
|
|
336
|
+
"""Resolve Climb, Campaign, DataPolicy, receipt, and evidence."""
|
|
337
|
+
resolved = self._catalog.get_climb(climb_reference)
|
|
338
|
+
reference = resolved.publisher_validation.normalized_evidence
|
|
339
|
+
if reference is None:
|
|
340
|
+
raise VerificationError(
|
|
341
|
+
"this Climb's publisher validation names no normalized "
|
|
342
|
+
"evidence, so a draft prepared from it could not be checked "
|
|
343
|
+
"offline",
|
|
344
|
+
code=_PUBLISHER_EVIDENCE_MISSING,
|
|
345
|
+
details={"climb_reference": climb_reference},
|
|
346
|
+
)
|
|
347
|
+
return ResolvedClimbBundle(
|
|
348
|
+
resolved=resolved,
|
|
349
|
+
validation_evidence=self._catalog.validation_evidence(resolved),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _require_preparable(self, resolved: ResolvedClimb) -> None:
|
|
353
|
+
"""Refuse a Climb that is closed, or that this machine cannot run."""
|
|
354
|
+
status = resolved.climb.metadata.status
|
|
355
|
+
if status not in _PREPARABLE_STATUSES:
|
|
356
|
+
raise PolicyError(
|
|
357
|
+
f"this Climb is {status}, so it is not accepting submissions",
|
|
358
|
+
code=_CLIMB_NOT_PREPARABLE,
|
|
359
|
+
details={"status": status},
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
compatibility = self._catalog.compatibility(resolved)
|
|
363
|
+
blocking = [issue for issue in compatibility.issues if issue.blocking]
|
|
364
|
+
if blocking:
|
|
365
|
+
raise PrerequisiteError(
|
|
366
|
+
"this machine cannot run this Climb yet: "
|
|
367
|
+
+ " ".join(issue.message for issue in blocking),
|
|
368
|
+
code=_CLIMB_NOT_PREPARABLE,
|
|
369
|
+
details={"blocking_issues": [issue.code for issue in blocking]},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _validate_candidate_policy(
|
|
373
|
+
self, resolved: ResolvedClimb, scan: SkillScanResult
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Enforce Climb candidate constraints and DataPolicy."""
|
|
376
|
+
constraints = resolved.climb.candidate_policy.constraints
|
|
377
|
+
if not constraints.min_skills <= 1 <= constraints.max_skills:
|
|
378
|
+
raise PolicyError(
|
|
379
|
+
"this Climb does not accept a single candidate skill; it asks "
|
|
380
|
+
f"for {constraints.min_skills} to {constraints.max_skills}",
|
|
381
|
+
code=_CANDIDATE_POLICY_VIOLATION,
|
|
382
|
+
details={
|
|
383
|
+
"min_skills": constraints.min_skills,
|
|
384
|
+
"max_skills": constraints.max_skills,
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
release = resolved.data_policy.candidate_skill.public_release
|
|
389
|
+
if release == "prohibited":
|
|
390
|
+
raise PolicyError(
|
|
391
|
+
"this Climb's DataPolicy prohibits releasing a candidate skill, "
|
|
392
|
+
"so there is nothing a submission could be entered as",
|
|
393
|
+
code=_CANDIDATE_POLICY_VIOLATION,
|
|
394
|
+
details={"candidate_skill_public_release": release},
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
self._require_candidate_files(scan)
|
|
398
|
+
|
|
399
|
+
def _require_candidate_files(self, scan: SkillScanResult) -> None:
|
|
400
|
+
"""Enforce the file-count ceiling every candidate is held to.
|
|
401
|
+
|
|
402
|
+
The Climb's own constraints and its public-release rule are checked
|
|
403
|
+
beside this one when a Climb is what invited the submission. A locally
|
|
404
|
+
derived replacement has neither, and inventing a public rule for a
|
|
405
|
+
private comparison would be inventing a policy nobody stated.
|
|
406
|
+
"""
|
|
407
|
+
if len(scan.files) > self._policy.maximum_files:
|
|
408
|
+
raise PolicyError(
|
|
409
|
+
f"this candidate has {len(scan.files)} files and the limit is "
|
|
410
|
+
f"{self._policy.maximum_files}",
|
|
411
|
+
code=_CANDIDATE_POLICY_VIOLATION,
|
|
412
|
+
details={"file_count": len(scan.files)},
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# -- The snapshot ------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
def _scan(self, skill_path: Path) -> SkillScanResult:
|
|
418
|
+
"""Run the PR5 scanner and report its refusals in PR6's vocabulary."""
|
|
419
|
+
try:
|
|
420
|
+
return scan_skill(skill_path, self._policy)
|
|
421
|
+
except ValidationError as error:
|
|
422
|
+
# The scanner's own message and details are kept; only the code is
|
|
423
|
+
# added here, so a caller can branch on it.
|
|
424
|
+
raise ValidationError(
|
|
425
|
+
error.message,
|
|
426
|
+
code=_SKILL_INVALID,
|
|
427
|
+
details=error.details,
|
|
428
|
+
) from error
|
|
429
|
+
|
|
430
|
+
def _snapshot_skill(
|
|
431
|
+
self,
|
|
432
|
+
*,
|
|
433
|
+
skill_dir: Path,
|
|
434
|
+
scan: SkillScanResult,
|
|
435
|
+
candidate_label: str | None,
|
|
436
|
+
parent_skill_digest: Digest | None = None,
|
|
437
|
+
) -> StagedSkill:
|
|
438
|
+
"""Create ``artifact.json``, ``bundle.tar``, and ``files/`` in staging.
|
|
439
|
+
|
|
440
|
+
Each file is read once, checked against what the scan recorded, and
|
|
441
|
+
written into the snapshot. The archive is then built from the copies
|
|
442
|
+
rather than from the originals, so the expanded tree and the archive
|
|
443
|
+
cannot disagree even if the source directory changes underneath us.
|
|
444
|
+
|
|
445
|
+
The staged ``artifact.json`` makes the staging skill directory complete
|
|
446
|
+
and self-describing. The draft store writes its own copy from
|
|
447
|
+
``draft.skill_artifact`` rather than moving this one, because the only
|
|
448
|
+
artifact document a draft may hold is the one the draft's own digest
|
|
449
|
+
commits to.
|
|
450
|
+
|
|
451
|
+
``parent_skill_digest`` names the Skill this one revises. It is set
|
|
452
|
+
for a replacement candidate and absent for an insertion, which is the
|
|
453
|
+
only lineage a ``SkillArtifact`` records.
|
|
454
|
+
"""
|
|
455
|
+
files_dir = skill_dir / _FILES_DIR
|
|
456
|
+
archive_path = skill_dir / _BUNDLE_FILE
|
|
457
|
+
ensure_private_directory(skill_dir)
|
|
458
|
+
ensure_private_directory(files_dir)
|
|
459
|
+
|
|
460
|
+
copied = [self._copy_one(item, files_dir) for item in scan.files]
|
|
461
|
+
archive_digest = build_deterministic_tar(copied, archive_path)
|
|
462
|
+
|
|
463
|
+
entries = [
|
|
464
|
+
SkillFile(
|
|
465
|
+
path=item.relative_path.as_posix(),
|
|
466
|
+
media_type=item.media_type,
|
|
467
|
+
size=item.size,
|
|
468
|
+
digest=item.digest,
|
|
469
|
+
)
|
|
470
|
+
for item in copied
|
|
471
|
+
]
|
|
472
|
+
artifact = SkillArtifact(
|
|
473
|
+
schema_version=SKILL_SCHEMA_VERSION,
|
|
474
|
+
name=_candidate_name(candidate_label, scan.root),
|
|
475
|
+
root_digest=skill_content_digest(entries),
|
|
476
|
+
archive_digest=archive_digest,
|
|
477
|
+
files=entries,
|
|
478
|
+
source_kind="manual",
|
|
479
|
+
parent_skill_digest=parent_skill_digest,
|
|
480
|
+
)
|
|
481
|
+
atomic_write_bytes(skill_dir / _ARTIFACT_FILE, canonical_json_bytes(artifact))
|
|
482
|
+
return StagedSkill(artifact=artifact, archive=archive_path, files=files_dir)
|
|
483
|
+
|
|
484
|
+
def _copy_one(self, item: ScannedFile, files_dir: Path) -> ScannedFile:
|
|
485
|
+
"""Copy one scanned file into the snapshot, or refuse the snapshot."""
|
|
486
|
+
try:
|
|
487
|
+
data = item.source_path.read_bytes()
|
|
488
|
+
except OSError as error:
|
|
489
|
+
raise VerificationError(
|
|
490
|
+
"a candidate skill file could not be read while it was being "
|
|
491
|
+
f"snapshotted: {item.relative_path}",
|
|
492
|
+
code=_SKILL_SNAPSHOT_FAILED,
|
|
493
|
+
details={"path": item.relative_path.as_posix()},
|
|
494
|
+
) from error
|
|
495
|
+
|
|
496
|
+
if len(data) != item.size or sha256_digest_bytes(data) != item.digest:
|
|
497
|
+
raise VerificationError(
|
|
498
|
+
"a candidate skill file changed while it was being "
|
|
499
|
+
f"snapshotted, so the draft was abandoned: {item.relative_path}",
|
|
500
|
+
code=_SKILL_SNAPSHOT_FAILED,
|
|
501
|
+
details={"path": item.relative_path.as_posix()},
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
target = files_dir / item.relative_path
|
|
505
|
+
ensure_private_directory(target.parent)
|
|
506
|
+
atomic_write_bytes(target, data)
|
|
507
|
+
return ScannedFile(
|
|
508
|
+
source_path=target,
|
|
509
|
+
relative_path=PurePosixPath(item.relative_path),
|
|
510
|
+
size=item.size,
|
|
511
|
+
media_type=item.media_type,
|
|
512
|
+
digest=item.digest,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# -- The science -------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
def _require_controlled(
|
|
518
|
+
self,
|
|
519
|
+
baseline: ExperimentManifest,
|
|
520
|
+
candidate: ExperimentManifest,
|
|
521
|
+
campaign: CampaignSpec,
|
|
522
|
+
) -> ManifestComparison:
|
|
523
|
+
"""Compare both variants and refuse a pair that measures more than one thing."""
|
|
524
|
+
comparison = compare_manifests(baseline, candidate, campaign.mutation_contract)
|
|
525
|
+
assert_controlled_comparison(comparison)
|
|
526
|
+
return comparison
|
|
527
|
+
|
|
528
|
+
def _build_policy_requirement(
|
|
529
|
+
self, data_policy: DataPolicy
|
|
530
|
+
) -> PolicyAcceptanceRequirement:
|
|
531
|
+
"""State the rights a participant will be asked to accept."""
|
|
532
|
+
return PolicyAcceptanceRequirement(
|
|
533
|
+
data_policy_digest=digest_object(data_policy),
|
|
534
|
+
required=True,
|
|
535
|
+
summary=rights_summary(data_policy),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def _build_draft(
|
|
539
|
+
self,
|
|
540
|
+
*,
|
|
541
|
+
source: CampaignSource,
|
|
542
|
+
skill: SkillArtifact,
|
|
543
|
+
baseline: ExperimentManifest,
|
|
544
|
+
candidate: ExperimentManifest,
|
|
545
|
+
policy: PolicyAcceptanceRequirement,
|
|
546
|
+
created_at: datetime,
|
|
547
|
+
warnings: list[str],
|
|
548
|
+
) -> SubmissionDraft:
|
|
549
|
+
"""Construct the immutable protocol draft."""
|
|
550
|
+
return SubmissionDraft(
|
|
551
|
+
schema_version=SUBMISSION_DRAFT_SCHEMA_VERSION,
|
|
552
|
+
id=new_id("draft"),
|
|
553
|
+
campaign_spec_digest=source.campaign_digest,
|
|
554
|
+
program_ref=source.campaign.context.program_ref,
|
|
555
|
+
public_context=source.public_context,
|
|
556
|
+
data_policy_digest=source.data_policy_digest,
|
|
557
|
+
outcome_contract_digest=source.campaign.context.outcome_contract_digest,
|
|
558
|
+
skill_artifact=skill,
|
|
559
|
+
baseline_manifest_digest=digest_object(baseline),
|
|
560
|
+
candidate_manifest_digest=digest_object(candidate),
|
|
561
|
+
included_files=[file.path for file in skill.files],
|
|
562
|
+
estimated_episodes=self._estimate_episodes(source.campaign),
|
|
563
|
+
policy_acceptance=policy,
|
|
564
|
+
warnings=warnings,
|
|
565
|
+
created_at=created_at,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
def _estimate_episodes(self, campaign: CampaignSpec) -> int:
|
|
569
|
+
"""Return tasks × rollouts × two variants."""
|
|
570
|
+
selection = campaign.taskset.selection
|
|
571
|
+
return selection.num_tasks * selection.num_rollouts * _VARIANTS
|
|
572
|
+
|
|
573
|
+
def _warnings(self, resolved: ResolvedClimb) -> list[str]:
|
|
574
|
+
"""Say everything a participant should know before confirming."""
|
|
575
|
+
warnings: list[str] = []
|
|
576
|
+
|
|
577
|
+
if resolved.climb.publication.proof_grade == "development_only":
|
|
578
|
+
warnings.append(
|
|
579
|
+
"This is a development Climb. Its results are for trying the "
|
|
580
|
+
"flow out and are not comparable evidence."
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
warnings.append(
|
|
584
|
+
"Results are attested by this machine only. Nobody else has "
|
|
585
|
+
"verified that this run happened as described."
|
|
586
|
+
)
|
|
587
|
+
warnings.append(_comparison_warning(resolved.campaign))
|
|
588
|
+
if executor_kind_for(resolved.campaign) == "fake":
|
|
589
|
+
warnings.append(
|
|
590
|
+
"Nothing produced here is a public proof. No agent is "
|
|
591
|
+
"evaluated and no model is called on this Climb's runs."
|
|
592
|
+
)
|
|
593
|
+
else:
|
|
594
|
+
warnings.append(
|
|
595
|
+
"Nothing produced here is a public proof. Starting this run "
|
|
596
|
+
"evaluates the agent for real and spends model tokens on "
|
|
597
|
+
"inference at the model provider you configured. A provider "
|
|
598
|
+
"that charges for tokens bills that use to your own account; "
|
|
599
|
+
"a model you run yourself sends no bill."
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
release = resolved.data_policy.candidate_skill.public_release
|
|
603
|
+
if release == "required_for_climb":
|
|
604
|
+
warnings.append(
|
|
605
|
+
"Entering this Climb requires releasing the candidate skill publicly."
|
|
606
|
+
)
|
|
607
|
+
elif release == "consent_required":
|
|
608
|
+
warnings.append(
|
|
609
|
+
"Releasing the candidate skill publicly needs your separate consent."
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
return warnings
|
|
613
|
+
|
|
614
|
+
def _replacement_warnings(self) -> list[str]:
|
|
615
|
+
"""Say what a local Skill-against-Skill comparison is, and is not.
|
|
616
|
+
|
|
617
|
+
None of the public warnings apply: there is no Climb to enter, nothing
|
|
618
|
+
is released, and no leaderboard is involved. What a reader has to know
|
|
619
|
+
instead is that the baseline is no longer "no Skill" — a candidate that
|
|
620
|
+
loses here lost to a Skill that already worked.
|
|
621
|
+
"""
|
|
622
|
+
return [
|
|
623
|
+
# First, because the rights summary this draft carries is the
|
|
624
|
+
# policy of the Climb the first run entered, and read on its own it
|
|
625
|
+
# would describe a public submission this is not.
|
|
626
|
+
"The data rights stated for this run are the ones the run it was "
|
|
627
|
+
"prepared from was carried out under. They still govern what "
|
|
628
|
+
"happens to this run's material, and they are why acceptance is "
|
|
629
|
+
"asked for again.",
|
|
630
|
+
"This comparison is local. No Climb wraps it, and nothing is "
|
|
631
|
+
"entered anywhere. Nothing is published unless you publish this "
|
|
632
|
+
"run yourself once it has finished, and what travels then is the "
|
|
633
|
+
"run's proof — the signed report and its receipts — and never the "
|
|
634
|
+
"episodes. Model inference still goes to the model provider you "
|
|
635
|
+
"configured, under that provider's policies.",
|
|
636
|
+
"This compares one Skill against another Skill, not against no "
|
|
637
|
+
"Skill. The baseline is the version measured by the run this was "
|
|
638
|
+
"prepared from.",
|
|
639
|
+
"Results are attested by this machine only. Nobody else has "
|
|
640
|
+
"verified that this run happened as described.",
|
|
641
|
+
]
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# ---------------------------------------------------------------------------
|
|
645
|
+
# How the comparison is run
|
|
646
|
+
# ---------------------------------------------------------------------------
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _comparison_warning(campaign: CampaignSpec) -> str:
|
|
650
|
+
"""Say how this Campaign runs its two sides, reading it off the Campaign.
|
|
651
|
+
|
|
652
|
+
The approval screen is where a person decides to spend model tokens on a
|
|
653
|
+
comparison, so the sentence describing how that comparison is controlled
|
|
654
|
+
has to be the one this Campaign will actually produce. It is therefore
|
|
655
|
+
derived from ``execution.order`` — the same field the executor dispatches
|
|
656
|
+
on when it picks between running the two sides side by side and running one
|
|
657
|
+
after the other (``runs/real.py``). Writing the sentence out by hand is how
|
|
658
|
+
it came to describe an order the Campaign had not asked for since the day
|
|
659
|
+
side-by-side execution arrived.
|
|
660
|
+
|
|
661
|
+
Exhaustive on purpose: a Campaign that grows a third way of running its two
|
|
662
|
+
sides fails to typecheck here rather than quietly inheriting a sentence
|
|
663
|
+
written for a different one.
|
|
664
|
+
"""
|
|
665
|
+
match campaign.execution.order:
|
|
666
|
+
case VariantSchedule.PARALLEL:
|
|
667
|
+
return (
|
|
668
|
+
"The baseline and the candidate launch in parallel against the "
|
|
669
|
+
"same committed task set, under matched configuration, so the "
|
|
670
|
+
"two are compared and not merely reported."
|
|
671
|
+
)
|
|
672
|
+
case VariantSchedule.SEQUENTIAL:
|
|
673
|
+
return (
|
|
674
|
+
"The baseline runs first and the candidate second, on the same "
|
|
675
|
+
"committed tasks, so the two are compared and not merely "
|
|
676
|
+
"reported."
|
|
677
|
+
)
|
|
678
|
+
assert_never(campaign.execution.order)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
# ---------------------------------------------------------------------------
|
|
682
|
+
# The rights summary
|
|
683
|
+
# ---------------------------------------------------------------------------
|
|
684
|
+
|
|
685
|
+
#: How each permission is said in a sentence. Fixed strings: the summary is
|
|
686
|
+
#: stored in a protocol artifact and must read the same way every time the same
|
|
687
|
+
#: policy is prepared against.
|
|
688
|
+
_PERMISSION_PHRASE: Final[dict[str, str]] = {
|
|
689
|
+
"allowed": "allowed",
|
|
690
|
+
"prohibited": "prohibited",
|
|
691
|
+
"consent_required": "only with your separate consent",
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
_RELEASE_PHRASE: Final[dict[str, str]] = {
|
|
695
|
+
"required_for_climb": "required in order to enter this Climb",
|
|
696
|
+
"allowed": "allowed",
|
|
697
|
+
"prohibited": "prohibited",
|
|
698
|
+
"consent_required": "only with your separate consent",
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
_OWNER_PHRASE: Final[dict[str, str]] = {
|
|
702
|
+
"participant": "You own",
|
|
703
|
+
"account": "Your account owns",
|
|
704
|
+
"shared": "You and Techtree jointly own",
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
_VISIBILITY_PHRASE: Final[dict[str, str]] = {
|
|
708
|
+
"public": "published",
|
|
709
|
+
"private": "kept private",
|
|
710
|
+
"prohibited": "not produced",
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def rights_summary(data_policy: DataPolicy) -> str:
|
|
715
|
+
"""Return the stable sentence-per-right summary shown before confirming.
|
|
716
|
+
|
|
717
|
+
Derived entirely from the policy, so it cannot describe rights the policy
|
|
718
|
+
does not grant, and worded from fixed phrases, so the same policy always
|
|
719
|
+
produces the same bytes.
|
|
720
|
+
"""
|
|
721
|
+
owner = _OWNER_PHRASE[data_policy.owner.kind]
|
|
722
|
+
raw = data_policy.raw_episodes
|
|
723
|
+
return " ".join(
|
|
724
|
+
[
|
|
725
|
+
f"{owner} the candidate skill and everything this run produces.",
|
|
726
|
+
"Publishing the candidate skill is "
|
|
727
|
+
f"{_RELEASE_PHRASE[data_policy.candidate_skill.public_release]}.",
|
|
728
|
+
"Uploading raw episodes to a server is "
|
|
729
|
+
f"{_PERMISSION_PHRASE[raw.server_upload]}.",
|
|
730
|
+
f"Training on raw episodes is {_PERMISSION_PHRASE[raw.training_use]}.",
|
|
731
|
+
"The uplift report is "
|
|
732
|
+
f"{_VISIBILITY_PHRASE[data_policy.derived_artifacts.uplift_report]}.",
|
|
733
|
+
(
|
|
734
|
+
"You can withdraw future uses later."
|
|
735
|
+
if data_policy.revocation.future_use_revocable
|
|
736
|
+
else "Future uses cannot be withdrawn later."
|
|
737
|
+
),
|
|
738
|
+
]
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _candidate_name(candidate_label: str | None, root: Path) -> str:
|
|
743
|
+
"""Return the name this candidate is filed under."""
|
|
744
|
+
label = (candidate_label or root.name).strip()
|
|
745
|
+
if _LABEL_PATTERN.fullmatch(label) is None:
|
|
746
|
+
raise ValidationError(
|
|
747
|
+
"a candidate label is up to 64 letters, digits, spaces, dots, "
|
|
748
|
+
"dashes, or underscores, and starts with a letter or a digit",
|
|
749
|
+
code=_SKILL_INVALID,
|
|
750
|
+
details={"label": label},
|
|
751
|
+
)
|
|
752
|
+
return label
|