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.
Files changed (174) hide show
  1. techtree/__init__.py +35 -0
  2. techtree/__main__.py +14 -0
  3. techtree/canonical.py +239 -0
  4. techtree/catalog/__init__.py +25 -0
  5. techtree/catalog/repository.py +400 -0
  6. techtree/catalog/service.py +419 -0
  7. techtree/cli/__init__.py +1 -0
  8. techtree/cli/app.py +416 -0
  9. techtree/cli/commands/__init__.py +1 -0
  10. techtree/cli/commands/climb.py +1223 -0
  11. techtree/cli/commands/doctor.py +147 -0
  12. techtree/cli/commands/engine.py +207 -0
  13. techtree/cli/commands/proof.py +556 -0
  14. techtree/cli/commands/publish.py +447 -0
  15. techtree/cli/commands/release.py +303 -0
  16. techtree/cli/commands/run.py +1067 -0
  17. techtree/cli/commands/setup.py +181 -0
  18. techtree/cli/commands/skill.py +221 -0
  19. techtree/cli/commands/uplift.py +698 -0
  20. techtree/cli/commands/withdraw.py +212 -0
  21. techtree/cli/confirm.py +47 -0
  22. techtree/cli/context.py +96 -0
  23. techtree/cli/invoke.py +220 -0
  24. techtree/cli/output.py +280 -0
  25. techtree/constants.py +138 -0
  26. techtree/crypto.py +128 -0
  27. techtree/doctor/__init__.py +1 -0
  28. techtree/doctor/checks.py +675 -0
  29. techtree/doctor/execution_checks.py +435 -0
  30. techtree/doctor/service.py +326 -0
  31. techtree/drafts/__init__.py +32 -0
  32. techtree/drafts/source.py +146 -0
  33. techtree/drafts/store.py +992 -0
  34. techtree/engines/__init__.py +1 -0
  35. techtree/engines/bundle.py +251 -0
  36. techtree/engines/installer.py +679 -0
  37. techtree/engines/registry.py +235 -0
  38. techtree/engines/runner.py +170 -0
  39. techtree/errors.py +262 -0
  40. techtree/fs.py +234 -0
  41. techtree/harness.py +108 -0
  42. techtree/identity/__init__.py +41 -0
  43. techtree/identity/models.py +113 -0
  44. techtree/identity/service.py +199 -0
  45. techtree/identity/store.py +263 -0
  46. techtree/ids.py +85 -0
  47. techtree/manifests/__init__.py +39 -0
  48. techtree/manifests/builder.py +433 -0
  49. techtree/manifests/compare.py +376 -0
  50. techtree/models/__init__.py +282 -0
  51. techtree/models/base.py +201 -0
  52. techtree/models/campaign.py +484 -0
  53. techtree/models/catalog.py +227 -0
  54. techtree/models/cli.py +151 -0
  55. techtree/models/climb.py +254 -0
  56. techtree/models/data_policy.py +130 -0
  57. techtree/models/engine.py +156 -0
  58. techtree/models/episode_receipt.py +130 -0
  59. techtree/models/evaluation_backend.py +113 -0
  60. techtree/models/experiment.py +154 -0
  61. techtree/models/run.py +214 -0
  62. techtree/models/skill.py +156 -0
  63. techtree/models/uplift_report.py +158 -0
  64. techtree/models/validation.py +299 -0
  65. techtree/paths.py +116 -0
  66. techtree/presentation/__init__.py +31 -0
  67. techtree/presentation/build.py +1242 -0
  68. techtree/presentation/compact.py +246 -0
  69. techtree/presentation/evidence.py +169 -0
  70. techtree/presentation/models.py +358 -0
  71. techtree/presentation/rich.py +312 -0
  72. techtree/presentation/sanitize.py +156 -0
  73. techtree/publication/__init__.py +44 -0
  74. techtree/publication/address.py +180 -0
  75. techtree/publication/coordinates.py +26 -0
  76. techtree/publication/journal.py +212 -0
  77. techtree/publication/keccak.py +183 -0
  78. techtree/publication/models.py +209 -0
  79. techtree/publication/offer.py +35 -0
  80. techtree/publication/service.py +618 -0
  81. techtree/publication/transport.py +296 -0
  82. techtree/publication/verify.py +242 -0
  83. techtree/publication/withdraw.py +156 -0
  84. techtree/py.typed +0 -0
  85. techtree/receipts/__init__.py +52 -0
  86. techtree/receipts/bundle.py +578 -0
  87. techtree/receipts/compare.py +1065 -0
  88. techtree/receipts/episode.py +672 -0
  89. techtree/receipts/execution.py +630 -0
  90. techtree/receipts/observed.py +474 -0
  91. techtree/receipts/set.py +336 -0
  92. techtree/receipts/uplift.py +655 -0
  93. techtree/receipts/verify.py +1055 -0
  94. techtree/release/__init__.py +9 -0
  95. techtree/release/bootstrap.py +509 -0
  96. techtree/release/checks.py +376 -0
  97. techtree/release/document.py +125 -0
  98. techtree/release/generate.py +221 -0
  99. techtree/release/models.py +293 -0
  100. techtree/release/provenance.py +109 -0
  101. techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
  102. techtree/resources/catalog/catalog.json +32 -0
  103. techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
  104. techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
  105. techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
  106. techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
  107. techtree/resources/engines/default/engine.json +20 -0
  108. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
  109. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
  110. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
  111. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
  112. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
  113. techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
  114. techtree/resources/engines/default/pyproject.toml +23 -0
  115. techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
  116. techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
  117. techtree/resources/engines/default/tools/normalize_validation.py +222 -0
  118. techtree/resources/engines/default/uv.lock +1758 -0
  119. techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
  120. techtree/resources/release/build-provenance.json +4 -0
  121. techtree/resources/release/release-core.json +24 -0
  122. techtree/runs/__init__.py +31 -0
  123. techtree/runs/artifacts.py +750 -0
  124. techtree/runs/child_registry.py +228 -0
  125. techtree/runs/events.py +478 -0
  126. techtree/runs/executor.py +140 -0
  127. techtree/runs/fake.py +741 -0
  128. techtree/runs/launcher.py +253 -0
  129. techtree/runs/machine.py +489 -0
  130. techtree/runs/real.py +789 -0
  131. techtree/runs/service.py +616 -0
  132. techtree/runs/store.py +555 -0
  133. techtree/runs/validation.py +259 -0
  134. techtree/runs/variants.py +684 -0
  135. techtree/settings.py +143 -0
  136. techtree/skills/__init__.py +14 -0
  137. techtree/skills/archive.py +282 -0
  138. techtree/skills/policy.py +62 -0
  139. techtree/skills/scanner.py +394 -0
  140. techtree/skills/service.py +752 -0
  141. techtree/skills/starter.py +434 -0
  142. techtree/tasksets/__init__.py +1 -0
  143. techtree/tasksets/membership.py +269 -0
  144. techtree/tasksets/provider.py +207 -0
  145. techtree/tasksets/resolver.py +311 -0
  146. techtree/tasksets/service.py +484 -0
  147. techtree/tasksets/verifiers_cli.py +538 -0
  148. techtree/uplift/__init__.py +20 -0
  149. techtree/uplift/context.py +544 -0
  150. techtree/uplift/derive.py +203 -0
  151. techtree/uplift/public_tasks.py +151 -0
  152. techtree/uplift/service.py +719 -0
  153. techtree/uplift/source.py +160 -0
  154. techtree/verifiers/__init__.py +31 -0
  155. techtree/verifiers/budget.py +219 -0
  156. techtree/verifiers/child.py +633 -0
  157. techtree/verifiers/compiler.py +432 -0
  158. techtree/verifiers/config.py +365 -0
  159. techtree/verifiers/credentials.py +321 -0
  160. techtree/verifiers/image.py +126 -0
  161. techtree/verifiers/models.py +527 -0
  162. techtree/verifiers/outputs.py +368 -0
  163. techtree/verifiers/progress.py +192 -0
  164. techtree/verifiers/supervisor.py +341 -0
  165. techtree/verifiers/verify.py +782 -0
  166. techtree/version.py +39 -0
  167. techtree/worker/__init__.py +18 -0
  168. techtree/worker/execute.py +487 -0
  169. techtree/worker/main.py +57 -0
  170. techtree-0.1.0.dist-info/METADATA +344 -0
  171. techtree-0.1.0.dist-info/RECORD +174 -0
  172. techtree-0.1.0.dist-info/WHEEL +4 -0
  173. techtree-0.1.0.dist-info/entry_points.txt +3 -0
  174. 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
+ )