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,447 @@
|
|
|
1
|
+
"""``techtree publish``. Decisions document 0038.
|
|
2
|
+
|
|
3
|
+
This is the one command in Techtree that sends anything anywhere, and every
|
|
4
|
+
decision in it is about making that fact impossible to arrive at by accident.
|
|
5
|
+
|
|
6
|
+
*The proof is checked before the question is asked.* ``plan`` verifies the run's
|
|
7
|
+
bundle offline, from its stored bytes, and refuses everything that does not hold
|
|
8
|
+
together. A result whose own proof fails is never offered as something that
|
|
9
|
+
could be published, because a published number whose evidence does not check out
|
|
10
|
+
is the one outcome this product exists to prevent.
|
|
11
|
+
|
|
12
|
+
*Everything that will be sent is shown first.* How many files, how many bytes,
|
|
13
|
+
and the address they go to. The proof directory carries no transcripts — an
|
|
14
|
+
episode receipt holds digests, task hashes and scores, and the raw episodes are
|
|
15
|
+
outside it entirely — and the summary says so rather than leaving a reader to
|
|
16
|
+
wonder what is in three hundred kilobytes.
|
|
17
|
+
|
|
18
|
+
*Somebody has to say yes.* The prompt is the shared one, its default is no, and
|
|
19
|
+
where nobody can be asked the command stops and names the flag instead of
|
|
20
|
+
inventing an answer. ``--yes`` with ``--reviewed-on host-agent`` is how an agent
|
|
21
|
+
records that the person answered in the conversation, which is the same surface
|
|
22
|
+
``climb start`` already uses and the one decisions 0038 names for this.
|
|
23
|
+
|
|
24
|
+
*The address question is asked once, and promises nothing.* It defaults to no,
|
|
25
|
+
what is typed is checked and canonicalised before it goes anywhere, and it
|
|
26
|
+
travels in the ``x-techtree-contributor-address`` request header — beside the
|
|
27
|
+
submission and never inside it, because the run log serves a stored submission
|
|
28
|
+
back at a public address — and it is written down nowhere. The wording says what
|
|
29
|
+
is true: an address can be left, it is kept in case contributors can be
|
|
30
|
+
recognised later, and nothing is being offered in exchange today.
|
|
31
|
+
|
|
32
|
+
*Nothing is written down that was not checked.* The receipt the run log answers
|
|
33
|
+
with is verified against the network key this release pins — not against the key
|
|
34
|
+
the receipt carries, which a server that wanted to lie would simply invent —
|
|
35
|
+
before it becomes a file. An unverified receipt is not written at all.
|
|
36
|
+
|
|
37
|
+
The run's own files are not touched. The countersigned receipt is a new file in
|
|
38
|
+
the run directory and the outcome goes into a publication journal of the run's
|
|
39
|
+
own, which is what append-only permits and the whole of what it permits.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from typing import Annotated, Final
|
|
45
|
+
|
|
46
|
+
import typer
|
|
47
|
+
from rich.console import Console
|
|
48
|
+
|
|
49
|
+
from techtree.cli.commands.climb import ReviewSurface
|
|
50
|
+
from techtree.cli.confirm import confirmed
|
|
51
|
+
from techtree.cli.context import CliContext, cli_context
|
|
52
|
+
from techtree.cli.invoke import CommandResult, invoke_command
|
|
53
|
+
from techtree.cli.output import human_console, render_pairs
|
|
54
|
+
from techtree.drafts.store import utc_now
|
|
55
|
+
from techtree.errors import UsageError
|
|
56
|
+
from techtree.models.base import Digest, NonEmptyString, ProtocolModel, UtcDateTime
|
|
57
|
+
from techtree.models.cli import CliMessage, MessageLevel, NextAction
|
|
58
|
+
from techtree.models.uplift_report import PublicationStatus
|
|
59
|
+
from techtree.publication.address import (
|
|
60
|
+
canonical_contributor_address,
|
|
61
|
+
canonical_skill_github_url,
|
|
62
|
+
)
|
|
63
|
+
from techtree.publication.coordinates import packaged_publication_coordinates
|
|
64
|
+
from techtree.publication.service import (
|
|
65
|
+
PublicationPlan,
|
|
66
|
+
PublicationService,
|
|
67
|
+
)
|
|
68
|
+
from techtree.publication.transport import HttpsPublicationTransport
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"ADDRESS_QUESTION",
|
|
72
|
+
"NOTHING_IS_OFFERED",
|
|
73
|
+
"PUBLICATION_CONFIRMATION_REQUIRED",
|
|
74
|
+
"PUBLISH_COMMAND",
|
|
75
|
+
"PublicationPayload",
|
|
76
|
+
"publish_run_command",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
#: The command's own name, in the envelope and in every message about it.
|
|
80
|
+
#: Decisions 0038's founder ruling makes it a top-level command: ``proof`` keeps
|
|
81
|
+
#: ``verify`` and nothing else, and nothing has been released, so this is a hard
|
|
82
|
+
#: cut with no alias.
|
|
83
|
+
PUBLISH_COMMAND: Final = "publish"
|
|
84
|
+
|
|
85
|
+
#: Stable error code for "nobody said to publish this".
|
|
86
|
+
PUBLICATION_CONFIRMATION_REQUIRED: Final = "publication_confirmation_required"
|
|
87
|
+
|
|
88
|
+
#: The hard boundary, stated in the copy rather than only in the guard that
|
|
89
|
+
#: enforces it. Nothing is promised in return for an address; the intention to
|
|
90
|
+
#: be able to recognise contributors later is an intention, and an intention is
|
|
91
|
+
#: not a commitment. Decisions 0038.
|
|
92
|
+
NOTHING_IS_OFFERED: Final = (
|
|
93
|
+
"Nothing is being offered in exchange today. It is kept only so that "
|
|
94
|
+
"contributors can be recognised later, if that becomes possible."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
#: What a person is asked, once, at publish time. It is the whole question:
|
|
98
|
+
#: what the address is for, that it is optional, and that leaving one buys
|
|
99
|
+
#: nothing.
|
|
100
|
+
ADDRESS_QUESTION: Final = (
|
|
101
|
+
"You can leave an Ethereum address with this run if you want to. "
|
|
102
|
+
f"It is optional, it is never shown on the log, and nobody checks that it "
|
|
103
|
+
f"is yours. {NOTHING_IS_OFFERED}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
_ADDRESS_PROMPT: Final = "Leave an address with this run?"
|
|
107
|
+
_PUBLISH_PROMPT: Final = "Publish this run to the public log?"
|
|
108
|
+
|
|
109
|
+
#: What the proof directory holds, said where somebody is deciding whether to
|
|
110
|
+
#: send it. Each episode receipt carries digests, task hashes and scores; the
|
|
111
|
+
#: episodes themselves are not in the directory at all.
|
|
112
|
+
_WHAT_TRAVELS: Final = (
|
|
113
|
+
"These are the run's proof files: the signed report, the receipts, and the "
|
|
114
|
+
"documents they cite. No prompts and no replies are among them — a receipt "
|
|
115
|
+
"records a task's digest and its score, and the episodes themselves are "
|
|
116
|
+
"not in this directory."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class PublicationPayload(ProtocolModel):
|
|
121
|
+
"""What ``techtree publish`` returns once the log has accepted a run."""
|
|
122
|
+
|
|
123
|
+
run_id: NonEmptyString
|
|
124
|
+
bundle_digest: Digest
|
|
125
|
+
endpoint: NonEmptyString
|
|
126
|
+
file_count: int
|
|
127
|
+
byte_count: int
|
|
128
|
+
publication: PublicationStatus
|
|
129
|
+
log_sequence: int
|
|
130
|
+
entry_url: NonEmptyString
|
|
131
|
+
accepted_at: UtcDateTime
|
|
132
|
+
receipt_path: NonEmptyString
|
|
133
|
+
#: Whether an address was sent. The address is not here, and is not
|
|
134
|
+
#: anywhere else this machine can be read: it went into the
|
|
135
|
+
#: ``x-techtree-contributor-address`` request header and into nothing
|
|
136
|
+
#: else.
|
|
137
|
+
contributor_address_sent: bool
|
|
138
|
+
#: Metadata read from the verified prepared Skill and optionally supplied
|
|
139
|
+
#: at publication time. These are headers beside, never members of, the
|
|
140
|
+
#: fixed four-member proof submission.
|
|
141
|
+
skill_name: NonEmptyString | None = None
|
|
142
|
+
skill_github_url: NonEmptyString | None = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def publish_run_command(
|
|
146
|
+
ctx: typer.Context,
|
|
147
|
+
run_id: Annotated[
|
|
148
|
+
str,
|
|
149
|
+
typer.Argument(
|
|
150
|
+
metavar="RUN_ID",
|
|
151
|
+
help="The finished run whose proof should be published.",
|
|
152
|
+
),
|
|
153
|
+
],
|
|
154
|
+
yes: Annotated[
|
|
155
|
+
bool,
|
|
156
|
+
typer.Option(
|
|
157
|
+
"--yes",
|
|
158
|
+
help=(
|
|
159
|
+
"Publish without being asked. For an operator running Techtree "
|
|
160
|
+
"where nobody can answer a prompt; it is never a shortcut for "
|
|
161
|
+
"an agent to take on a person's behalf."
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
] = False,
|
|
165
|
+
reviewed_on: Annotated[
|
|
166
|
+
ReviewSurface,
|
|
167
|
+
typer.Option(
|
|
168
|
+
"--reviewed-on",
|
|
169
|
+
help=(
|
|
170
|
+
"Where the person who agreed to publish answered. Pass "
|
|
171
|
+
"host-agent when what would be sent was shown in a "
|
|
172
|
+
"conversation and agreed there. Like --yes, and for the same "
|
|
173
|
+
"reason, it states what a person already did."
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
] = ReviewSurface.CLI,
|
|
177
|
+
address: Annotated[
|
|
178
|
+
str | None,
|
|
179
|
+
typer.Option(
|
|
180
|
+
"--address",
|
|
181
|
+
metavar="ADDRESS",
|
|
182
|
+
help=(
|
|
183
|
+
"An Ethereum address to send with this run. Optional, never "
|
|
184
|
+
"shown on the log, and nothing is offered in exchange for it."
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
] = None,
|
|
188
|
+
github_url: Annotated[
|
|
189
|
+
str | None,
|
|
190
|
+
typer.Option(
|
|
191
|
+
"--github-url",
|
|
192
|
+
metavar="URL",
|
|
193
|
+
help=(
|
|
194
|
+
"An optional canonical https://github.com/owner/repo URL for "
|
|
195
|
+
"the Skill. It is public descriptive metadata, not proof of "
|
|
196
|
+
"ownership."
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
] = None,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Publish a verified run's proof to the public run log."""
|
|
202
|
+
context = cli_context(ctx)
|
|
203
|
+
|
|
204
|
+
def action() -> CommandResult[PublicationPayload]:
|
|
205
|
+
service = build_publication_service(context)
|
|
206
|
+
skill_github_url = (
|
|
207
|
+
canonical_skill_github_url(github_url) if github_url is not None else None
|
|
208
|
+
)
|
|
209
|
+
plan = service.plan(run_id)
|
|
210
|
+
# The order a person meets this in is the order it is written in: what
|
|
211
|
+
# would be sent, then the one optional question, then the answer that
|
|
212
|
+
# covers both. An address asked for before the review would be asked
|
|
213
|
+
# of somebody who does not yet know what they are being asked about.
|
|
214
|
+
asking = _require_an_answer_is_possible(
|
|
215
|
+
context, plan, assume_yes=yes, reviewed_on=reviewed_on
|
|
216
|
+
)
|
|
217
|
+
if asking:
|
|
218
|
+
_show_what_would_be_sent(
|
|
219
|
+
context, plan, skill_github_url=skill_github_url
|
|
220
|
+
)
|
|
221
|
+
contributor = _contributor_address(context, typed=address, asking=asking)
|
|
222
|
+
if asking:
|
|
223
|
+
_require_publication_confirmation(context, plan)
|
|
224
|
+
|
|
225
|
+
outcome = service.publish(
|
|
226
|
+
plan,
|
|
227
|
+
contributor_address=contributor,
|
|
228
|
+
skill_github_url=skill_github_url,
|
|
229
|
+
)
|
|
230
|
+
receipt = outcome.receipt
|
|
231
|
+
payload = PublicationPayload(
|
|
232
|
+
run_id=outcome.run_id,
|
|
233
|
+
bundle_digest=plan.bundle_digest,
|
|
234
|
+
endpoint=plan.endpoint,
|
|
235
|
+
file_count=plan.file_count,
|
|
236
|
+
byte_count=plan.byte_count,
|
|
237
|
+
publication=outcome.status,
|
|
238
|
+
log_sequence=receipt.log_sequence,
|
|
239
|
+
entry_url=receipt.entry_url,
|
|
240
|
+
accepted_at=receipt.accepted_at,
|
|
241
|
+
receipt_path=str(outcome.receipt_path),
|
|
242
|
+
contributor_address_sent=outcome.contributor_address_sent,
|
|
243
|
+
skill_name=plan.skill_name,
|
|
244
|
+
skill_github_url=skill_github_url,
|
|
245
|
+
)
|
|
246
|
+
return CommandResult(
|
|
247
|
+
data=payload,
|
|
248
|
+
messages=[
|
|
249
|
+
CliMessage(
|
|
250
|
+
level=MessageLevel.INFO,
|
|
251
|
+
code="run_published",
|
|
252
|
+
text=(
|
|
253
|
+
f"Run {payload.run_id} is entry {payload.log_sequence} "
|
|
254
|
+
f"of the public log, at {payload.entry_url}. The log "
|
|
255
|
+
"records arrivals in order and ranks nothing."
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
],
|
|
259
|
+
next_actions=[_verify_proof(payload.run_id)],
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
invoke_command(context, PUBLISH_COMMAND, action, render_data=_render)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def build_publication_service(context: CliContext) -> PublicationService:
|
|
266
|
+
"""Construct the service this command publishes through.
|
|
267
|
+
|
|
268
|
+
The transport is the only substitutable part and it is chosen here, at the
|
|
269
|
+
edge, so that everything below it is the same code whether it is a test or
|
|
270
|
+
a person running it.
|
|
271
|
+
|
|
272
|
+
The address is the pinned one unless a development override is set, and the
|
|
273
|
+
pinned network key is never overridable: an address a person can point
|
|
274
|
+
somewhere else is a convenience, and a key a person can point somewhere else
|
|
275
|
+
is the removal of the only thing that makes a receipt mean anything.
|
|
276
|
+
"""
|
|
277
|
+
return PublicationService(
|
|
278
|
+
runs_dir=context.paths.runs_dir,
|
|
279
|
+
coordinates=packaged_publication_coordinates(),
|
|
280
|
+
endpoint_override=context.settings.publication_endpoint,
|
|
281
|
+
transport=HttpsPublicationTransport(),
|
|
282
|
+
clock=utc_now,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# The asking
|
|
288
|
+
#
|
|
289
|
+
# Three steps, in the order somebody meets them: find out whether there is
|
|
290
|
+
# anybody to ask, show them what would be sent, and take the one answer that
|
|
291
|
+
# covers all of it. They are separate functions because the middle one has to
|
|
292
|
+
# happen between the other two — an address asked for before the review would
|
|
293
|
+
# be asked of somebody who does not yet know what they are agreeing to.
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _require_an_answer_is_possible(
|
|
298
|
+
context: CliContext,
|
|
299
|
+
plan: PublicationPlan,
|
|
300
|
+
*,
|
|
301
|
+
assume_yes: bool,
|
|
302
|
+
reviewed_on: ReviewSurface,
|
|
303
|
+
) -> bool:
|
|
304
|
+
"""Return whether a person is about to be asked, or refuse to go on.
|
|
305
|
+
|
|
306
|
+
The shape is ``climb start``'s rather than ``run cancel``'s, because this is
|
|
307
|
+
the same kind of decision: something a person has to be shown before they
|
|
308
|
+
can meaningfully agree to it, and something a host agent may show in its own
|
|
309
|
+
conversation instead. ``--confirm`` on its own would record that a flag was
|
|
310
|
+
passed; ``--yes --reviewed-on host-agent`` records where somebody answered.
|
|
311
|
+
"""
|
|
312
|
+
if assume_yes:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
if reviewed_on is not ReviewSurface.CLI:
|
|
316
|
+
raise UsageError(
|
|
317
|
+
"--reviewed-on says where an agreement was already given, so it "
|
|
318
|
+
"goes with --yes; without it what would be sent is shown here and "
|
|
319
|
+
"answered here",
|
|
320
|
+
code=PUBLICATION_CONFIRMATION_REQUIRED,
|
|
321
|
+
details={"run_id": plan.run_id, "reviewed_on": reviewed_on.value},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if context.no_input:
|
|
325
|
+
raise UsageError(
|
|
326
|
+
"publishing sends this run's proof to the public run log, and a "
|
|
327
|
+
"published entry is withdrawn rather than deleted, so somebody has "
|
|
328
|
+
"to agree to it. Nothing here can be asked, so say so with --yes",
|
|
329
|
+
code=PUBLICATION_CONFIRMATION_REQUIRED,
|
|
330
|
+
details={"run_id": plan.run_id},
|
|
331
|
+
)
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _show_what_would_be_sent(
|
|
336
|
+
context: CliContext,
|
|
337
|
+
plan: PublicationPlan,
|
|
338
|
+
*,
|
|
339
|
+
skill_github_url: str | None,
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Print the review, before anything is asked about it."""
|
|
342
|
+
console = human_console(no_color=context.no_color)
|
|
343
|
+
for line in publication_review_lines(plan, skill_github_url=skill_github_url):
|
|
344
|
+
console.print(line)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _contributor_address(
|
|
348
|
+
context: CliContext, *, typed: str | None, asking: bool
|
|
349
|
+
) -> str | None:
|
|
350
|
+
"""Return the canonical address to send, or nothing at all.
|
|
351
|
+
|
|
352
|
+
The default is nothing, in every direction. Somebody who passed the option
|
|
353
|
+
has answered; somebody who is not being asked has not been asked, and no
|
|
354
|
+
address is sent; and a person at a terminal is asked once, with the answer
|
|
355
|
+
defaulting to no.
|
|
356
|
+
"""
|
|
357
|
+
if typed is not None:
|
|
358
|
+
return canonical_contributor_address(typed)
|
|
359
|
+
if not asking:
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
console = human_console(no_color=context.no_color)
|
|
363
|
+
console.print()
|
|
364
|
+
console.print(ADDRESS_QUESTION)
|
|
365
|
+
if not confirmed(_ADDRESS_PROMPT):
|
|
366
|
+
return None
|
|
367
|
+
return canonical_contributor_address(typer.prompt("Address"))
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _require_publication_confirmation(
|
|
371
|
+
context: CliContext, plan: PublicationPlan
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Take the answer, or send nothing."""
|
|
374
|
+
console = human_console(no_color=context.no_color)
|
|
375
|
+
console.print()
|
|
376
|
+
if not confirmed(_PUBLISH_PROMPT):
|
|
377
|
+
raise UsageError(
|
|
378
|
+
f"run {plan.run_id} was not published; its proof did not leave "
|
|
379
|
+
"this machine",
|
|
380
|
+
code=PUBLICATION_CONFIRMATION_REQUIRED,
|
|
381
|
+
details={"run_id": plan.run_id},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def publication_review_lines(
|
|
386
|
+
plan: PublicationPlan, *, skill_github_url: str | None = None
|
|
387
|
+
) -> list[str]:
|
|
388
|
+
"""Return what a person reads before they answer.
|
|
389
|
+
|
|
390
|
+
Exactly what would leave this machine, in the order somebody would ask it:
|
|
391
|
+
what it is, how much of it there is, where it is going, and what it does not
|
|
392
|
+
contain.
|
|
393
|
+
"""
|
|
394
|
+
return [
|
|
395
|
+
f"Publishing run {plan.run_id}",
|
|
396
|
+
"",
|
|
397
|
+
f"{plan.file_count} files, {plan.byte_count} bytes, to {plan.endpoint}",
|
|
398
|
+
f"Proof {plan.bundle_digest}",
|
|
399
|
+
f"Skill {plan.skill_name or 'candidate Skill'}",
|
|
400
|
+
f"GitHub {skill_github_url or 'none'}",
|
|
401
|
+
"",
|
|
402
|
+
_WHAT_TRAVELS,
|
|
403
|
+
"",
|
|
404
|
+
"The log shows arrivals in the order they arrive and ranks nothing. "
|
|
405
|
+
"An entry that is published stays published: it can be withdrawn, "
|
|
406
|
+
"which is recorded, and it is not deleted.",
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# Saying what happened
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _verify_proof(run_id: str) -> NextAction:
|
|
416
|
+
return NextAction(
|
|
417
|
+
id="verify_proof",
|
|
418
|
+
label="Verify this run's local proof again",
|
|
419
|
+
reason="It checks offline, from the bytes the run stored.",
|
|
420
|
+
cli=["techtree", "proof", "verify", run_id],
|
|
421
|
+
hermes_tool=None,
|
|
422
|
+
hermes_args=None,
|
|
423
|
+
requires_user_confirmation=False,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _render(data: object, console: Console) -> None:
|
|
428
|
+
if not isinstance(data, PublicationPayload):
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
render_pairs(
|
|
432
|
+
[
|
|
433
|
+
("Run", data.run_id),
|
|
434
|
+
("Entry", data.entry_url),
|
|
435
|
+
("Log sequence", str(data.log_sequence)),
|
|
436
|
+
("Accepted", data.accepted_at.isoformat()),
|
|
437
|
+
("Sent", f"{data.file_count} files, {data.byte_count} bytes"),
|
|
438
|
+
("Proof", data.bundle_digest),
|
|
439
|
+
("Receipt", data.receipt_path),
|
|
440
|
+
],
|
|
441
|
+
console,
|
|
442
|
+
)
|
|
443
|
+
console.print()
|
|
444
|
+
console.print(
|
|
445
|
+
"The log holds what was sent and the receipt holds what it answered. "
|
|
446
|
+
"Neither changes this run's own files, which are final."
|
|
447
|
+
)
|