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,183 @@
|
|
|
1
|
+
"""Keccak-256, because EIP-55 is defined over it and nothing here provides it.
|
|
2
|
+
|
|
3
|
+
This module exists for one reason and has one caller. The mixed-case spelling
|
|
4
|
+
of an Ethereum address is a checksum, and the checksum is the original Keccak
|
|
5
|
+
submission rather than the NIST standard that came out of it. The two differ by
|
|
6
|
+
one byte of padding and produce entirely different digests, so ``hashlib``'s
|
|
7
|
+
``sha3_256`` cannot be used and quietly produces a checksum that is wrong for
|
|
8
|
+
every address. ``cryptography`` exposes SHA-3 and SHAKE and does not expose
|
|
9
|
+
Keccak either.
|
|
10
|
+
|
|
11
|
+
The alternative to sixty lines here is a third-party dependency in a package
|
|
12
|
+
whose dependency list is deliberately eight entries long, pulled in so that a
|
|
13
|
+
forty-character string can be checked before it is sent. That trade is not
|
|
14
|
+
worth making, and the permutation is fixed for ever: Keccak-f[1600] has not
|
|
15
|
+
changed since 2011 and cannot, because the digests it produces are what
|
|
16
|
+
everything already published is addressed by.
|
|
17
|
+
|
|
18
|
+
So this is a plain transcription of the permutation, and it is held to the
|
|
19
|
+
published vectors in ``tests/unit/test_contributor_address.py`` rather than to
|
|
20
|
+
a reading of the code.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Final
|
|
26
|
+
|
|
27
|
+
__all__ = ["keccak256"]
|
|
28
|
+
|
|
29
|
+
#: Keccak-f[1600] operates on twenty-five 64-bit lanes.
|
|
30
|
+
_LANE_BITS: Final = 64
|
|
31
|
+
_LANE_MASK: Final = (1 << _LANE_BITS) - 1
|
|
32
|
+
_LANE_BYTES: Final = _LANE_BITS // 8
|
|
33
|
+
|
|
34
|
+
#: The sponge rate for a 256-bit digest: 1600 bits of state less twice the
|
|
35
|
+
#: capacity, in bytes.
|
|
36
|
+
_RATE_BYTES: Final = (1600 - 2 * 256) // 8
|
|
37
|
+
|
|
38
|
+
_DIGEST_BYTES: Final = 32
|
|
39
|
+
|
|
40
|
+
#: The domain separator Keccak pads with. SHA-3 pads with ``0x06`` instead, and
|
|
41
|
+
#: that single byte is the whole of the difference between this function and
|
|
42
|
+
#: ``hashlib.sha3_256``.
|
|
43
|
+
_PAD_BYTE: Final = 0x01
|
|
44
|
+
_PAD_FINAL_BIT: Final = 0x80
|
|
45
|
+
|
|
46
|
+
#: Rotation offsets, indexed ``[x][y]``.
|
|
47
|
+
_ROTATIONS: Final[tuple[tuple[int, ...], ...]] = (
|
|
48
|
+
(0, 36, 3, 41, 18),
|
|
49
|
+
(1, 44, 10, 45, 2),
|
|
50
|
+
(62, 6, 43, 15, 61),
|
|
51
|
+
(28, 55, 25, 21, 56),
|
|
52
|
+
(27, 20, 39, 8, 14),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_ROUND_CONSTANTS: Final[tuple[int, ...]] = (
|
|
56
|
+
0x0000000000000001,
|
|
57
|
+
0x0000000000008082,
|
|
58
|
+
0x800000000000808A,
|
|
59
|
+
0x8000000080008000,
|
|
60
|
+
0x000000000000808B,
|
|
61
|
+
0x0000000080000001,
|
|
62
|
+
0x8000000080008081,
|
|
63
|
+
0x8000000000008009,
|
|
64
|
+
0x000000000000008A,
|
|
65
|
+
0x0000000000000088,
|
|
66
|
+
0x0000000080008009,
|
|
67
|
+
0x000000008000000A,
|
|
68
|
+
0x000000008000808B,
|
|
69
|
+
0x800000000000008B,
|
|
70
|
+
0x8000000000008089,
|
|
71
|
+
0x8000000000008003,
|
|
72
|
+
0x8000000000008002,
|
|
73
|
+
0x8000000000000080,
|
|
74
|
+
0x000000000000800A,
|
|
75
|
+
0x800000008000000A,
|
|
76
|
+
0x8000000080008081,
|
|
77
|
+
0x8000000000008080,
|
|
78
|
+
0x0000000080000001,
|
|
79
|
+
0x8000000080008008,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
type _State = list[list[int]]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def keccak256(data: bytes) -> bytes:
|
|
86
|
+
"""Return the Keccak-256 digest of ``data``.
|
|
87
|
+
|
|
88
|
+
This is the pre-standardisation Keccak that Ethereum addresses, not
|
|
89
|
+
SHA3-256.
|
|
90
|
+
"""
|
|
91
|
+
state: _State = [[0] * 5 for _ in range(5)]
|
|
92
|
+
for block in _blocks(data):
|
|
93
|
+
_absorb(state, block)
|
|
94
|
+
_permute(state)
|
|
95
|
+
return _squeeze(state)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _blocks(data: bytes) -> list[bytes]:
|
|
99
|
+
"""Return the padded message as whole rate-sized blocks.
|
|
100
|
+
|
|
101
|
+
Keccak's ``pad10*1``: a one bit, as many zero bits as it takes, and a final
|
|
102
|
+
one bit. Written here in the byte order the lanes are read in, which is why
|
|
103
|
+
the two one bits are the low bit of the first pad byte and the high bit of
|
|
104
|
+
the last.
|
|
105
|
+
"""
|
|
106
|
+
padded = bytearray(data)
|
|
107
|
+
padded.append(_PAD_BYTE)
|
|
108
|
+
while len(padded) % _RATE_BYTES != 0:
|
|
109
|
+
padded.append(0)
|
|
110
|
+
padded[-1] |= _PAD_FINAL_BIT
|
|
111
|
+
return [
|
|
112
|
+
bytes(padded[start : start + _RATE_BYTES])
|
|
113
|
+
for start in range(0, len(padded), _RATE_BYTES)
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _absorb(state: _State, block: bytes) -> None:
|
|
118
|
+
"""Exclusive-or one block into the rate portion of the state."""
|
|
119
|
+
for position in range(_RATE_BYTES // _LANE_BYTES):
|
|
120
|
+
lane = int.from_bytes(
|
|
121
|
+
block[position * _LANE_BYTES : (position + 1) * _LANE_BYTES],
|
|
122
|
+
"little",
|
|
123
|
+
)
|
|
124
|
+
state[position % 5][position // 5] ^= lane
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _squeeze(state: _State) -> bytes:
|
|
128
|
+
"""Return the first 32 bytes of the state, in lane order.
|
|
129
|
+
|
|
130
|
+
A 256-bit digest is shorter than the rate, so the sponge never squeezes
|
|
131
|
+
twice and there is no second permutation to perform here.
|
|
132
|
+
"""
|
|
133
|
+
out = bytearray()
|
|
134
|
+
position = 0
|
|
135
|
+
while len(out) < _DIGEST_BYTES:
|
|
136
|
+
out += state[position % 5][position // 5].to_bytes(_LANE_BYTES, "little")
|
|
137
|
+
position += 1
|
|
138
|
+
return bytes(out[:_DIGEST_BYTES])
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _permute(state: _State) -> None:
|
|
142
|
+
"""Apply the twenty-four rounds of Keccak-f[1600] in place."""
|
|
143
|
+
for constant in _ROUND_CONSTANTS:
|
|
144
|
+
_theta(state)
|
|
145
|
+
state[:] = _rho_and_pi(state)
|
|
146
|
+
_chi(state)
|
|
147
|
+
state[0][0] ^= constant
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _theta(state: _State) -> None:
|
|
151
|
+
columns = [
|
|
152
|
+
state[x][0] ^ state[x][1] ^ state[x][2] ^ state[x][3] ^ state[x][4]
|
|
153
|
+
for x in range(5)
|
|
154
|
+
]
|
|
155
|
+
parities = [
|
|
156
|
+
columns[(x - 1) % 5] ^ _rotate(columns[(x + 1) % 5], 1) for x in range(5)
|
|
157
|
+
]
|
|
158
|
+
for x in range(5):
|
|
159
|
+
for y in range(5):
|
|
160
|
+
state[x][y] ^= parities[x]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _rho_and_pi(state: _State) -> _State:
|
|
164
|
+
moved: _State = [[0] * 5 for _ in range(5)]
|
|
165
|
+
for x in range(5):
|
|
166
|
+
for y in range(5):
|
|
167
|
+
moved[y][(2 * x + 3 * y) % 5] = _rotate(state[x][y], _ROTATIONS[x][y])
|
|
168
|
+
return moved
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _chi(state: _State) -> None:
|
|
172
|
+
previous = [list(column) for column in state]
|
|
173
|
+
for x in range(5):
|
|
174
|
+
for y in range(5):
|
|
175
|
+
state[x][y] = previous[x][y] ^ (
|
|
176
|
+
(~previous[(x + 1) % 5][y] & _LANE_MASK) & previous[(x + 2) % 5][y]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _rotate(lane: int, places: int) -> int:
|
|
181
|
+
"""Return one 64-bit lane rotated left."""
|
|
182
|
+
places %= _LANE_BITS
|
|
183
|
+
return ((lane << places) | (lane >> (_LANE_BITS - places))) & _LANE_MASK
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""What is sent, and what comes back. Decisions document 0038.
|
|
2
|
+
|
|
3
|
+
Four documents cross the one boundary this package opens, and they are separate
|
|
4
|
+
objects because they are made by different parties and prove different things.
|
|
5
|
+
|
|
6
|
+
A :class:`PublicationSubmission` is what the participant sends: the proof
|
|
7
|
+
directory, file by file, exactly as it sits on disk. Nothing is summarised,
|
|
8
|
+
nothing is re-derived, and nothing is added — the bundle already commits to
|
|
9
|
+
every one of those files by digest under the participant's own signature, so
|
|
10
|
+
sending anything but the stored bytes would be sending something nobody signed.
|
|
11
|
+
It carries no episodes and no transcripts, because the proof directory holds
|
|
12
|
+
none: an episode receipt carries digests, task hashes and scores, and the raw
|
|
13
|
+
episodes live outside the directory entirely.
|
|
14
|
+
|
|
15
|
+
The submission is a mapping of path to bytes and it has exactly four members,
|
|
16
|
+
which decisions 0038's wire contract fixes so that the two halves of this
|
|
17
|
+
feature cannot drift. Both of those shapes are refusals rather than
|
|
18
|
+
preferences, and each is written into the model below beside the field it
|
|
19
|
+
constrains.
|
|
20
|
+
|
|
21
|
+
A :class:`PublicationReceiptPayload` is what the network sends back: where the
|
|
22
|
+
entry landed in the log, which bundle it accepted, when, and which of its own
|
|
23
|
+
checks it ran. The participant signed their run and the network countersigns
|
|
24
|
+
that it accepted it, which is what makes an accepted entry checkable by somebody
|
|
25
|
+
who trusts neither party's word about the other.
|
|
26
|
+
|
|
27
|
+
It travels inside an :class:`~techtree.models.base.ObjectEnvelope`, which is how
|
|
28
|
+
every other signed document in this protocol travels: payload, the digest of the
|
|
29
|
+
payload's canonical bytes, and a detached signature over that digest. An earlier
|
|
30
|
+
draft carried the signature flat beside the fields it covered, and that shape
|
|
31
|
+
cannot say which members the digest was taken over — two implementations would
|
|
32
|
+
have had to agree on it by reading the same paragraph the same way, which is
|
|
33
|
+
exactly the drift decisions 0038's wire contract exists to prevent. The envelope
|
|
34
|
+
answers it structurally: the digest is over ``payload`` and over nothing else.
|
|
35
|
+
|
|
36
|
+
A :class:`WithdrawalRequest` is the participant asking that a published entry be
|
|
37
|
+
marked withdrawn, signed with the same key that signed the run. It carries no
|
|
38
|
+
free text: nothing a submitter writes appears on the site, and a reason field
|
|
39
|
+
would be the one string that did. A :class:`WithdrawalReceiptPayload` is the
|
|
40
|
+
network's countersigned record that the entry is now marked withdrawn — the
|
|
41
|
+
entry stays where it is, because a published entry is withdrawn and never
|
|
42
|
+
deleted. Both travel in the same envelope, for the same reason.
|
|
43
|
+
|
|
44
|
+
The one field that is none of them is the contributor address. It is optional,
|
|
45
|
+
it is volunteered, and its name says it is unverified, because a string somebody
|
|
46
|
+
typed is not proof of control of an account. It travels in the
|
|
47
|
+
``x-techtree-contributor-address`` header, beside the submission and never
|
|
48
|
+
inside it, because the run log serves a stored submission back at a public
|
|
49
|
+
address. It is kept nowhere on this machine.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
from typing import Literal, Self
|
|
55
|
+
|
|
56
|
+
from pydantic import Field, model_validator
|
|
57
|
+
|
|
58
|
+
from techtree.models.base import (
|
|
59
|
+
Base64String,
|
|
60
|
+
Digest,
|
|
61
|
+
NonEmptyString,
|
|
62
|
+
ProtocolModel,
|
|
63
|
+
PublicKeyRef,
|
|
64
|
+
UtcDateTime,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"PublicationCheck",
|
|
69
|
+
"PublicationReceiptPayload",
|
|
70
|
+
"PublicationSubmission",
|
|
71
|
+
"WithdrawalReceiptPayload",
|
|
72
|
+
"WithdrawalRequest",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PublicationSubmission(ProtocolModel):
|
|
77
|
+
"""Everything one publication sends, and nothing else.
|
|
78
|
+
|
|
79
|
+
Four members, fixed by decisions 0038 and by nothing else. The document is
|
|
80
|
+
stored by the run log and served back at a public address, so a member that
|
|
81
|
+
arrived here without being in the contract would be published without
|
|
82
|
+
anybody having agreed to publish it. The receiving side refuses a body that
|
|
83
|
+
carries more; ``extra="forbid"`` on :class:`~techtree.models.base.
|
|
84
|
+
ProtocolModel` means this side cannot build one.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
schema_version: Literal["techtree.publication-submission.v1alpha1"]
|
|
88
|
+
run_id: NonEmptyString
|
|
89
|
+
#: The digest of the bundle's own signed manifest, which commits to every
|
|
90
|
+
#: file below. One value identifies the whole submission.
|
|
91
|
+
bundle_digest: Digest
|
|
92
|
+
#: Where each file sits inside the proof directory, in POSIX spelling,
|
|
93
|
+
#: against the base64 of its bytes. The key is the same string the bundle
|
|
94
|
+
#: manifest places the file under, so the receiving side can find each file
|
|
95
|
+
#: in the manifest that arrived with it.
|
|
96
|
+
#:
|
|
97
|
+
#: A mapping, and deliberately nothing richer. An earlier shape put a
|
|
98
|
+
#: digest and a size beside each file's content, and both were the
|
|
99
|
+
#: submitter's own claims about the submitter's own bytes: worth nothing if
|
|
100
|
+
#: honest and dangerous if believed. Every digest and every length the
|
|
101
|
+
#: receiving side works with has to come from the bundle's own signed
|
|
102
|
+
#: manifest, which is inside this mapping under ``bundle.json`` and is
|
|
103
|
+
#: signed by the key the bundle carries. Sending a digest beside the
|
|
104
|
+
#: content invites somebody downstream to trust it instead, and the only
|
|
105
|
+
#: reliable way to stop that is to have none to trust. Base64 because every
|
|
106
|
+
#: file in a proof bundle is canonical JSON, and base64 is what carries
|
|
107
|
+
#: bytes through JSON without a second encoding decision.
|
|
108
|
+
files: dict[NonEmptyString, Base64String]
|
|
109
|
+
|
|
110
|
+
@model_validator(mode="after")
|
|
111
|
+
def _check_the_files_are_a_bundle(self) -> Self:
|
|
112
|
+
"""Reject a submission that places no file.
|
|
113
|
+
|
|
114
|
+
A mapping cannot place one file twice, which is the second reason the
|
|
115
|
+
contract chose one: the shape refuses a whole class of malformed
|
|
116
|
+
submission rather than a validator having to notice it.
|
|
117
|
+
"""
|
|
118
|
+
if not self.files:
|
|
119
|
+
raise ValueError("a publication submits at least one file")
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class PublicationCheck(ProtocolModel):
|
|
124
|
+
"""One check the network ran on a submission, and how it came out."""
|
|
125
|
+
|
|
126
|
+
id: NonEmptyString
|
|
127
|
+
passed: bool
|
|
128
|
+
detail: NonEmptyString
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class PublicationReceiptPayload(ProtocolModel):
|
|
132
|
+
"""What the network countersigns when it accepts one submission.
|
|
133
|
+
|
|
134
|
+
The payload of an :class:`~techtree.models.base.ObjectEnvelope`, so the
|
|
135
|
+
digest the network signs is the digest of exactly these members and the two
|
|
136
|
+
halves of this feature need not agree about anything else.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
schema_version: Literal["techtree.publication-receipt.v1alpha1"]
|
|
140
|
+
id: NonEmptyString
|
|
141
|
+
run_id: NonEmptyString
|
|
142
|
+
#: Where this entry sits in the log. The log is ordered by arrival and by
|
|
143
|
+
#: nothing else, so this is a position in time rather than a rank, and it
|
|
144
|
+
#: may have gaps.
|
|
145
|
+
log_sequence: int = Field(ge=0)
|
|
146
|
+
bundle_digest: Digest
|
|
147
|
+
accepted_at: UtcDateTime
|
|
148
|
+
checks: list[PublicationCheck]
|
|
149
|
+
#: Where the entry can now be read.
|
|
150
|
+
entry_url: NonEmptyString
|
|
151
|
+
#: The network's own key, carried so that the receipt can be checked later
|
|
152
|
+
#: by anybody holding the network's published public half, including by
|
|
153
|
+
#: somebody who was not party to the exchange. The participant's own CLI
|
|
154
|
+
#: does not learn the key from here — it checks this against the key the
|
|
155
|
+
#: release pins, because a key learned from the answer proves nothing about
|
|
156
|
+
#: who wrote the answer.
|
|
157
|
+
public_key: PublicKeyRef
|
|
158
|
+
|
|
159
|
+
@model_validator(mode="after")
|
|
160
|
+
def _check_the_receipt_reports_its_own_checks(self) -> Self:
|
|
161
|
+
"""Reject a receipt that accepted a submission it never checked."""
|
|
162
|
+
if not self.checks:
|
|
163
|
+
raise ValueError("a publication receipt names the checks that ran")
|
|
164
|
+
identifiers = [check.id for check in self.checks]
|
|
165
|
+
if len(set(identifiers)) != len(identifiers):
|
|
166
|
+
raise ValueError("a publication receipt reports each check once")
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def failed_checks(self) -> list[PublicationCheck]:
|
|
171
|
+
"""Return every check the network ran and did not pass."""
|
|
172
|
+
return [check for check in self.checks if not check.passed]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class WithdrawalRequest(ProtocolModel):
|
|
176
|
+
"""The participant asking that one published entry be marked withdrawn.
|
|
177
|
+
|
|
178
|
+
Three members and no fourth. There is no reason field and there will not be
|
|
179
|
+
one: decisions 0038 settled that nothing a submitter writes appears on the
|
|
180
|
+
site, and a free-text reason attached to a public entry is exactly that.
|
|
181
|
+
|
|
182
|
+
The bundle digest is the whole of the subject, because a run is addressed on
|
|
183
|
+
the log by its bundle digest. ``requested_at`` makes the request a dated
|
|
184
|
+
statement rather than a bare assertion, and the signature over the payload's
|
|
185
|
+
digest is what proves it came from the key that signed the run. Nothing here
|
|
186
|
+
carries a public key: the network already holds the participant's, inside
|
|
187
|
+
the bundle it accepted, and looking the key up there rather than believing
|
|
188
|
+
one that arrived with the request is the point.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
schema_version: Literal["techtree.publication-withdrawal.v1alpha1"]
|
|
192
|
+
bundle_digest: Digest
|
|
193
|
+
requested_at: UtcDateTime
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class WithdrawalReceiptPayload(ProtocolModel):
|
|
197
|
+
"""The network's countersigned record that an entry is marked withdrawn.
|
|
198
|
+
|
|
199
|
+
The entry itself stays: withdrawal is an appended event, not a deletion, so
|
|
200
|
+
what comes back names where the entry still lives rather than saying it is
|
|
201
|
+
gone.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
schema_version: Literal["techtree.publication-withdrawal-receipt.v1alpha1"]
|
|
205
|
+
bundle_digest: Digest
|
|
206
|
+
#: Where the entry, now marked withdrawn, can still be read.
|
|
207
|
+
entry_url: NonEmptyString
|
|
208
|
+
withdrawn_at: UtcDateTime
|
|
209
|
+
public_key: PublicKeyRef
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""The offer to publish, written once. Decisions document 0038.
|
|
2
|
+
|
|
3
|
+
Two surfaces make this offer — the finished result and the proof check — and
|
|
4
|
+
they are the two places somebody has just been told their run holds together.
|
|
5
|
+
An offer that read differently depending on which of them a person happened to
|
|
6
|
+
be looking at would be two offers, so it is one function and both call it.
|
|
7
|
+
|
|
8
|
+
It carries ``requires_user_confirmation``, which is not advisory. Publishing is
|
|
9
|
+
the one thing this product does that leaves the machine, and the flag is how a
|
|
10
|
+
host agent is told to ask rather than act. The plugin itself publishes nothing
|
|
11
|
+
and can open no network connection at all; what it may do is put this command in
|
|
12
|
+
front of a person.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from techtree.models.cli import NextAction
|
|
18
|
+
|
|
19
|
+
__all__ = ["publish_action"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def publish_action(run_id: str) -> NextAction:
|
|
23
|
+
"""Return the offer to publish one verified run."""
|
|
24
|
+
return NextAction(
|
|
25
|
+
id="publish_run",
|
|
26
|
+
label="Publish this run to the public run log",
|
|
27
|
+
reason=(
|
|
28
|
+
"The proof just verified, so the run's own evidence travels with "
|
|
29
|
+
"it. It shows what would be sent and asks before sending anything."
|
|
30
|
+
),
|
|
31
|
+
cli=["techtree", "publish", run_id],
|
|
32
|
+
hermes_tool=None,
|
|
33
|
+
hermes_args=None,
|
|
34
|
+
requires_user_confirmation=True,
|
|
35
|
+
)
|