fleetmesh-node 0.2.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 (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,572 @@
1
+ """ComputeSession: the per-job state machine driving quote/accept/sealed/settled/refute.
2
+
3
+ Separate because it is the largest piece of compute/wire and the only one with per-job mutable state.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import time
11
+ from typing import Any, Callable, Collection, Mapping, Optional, Sequence, Union, cast
12
+
13
+ from fleetmesh_core.accounting import market
14
+ from fleetmesh_core.crypto import crypto
15
+ from fleetmesh_core.wire.interfaces import NostrEvent
16
+ from fleetmesh_core.wire.wire_types import (
17
+ ComputeAcceptBody, ComputeCancelBody, ComputeProgressBody, ComputeQuoteBody,
18
+ ComputeRefuteBody, ComputeSealedBody, ComputeSettledBody, ComputeStreamBody,
19
+ Message, ProgressState, ProofFailure, SealedVerify,
20
+ )
21
+ from node.planes.services.compute import executors as compute_executors, jobs as compute_jobs
22
+ from fleetmesh_core.identity.did import did_nostr as did_of
23
+ from .messages import (
24
+ ComputeWireError,
25
+ make_quote,
26
+ make_accept,
27
+ make_sealed,
28
+ make_settled,
29
+ make_refute,
30
+ make_stream_chunk,
31
+ make_progress,
32
+ make_cancel,
33
+ )
34
+ from .sealing import seal_result, open_sealed, encrypt_stream_frame, decrypt_stream_frame
35
+ from .verify import evaluate_job_evidence
36
+
37
+ # -------------------------------------------------------------------------
38
+ # Bilateral Compute State Machine (Sessions)
39
+ # -------------------------------------------------------------------------
40
+ class ComputeSession:
41
+ """Manages the bilateral state machine for a compute job."""
42
+
43
+ def __init__(
44
+ self,
45
+ role: str, # 'buyer' or 'seller'
46
+ my_sk: str,
47
+ peer_pk: str,
48
+ ask_id: str,
49
+ job_payload: Optional[Mapping[str, Any]] = None,
50
+ encrypted: bool = False,
51
+ ) -> None:
52
+ self.role = role
53
+ self.my_sk = my_sk
54
+ self.my_pk = crypto.pubkey(my_sk)
55
+ self.peer_pk = peer_pk
56
+ self.ask_id = ask_id
57
+ self.thid = ask_id
58
+ self.job_payload = dict(job_payload) if job_payload else None
59
+ self.state = 'INITIAL'
60
+ self.encrypted = encrypted
61
+ self._session_key: Optional[bytes] = None
62
+
63
+ # Negotiation parameters
64
+ # ours as built, or the peer's as received: either way read, not edited
65
+ self.quote_msg: Optional[Mapping[str, Any]] = None
66
+ self.accept_msg: Optional[Mapping[str, Any]] = None
67
+ self.sealed_msg: Optional[Mapping[str, Any]] = None
68
+ self.settled_msg: Optional[Mapping[str, Any]] = None
69
+ self.refute_msg: Optional[Message[ComputeRefuteBody]] = None
70
+ self.receipt_event: Optional[NostrEvent] = None
71
+
72
+ # Cryptographic keys
73
+ self.payment_preimage: Optional[str] = None
74
+ self.payment_hash: Optional[str] = None
75
+ self.raw_output: Optional[bytes] = None
76
+ self.result_hash: Optional[str] = None
77
+ self.fuel_used: int = 0
78
+ self._engine: str = 'wasmtime'
79
+ self._wasi_profile_digest: str = '0' * 64
80
+
81
+ # Streaming and progress tracking (compute/1.1)
82
+ self.stream_chunks: list[Mapping[str, Any]] = []
83
+ self.stream_buffer: list[str] = []
84
+ self.progress_history: list[Mapping[str, Any]] = []
85
+ self.cancel_msg: Optional[Message[ComputeCancelBody]] = None
86
+ self._cancellation_hooks: list[Callable[[str], None]] = []
87
+ self.last_seq: int = -1
88
+
89
+ def session_key(self) -> bytes:
90
+ """Derive the 32-byte ChaCha20Poly1305 symmetric session key via HKDF over ECDH."""
91
+ if self._session_key is None:
92
+ shared_x = crypto.ecdh_x(self.my_sk, self.peer_pk)
93
+ self._session_key = crypto.hkdf(
94
+ bytes.fromhex(shared_x),
95
+ info=b'fleetmesh/compute/session-stream/1',
96
+ salt=b'fleetmesh/compute/1',
97
+ length=32,
98
+ )
99
+ return self._session_key
100
+
101
+ # Seller methods
102
+ def seller_quote(
103
+ self,
104
+ price_msat: int,
105
+ invoice: str,
106
+ payment_hash: Optional[str] = None,
107
+ preimage_hex: Optional[str] = None,
108
+ estimated_duration_s: int = 10,
109
+ ) -> Message[ComputeQuoteBody]:
110
+ if self.role != 'seller':
111
+ raise ComputeWireError('not a seller')
112
+ if preimage_hex is not None:
113
+ self.payment_preimage = preimage_hex
114
+ self.payment_hash = hashlib.sha256(bytes.fromhex(preimage_hex)).hexdigest()
115
+ elif payment_hash is not None:
116
+ self.payment_hash = payment_hash
117
+ else:
118
+ self.payment_preimage = os.urandom(32).hex()
119
+ self.payment_hash = hashlib.sha256(bytes.fromhex(self.payment_preimage)).hexdigest()
120
+
121
+ q = make_quote(
122
+ from_did=did_of(self.my_pk),
123
+ to_did=did_of(self.peer_pk),
124
+ ask_id=self.ask_id,
125
+ price_msat=price_msat,
126
+ invoice=invoice,
127
+ payment_hash=self.payment_hash,
128
+ thid=self.thid,
129
+ estimated_duration_s=estimated_duration_s,
130
+ )
131
+ self.quote_msg = q
132
+ self.state = 'QUOTED'
133
+ return q
134
+
135
+ def seller_receive_accept(
136
+ self,
137
+ accept_msg: Mapping[str, Any],
138
+ ask_event: Optional[NostrEvent] = None,
139
+ registry: Optional[compute_executors.ExecutorRegistry] = None,
140
+ ) -> Message[ComputeSealedBody]:
141
+ if self.role != 'seller':
142
+ raise ComputeWireError('not a seller')
143
+ self.accept_msg = dict(accept_msg)
144
+ body = accept_msg['body']
145
+ job = body['job']
146
+ self.job_payload = job
147
+
148
+ if ask_event and not market.commitment_holds(ask_event, job):
149
+ raise ComputeWireError('accept job payload does not match ask commitment')
150
+
151
+ # Execute the job
152
+ reg = registry or compute_executors.DEFAULT_REGISTRY
153
+ exec_res = compute_jobs.run_job(job, registry=reg)
154
+ self.raw_output = exec_res['raw_output']
155
+ self.result_hash = exec_res['result_hash']
156
+ self.fuel_used = exec_res['fuel_used']
157
+ self._engine = str(exec_res.get('engine') or job.get('executor') or 'wasmtime')
158
+ self._wasi_profile_digest = str(exec_res.get('wasi_profile_digest') or ('0' * 64))
159
+
160
+ # Seal the result under preimage
161
+ seal_data = seal_result(self.raw_output, preimage_hex=self.payment_preimage)
162
+ self.payment_preimage = seal_data['payment_preimage']
163
+ self.payment_hash = seal_data['payment_hash']
164
+
165
+ verify_meta: Optional[SealedVerify] = None
166
+ verify_decl = job.get('verify', {})
167
+ if verify_decl.get('mode') == 'proof':
168
+ verify_meta = {
169
+ 'mode': 'proof',
170
+ 'proof_system': 'zk-verify-groth16',
171
+ 'proof': job.get('input', {}).get('proof', ''),
172
+ }
173
+
174
+ sealed = make_sealed(
175
+ from_did=did_of(self.my_pk),
176
+ to_did=did_of(self.peer_pk),
177
+ thid=self.thid,
178
+ ciphertext=seal_data['ciphertext'],
179
+ payment_hash=self.payment_hash,
180
+ result=self.result_hash,
181
+ fuel_used=self.fuel_used,
182
+ verify=verify_meta,
183
+ )
184
+ self.sealed_msg = sealed
185
+ self.state = 'SEALED'
186
+ return sealed
187
+
188
+ def seller_receive_settled(
189
+ self,
190
+ settled_msg: Mapping[str, Any],
191
+ blinding: Optional[str] = None,
192
+ ) -> NostrEvent:
193
+ if self.role != 'seller':
194
+ raise ComputeWireError('not a seller')
195
+ self.settled_msg = dict(settled_msg)
196
+ preimage = settled_msg['body']['payment_preimage']
197
+
198
+ # Verify preimage settles invoice
199
+ expected_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
200
+ if expected_hash != self.payment_hash:
201
+ raise ComputeWireError('disclosed preimage does not hash to payment_hash')
202
+
203
+ # Issue Kind 30811 Job Receipt
204
+ if self.job_payload is None:
205
+ raise ComputeWireError('no job payload accepted; nothing to receipt')
206
+ job_payload = self.job_payload
207
+ blind = blinding or crypto.sha256(os.urandom(32))
208
+ receipt_payload = {
209
+ 'ask': self.ask_id,
210
+ 'module': job_payload.get('module', {}).get('sha256', '0' * 64),
211
+ 'input': job_payload.get('input', {}).get('sha256', '0' * 64),
212
+ 'result': self.result_hash,
213
+ 'fuel_used': self.fuel_used,
214
+ 'paid_msat': self.quote_msg.get('body', {}).get('price_msat', 0) if self.quote_msg else 0,
215
+ 'payment_hash': self.payment_hash,
216
+ 'executor': {
217
+ 'engine': self._engine,
218
+ 'wasi_profile_digest': self._wasi_profile_digest,
219
+ },
220
+ 'verify': job_payload.get('verify', {'mode': 'none'}),
221
+ 'blinding': blind,
222
+ }
223
+ rec_ev = compute_jobs.build_receipt(
224
+ seller_sk=self.my_sk,
225
+ buyer_pk=self.peer_pk,
226
+ receipt_payload=receipt_payload,
227
+ blinding=blind,
228
+ )
229
+ self.receipt_event = rec_ev
230
+ self.state = 'RECEIPT_ISSUED'
231
+ return rec_ev
232
+
233
+ # Buyer methods
234
+ def buyer_accept_quote(
235
+ self,
236
+ quote_msg: Mapping[str, Any],
237
+ job_payload: Mapping[str, Any],
238
+ input_key: Optional[str] = None,
239
+ ) -> Message[ComputeAcceptBody]:
240
+ if self.role != 'buyer':
241
+ raise ComputeWireError('not a buyer')
242
+ self.quote_msg = dict(quote_msg)
243
+ self.payment_hash = quote_msg['body']['payment_hash']
244
+ self.job_payload = dict(job_payload)
245
+
246
+ acc = make_accept(
247
+ from_did=did_of(self.my_pk),
248
+ to_did=did_of(self.peer_pk),
249
+ thid=self.thid,
250
+ payment_hash=self.payment_hash,
251
+ job_payload=self.job_payload,
252
+ input_key=input_key,
253
+ )
254
+ self.accept_msg = acc
255
+ self.state = 'ACCEPTED'
256
+ return acc
257
+
258
+ def buyer_receive_sealed(
259
+ self,
260
+ sealed_msg: Mapping[str, Any],
261
+ preimage_hex: str,
262
+ ) -> Message[ComputeSettledBody]:
263
+ if self.role != 'buyer':
264
+ raise ComputeWireError('not a buyer')
265
+ self.sealed_msg = dict(sealed_msg)
266
+ self.payment_preimage = preimage_hex
267
+
268
+ settled = make_settled(
269
+ from_did=did_of(self.my_pk),
270
+ to_did=did_of(self.peer_pk),
271
+ thid=self.thid,
272
+ payment_preimage=preimage_hex,
273
+ )
274
+ self.settled_msg = settled
275
+ self.state = 'SETTLED'
276
+ return settled
277
+
278
+ def buyer_verify_and_settle(
279
+ self,
280
+ receipt_event: NostrEvent,
281
+ registry: Optional[compute_executors.ExecutorRegistry] = None,
282
+ pinned_vk_digests: Optional[Collection[str]] = None,
283
+ ) -> tuple[bool, Optional[Message[ComputeRefuteBody]]]:
284
+ """Verify delivery and result integrity. If invalid, returns (False, refute_msg)."""
285
+ if self.role != 'buyer':
286
+ raise ComputeWireError('not a buyer')
287
+ if self.job_payload is None:
288
+ # A buyer always carries the job it asked for; without it there is
289
+ # no declared verify mode to check the receipt against.
290
+ raise ComputeWireError('buyer has no job payload to verify against')
291
+ job_payload = self.job_payload
292
+ self.receipt_event = receipt_event
293
+
294
+ sealed_body = self.sealed_msg['body'] if self.sealed_msg else {}
295
+ ciphertext = sealed_body.get('ciphertext', '')
296
+ claimed_result = sealed_body.get('result', '')
297
+
298
+ # 1. Unseal ciphertext with preimage
299
+ ok_open, dec_bytes, fail_reason = open_sealed(
300
+ ciphertext=ciphertext,
301
+ preimage_hex=self.payment_preimage or '',
302
+ expected_payment_hash=self.payment_hash,
303
+ expected_result=claimed_result,
304
+ )
305
+
306
+ if not ok_open or dec_bytes is None:
307
+ # Delivery violation! (S3)
308
+ refute = make_refute(
309
+ from_did=did_of(self.my_pk),
310
+ to_did=did_of(self.peer_pk),
311
+ thid=self.thid,
312
+ job_receipt=receipt_event['id'],
313
+ mode=job_payload.get('verify', {}).get('mode', 'none'),
314
+ claimed_result=claimed_result,
315
+ actual_result='',
316
+ )
317
+ self.refute_msg = refute
318
+ self.state = 'REFUTED'
319
+ return False, refute
320
+
321
+ # 2. Verify according to declared mode
322
+ decl_mode = job_payload.get('verify', {}).get('mode', 'none')
323
+ valid, fail_kind, fail_detail = evaluate_job_evidence(
324
+ declared_mode=decl_mode,
325
+ sealed_body=sealed_body,
326
+ decrypted_output=dec_bytes,
327
+ job_payload=job_payload,
328
+ registry=registry,
329
+ pinned_vk_digests=pinned_vk_digests,
330
+ )
331
+
332
+ if not valid:
333
+ refute = make_refute(
334
+ from_did=did_of(self.my_pk),
335
+ to_did=did_of(self.peer_pk),
336
+ thid=self.thid,
337
+ job_receipt=receipt_event['id'],
338
+ mode=decl_mode,
339
+ claimed_result=claimed_result,
340
+ actual_result=fail_detail.get('actual_result') if fail_detail else '',
341
+ wasi_profile_digest=fail_detail.get('wasi_profile_digest') if fail_detail else '',
342
+ engine=fail_detail.get('engine') if fail_detail else '',
343
+ # Under proof the detail IS the verifier's rejection (proof_system,
344
+ # reason). It used to be looked up under a key it never had, so the
345
+ # refute's proof_failure slot went out empty on every disputed proof.
346
+ proof_failure=cast(ProofFailure, dict(fail_detail)) if fail_detail and fail_kind in ('proof-invalid', 'commitment-mismatch') else None,
347
+ )
348
+ self.refute_msg = refute
349
+ self.state = 'REFUTED'
350
+ return False, refute
351
+
352
+ self.state = 'COMPLETED'
353
+ return True, None
354
+
355
+ # compute/1.1 Streaming & Progress Extensions
356
+ def seller_send_stream_chunk(
357
+ self,
358
+ seq: int,
359
+ delta: str,
360
+ is_final: bool = False,
361
+ counters: Optional[Mapping[str, Any]] = None,
362
+ encrypted: Optional[bool] = None,
363
+ ) -> Message[ComputeStreamBody]:
364
+ """Produce and emit sequential compute/1.1/stream chunk frame."""
365
+ if self.role != 'seller':
366
+ raise ComputeWireError('not a seller')
367
+ if seq < 0:
368
+ raise ComputeWireError(f'stream seq negative ({seq})')
369
+ if seq <= self.last_seq and self.last_seq != -1:
370
+ raise ComputeWireError(f'stream seq out of order ({seq} <= {self.last_seq})')
371
+ self.last_seq = seq
372
+
373
+ do_encrypt = self.encrypted if encrypted is None else encrypted
374
+ frame_delta = delta
375
+ if do_encrypt:
376
+ frame_delta = encrypt_stream_frame(self.session_key(), seq, delta)
377
+
378
+ msg = make_stream_chunk(
379
+ from_did=did_of(self.my_pk),
380
+ to_did=did_of(self.peer_pk),
381
+ thid=self.thid,
382
+ seq=seq,
383
+ delta=frame_delta,
384
+ is_final=is_final,
385
+ counters=counters,
386
+ )
387
+ self.stream_chunks.append(msg)
388
+ self.stream_buffer.append(delta)
389
+ if not is_final:
390
+ self.state = 'STREAMING'
391
+ else:
392
+ self.state = 'STREAM_COMPLETED'
393
+ return msg
394
+
395
+ def buyer_receive_stream_chunk(
396
+ self,
397
+ stream_msg: Mapping[str, Any],
398
+ encrypted: Optional[bool] = None,
399
+ ) -> Mapping[str, Any]:
400
+ """Process incoming sequential stream chunk and reassemble output."""
401
+ if self.role != 'buyer':
402
+ raise ComputeWireError('not a buyer')
403
+ body = stream_msg.get('body', {})
404
+ seq = int(body.get('seq', 0))
405
+ raw_delta = str(body.get('delta', ''))
406
+ is_final = bool(body.get('is_final', False))
407
+
408
+ if seq < 0:
409
+ raise ComputeWireError(f'stream seq negative ({seq})')
410
+ if seq <= self.last_seq and self.last_seq != -1:
411
+ raise ComputeWireError(f'stream seq out of order ({seq} <= {self.last_seq})')
412
+ self.last_seq = seq
413
+
414
+ do_encrypt = self.encrypted if encrypted is None else encrypted
415
+ if do_encrypt:
416
+ delta = decrypt_stream_frame(self.session_key(), seq, raw_delta)
417
+ else:
418
+ delta = raw_delta
419
+
420
+ self.stream_chunks.append(dict(stream_msg))
421
+ self.stream_buffer.append(delta)
422
+ if not is_final:
423
+ self.state = 'STREAMING'
424
+ else:
425
+ self.state = 'STREAM_COMPLETED'
426
+ return {
427
+ 'seq': seq,
428
+ 'delta': delta,
429
+ 'is_final': is_final,
430
+ 'cumulative_text': ''.join(self.stream_buffer),
431
+ 'counters': body.get('counters', {}),
432
+ }
433
+
434
+ def seller_send_progress(
435
+ self,
436
+ state: ProgressState,
437
+ progress: Optional[float] = None,
438
+ elapsed_seconds: Optional[int] = None,
439
+ logs_tail: Optional[str] = None,
440
+ ) -> Message[ComputeProgressBody]:
441
+ """Emit compute/1.1/progress status update for long-running batch work."""
442
+ if self.role != 'seller':
443
+ raise ComputeWireError('not a seller')
444
+ msg = make_progress(
445
+ from_did=did_of(self.my_pk),
446
+ to_did=did_of(self.peer_pk),
447
+ thid=self.thid,
448
+ state=state,
449
+ progress=progress,
450
+ elapsed_seconds=elapsed_seconds,
451
+ logs_tail=logs_tail,
452
+ )
453
+ self.progress_history.append(msg)
454
+ return msg
455
+
456
+ def buyer_receive_progress(
457
+ self,
458
+ progress_msg: Mapping[str, Any],
459
+ ) -> Mapping[str, Any]:
460
+ """Record progress telemetry from seller."""
461
+ if self.role != 'buyer':
462
+ raise ComputeWireError('not a buyer')
463
+ self.progress_history.append(dict(progress_msg))
464
+ return dict(progress_msg.get('body', {}))
465
+
466
+ def create_progress_tracker(self) -> Callable[[str, float, Mapping[str, Any]], None]:
467
+ """Return a ProgressCallback that forwards telemetry updates to seller_send_progress."""
468
+ start_time = time.time()
469
+
470
+ def _tracker(state: str, progress: float, metadata: Mapping[str, Any]) -> None:
471
+ elapsed = int(time.time() - start_time)
472
+ logs_tail = str(metadata.get('logs_tail') or metadata.get('status') or '')
473
+ self.seller_send_progress(
474
+ state=cast(ProgressState, state),
475
+ progress=progress,
476
+ elapsed_seconds=elapsed,
477
+ logs_tail=logs_tail,
478
+ )
479
+
480
+ return _tracker
481
+
482
+ def register_cancellation_hook(self, hook: Callable[[str], None]) -> None:
483
+ """Register a callback to be invoked when the session is cancelled."""
484
+ self._cancellation_hooks.append(hook)
485
+
486
+ def cancel_session(
487
+ self,
488
+ reason: str,
489
+ penalty_msat: int = 0,
490
+ ) -> Message[ComputeCancelBody]:
491
+ """Abort ongoing compute session with reason and penalty terms."""
492
+ msg = make_cancel(
493
+ from_did=did_of(self.my_pk),
494
+ to_did=did_of(self.peer_pk),
495
+ thid=self.thid,
496
+ reason=reason,
497
+ penalty_msat=penalty_msat,
498
+ )
499
+ self.cancel_msg = msg
500
+ self.state = 'CANCELLED'
501
+ for hook in self._cancellation_hooks:
502
+ try:
503
+ hook(reason)
504
+ except Exception:
505
+ pass
506
+ return msg
507
+
508
+ def stream_over_quic(
509
+ self,
510
+ quic_engine: Any,
511
+ conn: Any,
512
+ chunks: Sequence[str],
513
+ encrypted: Optional[bool] = None,
514
+ ) -> list[Message[ComputeStreamBody]]:
515
+ """Stream a sequence of tokens directly over an active QUIC connection."""
516
+ sent_messages: list[Message[ComputeStreamBody]] = []
517
+ for i, delta in enumerate(chunks):
518
+ is_final = (i == len(chunks) - 1)
519
+ msg = self.seller_send_stream_chunk(seq=i, delta=delta, is_final=is_final, encrypted=encrypted)
520
+ if hasattr(quic_engine, 'send_stream') and conn is not None:
521
+ payload = json.dumps(msg).encode('utf-8')
522
+ quic_engine.send_stream(conn, payload)
523
+ sent_messages.append(msg)
524
+ return sent_messages
525
+
526
+ def stream_over_fips(
527
+ self,
528
+ flow_sock: Any,
529
+ chunks: Sequence[str],
530
+ fips_client: Optional[Any] = None,
531
+ encrypted: Optional[bool] = None,
532
+ ) -> list[Message[ComputeStreamBody]]:
533
+ """Stream sequential token frames directly over an active FIPS FSP datagram flow."""
534
+ sent_messages: list[Message[ComputeStreamBody]] = []
535
+ for i, delta in enumerate(chunks):
536
+ is_final = (i == len(chunks) - 1)
537
+ msg = self.seller_send_stream_chunk(seq=i, delta=delta, is_final=is_final, encrypted=encrypted)
538
+ if flow_sock is not None:
539
+ if fips_client is not None and hasattr(fips_client, 'send_framed'):
540
+ fips_client.send_framed(flow_sock, payload=msg, seq=i, is_final=is_final)
541
+ elif hasattr(flow_sock, 'send'):
542
+ from node.transport import fips_rung as fips_transport
543
+ packet = fips_transport.encode_frame(
544
+ seq=i,
545
+ payload=json.dumps(msg, sort_keys=True).encode('utf-8'),
546
+ is_final=is_final,
547
+ is_didcomm=True,
548
+ )
549
+ flow_sock.send(packet)
550
+ sent_messages.append(msg)
551
+ return sent_messages
552
+
553
+ @property
554
+ def cumulative_text(self) -> str:
555
+ """Assembled stream content received/sent so far."""
556
+ return "".join(self.stream_buffer)
557
+
558
+ def buyer_receive_fips_chunk(
559
+ self,
560
+ raw_frame: Union[bytes, Mapping[str, Any]],
561
+ ) -> Mapping[str, Any]:
562
+ """Receive and decode a binary-framed FSP datagram chunk or payload."""
563
+ if isinstance(raw_frame, Mapping):
564
+ msg = raw_frame
565
+ else:
566
+ from node.transport import fips_rung as fips_transport
567
+ try:
568
+ seq, flags, payload = fips_transport.decode_frame(raw_frame)
569
+ msg = json.loads(payload.decode('utf-8'))
570
+ except Exception:
571
+ msg = json.loads(raw_frame.decode('utf-8'))
572
+ return self.buyer_receive_stream_chunk(msg)
@@ -0,0 +1,49 @@
1
+ """Types and records for the ComputeWire transport compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, TypedDict
5
+
6
+
7
+ class ComputeDisputeRecord(TypedDict):
8
+ """Refute received from a peer, stored in the disputes ring."""
9
+ peer: str
10
+ refute: Any
11
+
12
+
13
+ class ComputeSessionSummary(TypedDict):
14
+ """Summary of an active bilateral compute session."""
15
+ role: str
16
+ peer: str
17
+ state: str
18
+
19
+
20
+ class ComputeWireStateReport(TypedDict):
21
+ """State report for operator status and telemetry."""
22
+ active_sessions: int
23
+ disputes_filed: int
24
+ refusals: int
25
+
26
+
27
+ class EvidenceDetail(TypedDict, total=False):
28
+ """Why evaluate_job_evidence said no, keyed by failure kind."""
29
+ claimed_result: str
30
+ actual_result: str
31
+ wasi_profile_digest: str
32
+ engine: str
33
+ engine_version: str
34
+ proof_system: str
35
+ reason: str
36
+ detail: str
37
+ path: list[str]
38
+ schema_path: list[str]
39
+ pattern: str
40
+ error: str
41
+
42
+
43
+ class SealedResult(TypedDict, total=False):
44
+ """Result of sealing a compute job output under a preimage."""
45
+ ciphertext: str
46
+ payment_hash: str
47
+ payment_preimage: str
48
+ result: str
49
+ result_hash: str