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,52 @@
|
|
|
1
|
+
"""Turning a real evaluation into receipts. Spec sections 7.6-7.8.
|
|
2
|
+
|
|
3
|
+
This package is where measured evidence becomes a Techtree claim. Everything
|
|
4
|
+
above it — the child processes, the containers, the provider calls — has
|
|
5
|
+
finished by the time anything here runs, and nothing here may change what was
|
|
6
|
+
measured. Three rules shape every module:
|
|
7
|
+
|
|
8
|
+
*The engine's normalizer is the boundary.* Nothing in this package reads
|
|
9
|
+
``traces.jsonl``. The raw upstream record is read once, by the pinned Verifiers
|
|
10
|
+
build that wrote it, inside the engine bundle (spec section 6.12); this package
|
|
11
|
+
reads that projection and no other source. A receipt built by re-parsing
|
|
12
|
+
upstream wire records with a different library version would be an
|
|
13
|
+
interpretation wearing the authority of a measurement.
|
|
14
|
+
|
|
15
|
+
*Rewards are copied, never computed.* A reward reaches a receipt exactly as
|
|
16
|
+
Verifiers recorded it — not rescored, not rounded, not re-weighted. The only
|
|
17
|
+
decisions taken here are about *status*: whether the number may be presented as
|
|
18
|
+
a result at all.
|
|
19
|
+
|
|
20
|
+
*A structural doubt is a refusal, a recorded failure is a status.* An
|
|
21
|
+
evaluation missing a committed task, scoring one twice, or carrying a trace in
|
|
22
|
+
the wrong seat cannot be turned into receipts at all, and those are the typed
|
|
23
|
+
refusals of spec section 15. An episode that genuinely failed, or a trace whose
|
|
24
|
+
scoring genuinely errored, is recorded with an honest ``score_status`` — the
|
|
25
|
+
receipt exists and says what happened.
|
|
26
|
+
|
|
27
|
+
The division of labour:
|
|
28
|
+
|
|
29
|
+
``episode``
|
|
30
|
+
The parsing boundary and one immutable receipt per committed task.
|
|
31
|
+
``set``
|
|
32
|
+
The ordered, content-addressed commitment over one variant's receipts.
|
|
33
|
+
``observed``
|
|
34
|
+
What the engine actually resolved, fingerprinted for comparison against
|
|
35
|
+
what the manifest declared.
|
|
36
|
+
``compare``
|
|
37
|
+
Whether the two variants' executions were one experiment: declared against
|
|
38
|
+
declared, observed against observed, and observed against declared.
|
|
39
|
+
``uplift``
|
|
40
|
+
The paired rewards, the aggregate over them, and the report — which is the
|
|
41
|
+
only place in this package where two variants meet.
|
|
42
|
+
``bundle``
|
|
43
|
+
The portable proof: which documents travel, and the decisions-0005 section
|
|
44
|
+
3.4 conditions a report has to establish before it may be graded.
|
|
45
|
+
``verify``
|
|
46
|
+
Reading one of those bundles back and answering, offline and from its own
|
|
47
|
+
bytes, whether it still holds together.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""The portable local proof. Spec section 7.11.
|
|
2
|
+
|
|
3
|
+
A run directory holds everything: inputs, raw upstream traces, container logs,
|
|
4
|
+
the worker's own log. Most of it may not leave the machine — the DataPolicy
|
|
5
|
+
prohibits uploading or publishing raw episodes — and most of it is not what a
|
|
6
|
+
reader checking a claim needs. The proof bundle is the subset that travels:
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
runs/<run-id>/proof/
|
|
10
|
+
├── bundle.json signed manifest over everything below
|
|
11
|
+
├── executor-public.json the key the participant signed with
|
|
12
|
+
├── campaign.json
|
|
13
|
+
├── data-policy.json
|
|
14
|
+
├── taskset-lock.json
|
|
15
|
+
├── taskset-validation-receipt.json
|
|
16
|
+
├── baseline-experiment.json
|
|
17
|
+
├── candidate-experiment.json
|
|
18
|
+
├── baseline-receipt-set.json
|
|
19
|
+
├── candidate-receipt-set.json
|
|
20
|
+
├── receipts/{baseline,candidate}/NNNN.json signed EpisodeReceipt envelopes
|
|
21
|
+
├── comparison-execution.json the signed ComparisonExecutionRecord,
|
|
22
|
+
│ present only when the run recorded one
|
|
23
|
+
└── uplift-report.json the signed UpliftReport envelope
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
``comparison-execution.json`` is the one optional member. Decisions document
|
|
27
|
+
0007 R6 makes it the operational record of what the comparison *consumed* —
|
|
28
|
+
cost, timing, the provider's own figures — rather than of what it measured, so
|
|
29
|
+
a run whose economics could not be recorded still has a complete proof of its
|
|
30
|
+
result. It is listed here because it is written, verified and covered by the
|
|
31
|
+
signed manifest exactly like the others when it is there.
|
|
32
|
+
|
|
33
|
+
Raw ``traces.jsonl``, the upstream log and the worker log stay behind. The
|
|
34
|
+
receipts cite their digests, so a reader holding the run directory can check
|
|
35
|
+
them; a reader holding only the bundle cannot, and the bundle never pretends
|
|
36
|
+
otherwise.
|
|
37
|
+
|
|
38
|
+
Three decisions shape this module.
|
|
39
|
+
|
|
40
|
+
*Every file is canonical bytes.* The digest of a bundle file and the digest of
|
|
41
|
+
the object inside it are the same number, so verification hashes what is on
|
|
42
|
+
disk rather than what a parser reconstructed. That is what "verifiable from
|
|
43
|
+
exact stored bytes" has to mean to be worth saying.
|
|
44
|
+
|
|
45
|
+
*The manifest is signed too.* Section 7.11 describes ``bundle.json`` as the
|
|
46
|
+
manifest; it is stored as a signed envelope around it. Without that, the one
|
|
47
|
+
thing the artifact digests cannot protect is the artifact list itself: a
|
|
48
|
+
bundle whose manifest was rewritten to name a different set of files — a
|
|
49
|
+
report from another run of the same participant, say — would otherwise verify.
|
|
50
|
+
Signing it binds the list to the same key as everything it lists.
|
|
51
|
+
|
|
52
|
+
*P1 is a conclusion, never an assumption.* :data:`P1_CONDITIONS` is the
|
|
53
|
+
decisions-0005 section 3.4 list, spelled once. It is evaluated twice on
|
|
54
|
+
purpose: :func:`assess_local_attestation` decides, before a report exists,
|
|
55
|
+
whether this run is entitled to a signed grade at all, and
|
|
56
|
+
:func:`techtree.receipts.verify.verify_local_bundle` re-derives every condition
|
|
57
|
+
from the written bytes afterwards. A report claiming P1 whose bundle does not
|
|
58
|
+
re-establish all six is not recorded as a result.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
from __future__ import annotations
|
|
62
|
+
|
|
63
|
+
from collections.abc import Mapping, Sequence
|
|
64
|
+
from dataclasses import dataclass
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
from typing import Final, Literal, Self
|
|
67
|
+
|
|
68
|
+
from pydantic import BaseModel, model_validator
|
|
69
|
+
|
|
70
|
+
from techtree.canonical import canonical_json_bytes, digest_object, sha256_digest_bytes
|
|
71
|
+
from techtree.fs import atomic_write_bytes, ensure_private_directory
|
|
72
|
+
from techtree.identity.models import ExecutorIdentity
|
|
73
|
+
from techtree.identity.service import IdentityService, verify_signed_object
|
|
74
|
+
from techtree.models.base import (
|
|
75
|
+
ArtifactRef,
|
|
76
|
+
Digest,
|
|
77
|
+
NonEmptyString,
|
|
78
|
+
ObjectEnvelope,
|
|
79
|
+
ProtocolModel,
|
|
80
|
+
)
|
|
81
|
+
from techtree.models.campaign import CampaignSpec
|
|
82
|
+
from techtree.models.data_policy import DataPolicy
|
|
83
|
+
from techtree.models.episode_receipt import EpisodeReceipt, ScoreStatus
|
|
84
|
+
from techtree.models.experiment import ExperimentManifest, ExperimentVariant
|
|
85
|
+
from techtree.models.uplift_report import ComparisonStatus, UpliftReport
|
|
86
|
+
from techtree.models.validation import TasksetLock, TasksetValidationReceipt
|
|
87
|
+
from techtree.receipts.execution import (
|
|
88
|
+
EXECUTION_RECORD_FILENAME,
|
|
89
|
+
ComparisonExecutionRecord,
|
|
90
|
+
)
|
|
91
|
+
from techtree.receipts.set import ReceiptSetManifest
|
|
92
|
+
from techtree.receipts.uplift import LocalAttestation
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"BUNDLE_DIRECTORY",
|
|
96
|
+
"BUNDLE_MANIFEST_FILENAME",
|
|
97
|
+
"BUNDLE_MEDIA_TYPE",
|
|
98
|
+
"LOCAL_PROOF_BUNDLE_SCHEMA_VERSION",
|
|
99
|
+
"P1_ARTIFACT_DIGESTS_VERIFY",
|
|
100
|
+
"P1_COMPARISON_CONTROLLED",
|
|
101
|
+
"P1_CONDITIONS",
|
|
102
|
+
"P1_PUBLIC_KEY_PRESENT",
|
|
103
|
+
"P1_RECEIPTS_SIGNED",
|
|
104
|
+
"P1_REPORT_SIGNED",
|
|
105
|
+
"P1_SCORE_VALID",
|
|
106
|
+
"PROOF_BUNDLE_INVALID",
|
|
107
|
+
"PUBLIC_IDENTITY_FILENAME",
|
|
108
|
+
"REPORT_FILENAME",
|
|
109
|
+
"AttestationAssessment",
|
|
110
|
+
"LocalProofBundleContents",
|
|
111
|
+
"LocalProofBundleManifest",
|
|
112
|
+
"ProofCondition",
|
|
113
|
+
"ReferencedObject",
|
|
114
|
+
"assess_local_attestation",
|
|
115
|
+
"build_local_bundle",
|
|
116
|
+
"bundle_files",
|
|
117
|
+
"experiment_filename",
|
|
118
|
+
"proof_bundle_dir",
|
|
119
|
+
"receipt_filename",
|
|
120
|
+
"receipt_set_filename",
|
|
121
|
+
"write_local_bundle",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
#: A local bundle manifest, versioned as one: the frozen v0.1 protocol has no
|
|
125
|
+
#: proof-bundle object and inventing one would be an unratified amendment.
|
|
126
|
+
LOCAL_PROOF_BUNDLE_SCHEMA_VERSION: Final = "techtree.local-proof-bundle.v1alpha1"
|
|
127
|
+
|
|
128
|
+
#: Stable error code. Spec section 15.
|
|
129
|
+
PROOF_BUNDLE_INVALID: Final = "proof_bundle_invalid"
|
|
130
|
+
|
|
131
|
+
BUNDLE_DIRECTORY: Final = "proof"
|
|
132
|
+
BUNDLE_MANIFEST_FILENAME: Final = "bundle.json"
|
|
133
|
+
PUBLIC_IDENTITY_FILENAME: Final = "executor-public.json"
|
|
134
|
+
CAMPAIGN_FILENAME: Final = "campaign.json"
|
|
135
|
+
DATA_POLICY_FILENAME: Final = "data-policy.json"
|
|
136
|
+
TASKSET_LOCK_FILENAME: Final = "taskset-lock.json"
|
|
137
|
+
VALIDATION_RECEIPT_FILENAME: Final = "taskset-validation-receipt.json"
|
|
138
|
+
REPORT_FILENAME: Final = "uplift-report.json"
|
|
139
|
+
RECEIPTS_DIRECTORY: Final = "receipts"
|
|
140
|
+
|
|
141
|
+
BUNDLE_MEDIA_TYPE: Final = "application/json"
|
|
142
|
+
|
|
143
|
+
#: Receipt filenames are positional, so reading the directory in name order
|
|
144
|
+
#: reads the receipts back in the Campaign's committed task order.
|
|
145
|
+
_POSITION_WIDTH: Final = 4
|
|
146
|
+
|
|
147
|
+
_PASSED: Final = "passed"
|
|
148
|
+
_FAILED: Final = "failed"
|
|
149
|
+
|
|
150
|
+
#: Both sides, always in comparison order, so a bundle written twice from the
|
|
151
|
+
#: same run places its files identically.
|
|
152
|
+
_VARIANT_ORDER: Final[tuple[ExperimentVariant, ...]] = (
|
|
153
|
+
ExperimentVariant.BASELINE,
|
|
154
|
+
ExperimentVariant.CANDIDATE,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# The section 3.4 conditions, spelled once
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
#: "all referenced artifact digests verify"
|
|
162
|
+
P1_ARTIFACT_DIGESTS_VERIFY: Final = "artifact_digests_verify"
|
|
163
|
+
#: "all EpisodeReceipts are wrapped in signed ObjectEnvelopes"
|
|
164
|
+
P1_RECEIPTS_SIGNED: Final = "receipts_signed"
|
|
165
|
+
#: "the UpliftReport is wrapped in a signed ObjectEnvelope"
|
|
166
|
+
P1_REPORT_SIGNED: Final = "report_signed"
|
|
167
|
+
#: "the local public key is included in the bundle"
|
|
168
|
+
P1_PUBLIC_KEY_PRESENT: Final = "public_key_in_bundle"
|
|
169
|
+
#: "the comparison is controlled or controlled_with_warnings"
|
|
170
|
+
P1_COMPARISON_CONTROLLED: Final = "comparison_controlled"
|
|
171
|
+
#: "score status is valid"
|
|
172
|
+
P1_SCORE_VALID: Final = "score_valid"
|
|
173
|
+
|
|
174
|
+
#: Decisions document 0005 section 3.4, in the order it states them. Every one
|
|
175
|
+
#: is a condition on stored bytes; none of them is satisfied by a claim in a
|
|
176
|
+
#: document about itself.
|
|
177
|
+
P1_CONDITIONS: Final[tuple[str, ...]] = (
|
|
178
|
+
P1_ARTIFACT_DIGESTS_VERIFY,
|
|
179
|
+
P1_RECEIPTS_SIGNED,
|
|
180
|
+
P1_REPORT_SIGNED,
|
|
181
|
+
P1_PUBLIC_KEY_PRESENT,
|
|
182
|
+
P1_COMPARISON_CONTROLLED,
|
|
183
|
+
P1_SCORE_VALID,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ProofCondition(ProtocolModel):
|
|
188
|
+
"""One decisions-0005 section 3.4 condition and whether it holds."""
|
|
189
|
+
|
|
190
|
+
id: NonEmptyString
|
|
191
|
+
status: Literal["passed", "failed"]
|
|
192
|
+
detail: NonEmptyString
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class AttestationAssessment:
|
|
197
|
+
"""Whether this run may sign its report into a P1 grade, and why."""
|
|
198
|
+
|
|
199
|
+
attestation: LocalAttestation
|
|
200
|
+
conditions: list[ProofCondition]
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def failures(self) -> list[ProofCondition]:
|
|
204
|
+
"""Return every condition that does not hold."""
|
|
205
|
+
return [
|
|
206
|
+
condition for condition in self.conditions if condition.status == _FAILED
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True)
|
|
211
|
+
class ReferencedObject:
|
|
212
|
+
"""One object a report cites, together with the digest it cites it under."""
|
|
213
|
+
|
|
214
|
+
label: str
|
|
215
|
+
value: BaseModel
|
|
216
|
+
expected: Digest
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# The manifest
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class LocalProofBundleManifest(ProtocolModel):
|
|
225
|
+
"""Everything one portable local proof carries, committed to by digest."""
|
|
226
|
+
|
|
227
|
+
schema_version: Literal["techtree.local-proof-bundle.v1alpha1"]
|
|
228
|
+
run_id: NonEmptyString
|
|
229
|
+
campaign_spec_digest: Digest
|
|
230
|
+
data_policy_digest: Digest
|
|
231
|
+
executor_identity: ExecutorIdentity
|
|
232
|
+
artifacts: list[ArtifactRef]
|
|
233
|
+
root_report_digest: Digest
|
|
234
|
+
|
|
235
|
+
@model_validator(mode="after")
|
|
236
|
+
def _check_every_artifact_is_placed_once(self) -> Self:
|
|
237
|
+
"""Reject a manifest that names a file twice or names none."""
|
|
238
|
+
paths = [artifact.relative_path for artifact in self.artifacts]
|
|
239
|
+
if not paths:
|
|
240
|
+
raise ValueError("a proof bundle commits to at least one artifact")
|
|
241
|
+
if any(path is None for path in paths):
|
|
242
|
+
raise ValueError(
|
|
243
|
+
"every artifact in a proof bundle is placed by relative path; "
|
|
244
|
+
"a digest with nowhere to look is not checkable"
|
|
245
|
+
)
|
|
246
|
+
if len(set(paths)) != len(paths):
|
|
247
|
+
raise ValueError("a proof bundle places each artifact exactly once")
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def artifact(self, relative_path: str) -> ArtifactRef | None:
|
|
251
|
+
"""Return one placed artifact, or ``None`` when it is not carried."""
|
|
252
|
+
for reference in self.artifacts:
|
|
253
|
+
if reference.relative_path == relative_path:
|
|
254
|
+
return reference
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass(frozen=True)
|
|
259
|
+
class LocalProofBundleContents:
|
|
260
|
+
"""The objects one bundle is written from.
|
|
261
|
+
|
|
262
|
+
The receipts and the report arrive already signed. This module places
|
|
263
|
+
bytes; it does not decide what is worth signing, and it never holds private
|
|
264
|
+
key material.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
identity: ExecutorIdentity
|
|
268
|
+
campaign: CampaignSpec
|
|
269
|
+
data_policy: DataPolicy
|
|
270
|
+
taskset_lock: TasksetLock
|
|
271
|
+
validation_receipt: TasksetValidationReceipt
|
|
272
|
+
experiments: Mapping[ExperimentVariant, ExperimentManifest]
|
|
273
|
+
receipt_sets: Mapping[ExperimentVariant, ReceiptSetManifest]
|
|
274
|
+
receipts: Mapping[ExperimentVariant, Sequence[ObjectEnvelope[EpisodeReceipt]]]
|
|
275
|
+
report: ObjectEnvelope[UpliftReport]
|
|
276
|
+
#: Decisions document 0007 R6's operational record, already signed. It is
|
|
277
|
+
#: optional because it describes what the comparison consumed rather than
|
|
278
|
+
#: what it measured: a run that could not record its economics still has a
|
|
279
|
+
#: complete proof of its result, and R6 forbids treating one as the other.
|
|
280
|
+
execution_record: ObjectEnvelope[ComparisonExecutionRecord] | None = None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# Placement
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def proof_bundle_dir(run_root: Path) -> Path:
|
|
289
|
+
"""Return where one run's portable proof lives."""
|
|
290
|
+
return run_root / BUNDLE_DIRECTORY
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def experiment_filename(variant: ExperimentVariant) -> str:
|
|
294
|
+
"""Return one variant's experiment manifest filename."""
|
|
295
|
+
return f"{variant.value}-experiment.json"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def receipt_set_filename(variant: ExperimentVariant) -> str:
|
|
299
|
+
"""Return one variant's receipt-set manifest filename."""
|
|
300
|
+
return f"{variant.value}-receipt-set.json"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def receipt_filename(variant: ExperimentVariant, position: int) -> str:
|
|
304
|
+
"""Return one receipt's placement inside the bundle."""
|
|
305
|
+
return f"{RECEIPTS_DIRECTORY}/{variant.value}/{position:0{_POSITION_WIDTH}d}.json"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Assembly
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def bundle_files(contents: LocalProofBundleContents) -> dict[str, bytes]:
|
|
314
|
+
"""Return every bundle file except the manifest, as canonical bytes.
|
|
315
|
+
|
|
316
|
+
The manifest is excluded because it commits to these: a file cannot carry
|
|
317
|
+
the digest of a document that carries its digest.
|
|
318
|
+
"""
|
|
319
|
+
files: dict[str, bytes] = {
|
|
320
|
+
PUBLIC_IDENTITY_FILENAME: canonical_json_bytes(contents.identity),
|
|
321
|
+
CAMPAIGN_FILENAME: canonical_json_bytes(contents.campaign),
|
|
322
|
+
DATA_POLICY_FILENAME: canonical_json_bytes(contents.data_policy),
|
|
323
|
+
TASKSET_LOCK_FILENAME: canonical_json_bytes(contents.taskset_lock),
|
|
324
|
+
VALIDATION_RECEIPT_FILENAME: canonical_json_bytes(contents.validation_receipt),
|
|
325
|
+
REPORT_FILENAME: canonical_json_bytes(contents.report),
|
|
326
|
+
}
|
|
327
|
+
if contents.execution_record is not None:
|
|
328
|
+
files[EXECUTION_RECORD_FILENAME] = canonical_json_bytes(
|
|
329
|
+
contents.execution_record
|
|
330
|
+
)
|
|
331
|
+
for variant in _VARIANT_ORDER:
|
|
332
|
+
files[experiment_filename(variant)] = canonical_json_bytes(
|
|
333
|
+
contents.experiments[variant]
|
|
334
|
+
)
|
|
335
|
+
files[receipt_set_filename(variant)] = canonical_json_bytes(
|
|
336
|
+
contents.receipt_sets[variant]
|
|
337
|
+
)
|
|
338
|
+
for position, envelope in enumerate(contents.receipts[variant]):
|
|
339
|
+
files[receipt_filename(variant, position)] = canonical_json_bytes(envelope)
|
|
340
|
+
return files
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def build_local_bundle(
|
|
344
|
+
*, run_id: str, contents: LocalProofBundleContents
|
|
345
|
+
) -> LocalProofBundleManifest:
|
|
346
|
+
"""Return the manifest committing to every file this bundle carries."""
|
|
347
|
+
files = bundle_files(contents)
|
|
348
|
+
return LocalProofBundleManifest(
|
|
349
|
+
schema_version=LOCAL_PROOF_BUNDLE_SCHEMA_VERSION,
|
|
350
|
+
run_id=run_id,
|
|
351
|
+
campaign_spec_digest=digest_object(contents.campaign),
|
|
352
|
+
data_policy_digest=digest_object(contents.data_policy),
|
|
353
|
+
executor_identity=contents.identity,
|
|
354
|
+
artifacts=[
|
|
355
|
+
ArtifactRef(
|
|
356
|
+
digest=sha256_digest_bytes(data),
|
|
357
|
+
media_type=BUNDLE_MEDIA_TYPE,
|
|
358
|
+
size=len(data),
|
|
359
|
+
relative_path=relative_path,
|
|
360
|
+
)
|
|
361
|
+
for relative_path, data in sorted(files.items())
|
|
362
|
+
],
|
|
363
|
+
root_report_digest=contents.report.payload_digest,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def write_local_bundle(
|
|
368
|
+
*,
|
|
369
|
+
run_root: Path,
|
|
370
|
+
contents: LocalProofBundleContents,
|
|
371
|
+
identity_service: IdentityService,
|
|
372
|
+
) -> Path:
|
|
373
|
+
"""Write the bundle and its signed manifest, and return the directory.
|
|
374
|
+
|
|
375
|
+
The manifest is written last. A bundle interrupted halfway has no manifest
|
|
376
|
+
and is therefore not a bundle, which is a better outcome than one that
|
|
377
|
+
names files that are not there yet.
|
|
378
|
+
"""
|
|
379
|
+
directory = proof_bundle_dir(run_root)
|
|
380
|
+
ensure_private_directory(directory)
|
|
381
|
+
|
|
382
|
+
for relative_path, data in sorted(bundle_files(contents).items()):
|
|
383
|
+
destination = directory / relative_path
|
|
384
|
+
ensure_private_directory(destination.parent)
|
|
385
|
+
atomic_write_bytes(destination, data)
|
|
386
|
+
|
|
387
|
+
manifest = build_local_bundle(
|
|
388
|
+
run_id=contents.report.payload.run_id, contents=contents
|
|
389
|
+
)
|
|
390
|
+
sealed = identity_service.sign_object(manifest)
|
|
391
|
+
atomic_write_bytes(
|
|
392
|
+
directory / BUNDLE_MANIFEST_FILENAME, canonical_json_bytes(sealed)
|
|
393
|
+
)
|
|
394
|
+
return directory
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
# The section 3.4 assessment
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def assess_local_attestation(
|
|
403
|
+
*,
|
|
404
|
+
identity: ExecutorIdentity | None,
|
|
405
|
+
identity_self_check: bool,
|
|
406
|
+
referenced_objects: Sequence[ReferencedObject],
|
|
407
|
+
signed_receipts: Mapping[
|
|
408
|
+
ExperimentVariant, Sequence[ObjectEnvelope[EpisodeReceipt]]
|
|
409
|
+
],
|
|
410
|
+
comparison: ComparisonStatus,
|
|
411
|
+
score: ScoreStatus,
|
|
412
|
+
) -> AttestationAssessment:
|
|
413
|
+
"""Decide whether this run's report may be signed into a P1 grade.
|
|
414
|
+
|
|
415
|
+
Five of the six section 3.4 conditions can be established before a report
|
|
416
|
+
exists, and they are established here, from the objects themselves rather
|
|
417
|
+
than from anything anyone recorded about them. The sixth — that the report
|
|
418
|
+
travels in a signed envelope — is a fact about a document that does not
|
|
419
|
+
exist yet; it is settled by verifying the written bundle before the report
|
|
420
|
+
is recorded as the run's result, and it is reported here as pending on the
|
|
421
|
+
same identity that would sign it.
|
|
422
|
+
|
|
423
|
+
Any failure returns :attr:`~techtree.receipts.uplift.LocalAttestation.
|
|
424
|
+
UNATTESTED`, which is what makes the report withhold its verdict instead of
|
|
425
|
+
claiming a grade it did not earn.
|
|
426
|
+
"""
|
|
427
|
+
conditions = [
|
|
428
|
+
_artifact_digests_condition(referenced_objects),
|
|
429
|
+
_receipts_signed_condition(identity, signed_receipts),
|
|
430
|
+
_report_signed_condition(identity, identity_self_check),
|
|
431
|
+
_public_key_condition(identity, identity_self_check),
|
|
432
|
+
_comparison_condition(comparison),
|
|
433
|
+
_score_condition(score),
|
|
434
|
+
]
|
|
435
|
+
failed = [condition for condition in conditions if condition.status == _FAILED]
|
|
436
|
+
return AttestationAssessment(
|
|
437
|
+
attestation=(
|
|
438
|
+
LocalAttestation.UNATTESTED if failed else LocalAttestation.LOCAL_ED25519
|
|
439
|
+
),
|
|
440
|
+
conditions=conditions,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _artifact_digests_condition(
|
|
445
|
+
referenced: Sequence[ReferencedObject],
|
|
446
|
+
) -> ProofCondition:
|
|
447
|
+
"""Require every object the report cites to re-digest to the cited digest."""
|
|
448
|
+
if not referenced:
|
|
449
|
+
return ProofCondition(
|
|
450
|
+
id=P1_ARTIFACT_DIGESTS_VERIFY,
|
|
451
|
+
status=_FAILED,
|
|
452
|
+
detail="no referenced artifact was offered for checking",
|
|
453
|
+
)
|
|
454
|
+
broken = [
|
|
455
|
+
item.label for item in referenced if digest_object(item.value) != item.expected
|
|
456
|
+
]
|
|
457
|
+
if broken:
|
|
458
|
+
return ProofCondition(
|
|
459
|
+
id=P1_ARTIFACT_DIGESTS_VERIFY,
|
|
460
|
+
status=_FAILED,
|
|
461
|
+
detail=(
|
|
462
|
+
"these objects no longer match the digests this run cites them "
|
|
463
|
+
f"under: {', '.join(sorted(broken))}"
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
return ProofCondition(
|
|
467
|
+
id=P1_ARTIFACT_DIGESTS_VERIFY,
|
|
468
|
+
status=_PASSED,
|
|
469
|
+
detail=f"{len(referenced)} referenced artifact digests recomputed and matched",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _receipts_signed_condition(
|
|
474
|
+
identity: ExecutorIdentity | None,
|
|
475
|
+
signed_receipts: Mapping[
|
|
476
|
+
ExperimentVariant, Sequence[ObjectEnvelope[EpisodeReceipt]]
|
|
477
|
+
],
|
|
478
|
+
) -> ProofCondition:
|
|
479
|
+
"""Require every receipt to travel in an envelope this key's signature verifies."""
|
|
480
|
+
if identity is None:
|
|
481
|
+
return ProofCondition(
|
|
482
|
+
id=P1_RECEIPTS_SIGNED,
|
|
483
|
+
status=_FAILED,
|
|
484
|
+
detail="there is no local identity, so no receipt is signed",
|
|
485
|
+
)
|
|
486
|
+
total = 0
|
|
487
|
+
unverified: list[str] = []
|
|
488
|
+
for variant in _VARIANT_ORDER:
|
|
489
|
+
for position, envelope in enumerate(signed_receipts.get(variant, ())):
|
|
490
|
+
total += 1
|
|
491
|
+
result = verify_signed_object(identity=identity, envelope=envelope)
|
|
492
|
+
if not result.verified:
|
|
493
|
+
unverified.append(f"{variant.value}/{position}")
|
|
494
|
+
if not total:
|
|
495
|
+
return ProofCondition(
|
|
496
|
+
id=P1_RECEIPTS_SIGNED,
|
|
497
|
+
status=_FAILED,
|
|
498
|
+
detail="this comparison has no receipts to sign",
|
|
499
|
+
)
|
|
500
|
+
if unverified:
|
|
501
|
+
return ProofCondition(
|
|
502
|
+
id=P1_RECEIPTS_SIGNED,
|
|
503
|
+
status=_FAILED,
|
|
504
|
+
detail=(
|
|
505
|
+
"these receipts are not sealed by a verifying signature: "
|
|
506
|
+
+ ", ".join(unverified)
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
return ProofCondition(
|
|
510
|
+
id=P1_RECEIPTS_SIGNED,
|
|
511
|
+
status=_PASSED,
|
|
512
|
+
detail=f"{total} receipts travel in signed envelopes that verify",
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _report_signed_condition(
|
|
517
|
+
identity: ExecutorIdentity | None, self_check: bool
|
|
518
|
+
) -> ProofCondition:
|
|
519
|
+
"""Require a usable key for the report envelope that is about to be made."""
|
|
520
|
+
if identity is None or not self_check:
|
|
521
|
+
return ProofCondition(
|
|
522
|
+
id=P1_REPORT_SIGNED,
|
|
523
|
+
status=_FAILED,
|
|
524
|
+
detail=("no usable local identity is available to sign this run's report"),
|
|
525
|
+
)
|
|
526
|
+
return ProofCondition(
|
|
527
|
+
id=P1_REPORT_SIGNED,
|
|
528
|
+
status=_PASSED,
|
|
529
|
+
detail=(
|
|
530
|
+
f"the report is signed under key {identity.key_id}, and the written "
|
|
531
|
+
"bundle is verified before the report is recorded"
|
|
532
|
+
),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _public_key_condition(
|
|
537
|
+
identity: ExecutorIdentity | None, self_check: bool
|
|
538
|
+
) -> ProofCondition:
|
|
539
|
+
"""Require the public half to exist and to match the key that signs."""
|
|
540
|
+
if identity is None:
|
|
541
|
+
return ProofCondition(
|
|
542
|
+
id=P1_PUBLIC_KEY_PRESENT,
|
|
543
|
+
status=_FAILED,
|
|
544
|
+
detail="this machine has no local identity to include in a bundle",
|
|
545
|
+
)
|
|
546
|
+
if not self_check:
|
|
547
|
+
return ProofCondition(
|
|
548
|
+
id=P1_PUBLIC_KEY_PRESENT,
|
|
549
|
+
status=_FAILED,
|
|
550
|
+
detail=("the stored public key does not describe the key that would sign"),
|
|
551
|
+
)
|
|
552
|
+
return ProofCondition(
|
|
553
|
+
id=P1_PUBLIC_KEY_PRESENT,
|
|
554
|
+
status=_PASSED,
|
|
555
|
+
detail=f"public key {identity.key_id} travels with the proof",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _comparison_condition(comparison: ComparisonStatus) -> ProofCondition:
|
|
560
|
+
"""Require a controlled comparison, warnings included."""
|
|
561
|
+
controlled = comparison in (
|
|
562
|
+
ComparisonStatus.CONTROLLED,
|
|
563
|
+
ComparisonStatus.CONTROLLED_WITH_WARNINGS,
|
|
564
|
+
)
|
|
565
|
+
return ProofCondition(
|
|
566
|
+
id=P1_COMPARISON_CONTROLLED,
|
|
567
|
+
status=_PASSED if controlled else _FAILED,
|
|
568
|
+
detail=f"the comparison is {comparison.value}",
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _score_condition(score: ScoreStatus) -> ProofCondition:
|
|
573
|
+
"""Require valid scores."""
|
|
574
|
+
return ProofCondition(
|
|
575
|
+
id=P1_SCORE_VALID,
|
|
576
|
+
status=_PASSED if score is ScoreStatus.VALID else _FAILED,
|
|
577
|
+
detail=f"the recorded score status is {score.value}",
|
|
578
|
+
)
|