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,407 @@
1
+ """Automated Cross-Compartment Barter Proof Bridge (node.planes.services.video.barter).
2
+
3
+ Wires verified work proofs from MeshNode's core service and storage compartments
4
+ into `TransitLedgerStore.credit_barter`:
5
+ - `blob_replication` (`blob_replicated` channel & `ReplicaStore`): 1 byte shored = 1 byte transit credit.
6
+ - `compute` (`job_executed` channel & `receipts_held` store): 1 compute fuel unit = 1 KB transit credit.
7
+ - `dvm` (`job_succeeded` channel & `recent_jobs` ring): 1 NIP-90 DVM job unit = 1 MB transit credit.
8
+ - `mediation` (`messages_drained` / `message_forwarded` channels): 1 mediated message = 4 KB transit credit.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ import time
14
+ from typing import Any, Callable, Mapping, Optional, TypedDict
15
+
16
+ from .ledger import BarterCreditRow, TransitLedgerStore
17
+
18
+
19
+ class BarterHookResult(TypedDict):
20
+ credited: bool
21
+ utility_kind: str
22
+ debtor: str
23
+ units: int
24
+ equivalent_transit_bytes: int
25
+ proof_ref: str
26
+ unsettled_bytes_after: int
27
+ degraded_audio_only_after: bool
28
+
29
+
30
+ def normalize_peer_pubkey(raw: str) -> str:
31
+ """Extract a normalized lowercase peer identifier / pubkey from `peer:<pk>`,
32
+ `did:nostr:<pk>`, `did:key:<id>`, or a raw hex pubkey.
33
+ """
34
+ val = (raw or '').strip()
35
+ if val.startswith('peer:'):
36
+ val = val[5:].strip()
37
+ elif val.startswith('did:nostr:'):
38
+ val = val[10:].strip()
39
+ return val.lower()
40
+
41
+
42
+ class BarterSettlementBridge:
43
+ """Listens to `NodeEventBus` channels and reconciles compartment stores to
44
+ automatically credit bartered network utility against unsettled video transit debt.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ ledger: TransitLedgerStore,
50
+ creditor_pk: str = '',
51
+ blob_store: Optional[Any] = None,
52
+ rhio_store: Optional[Any] = None,
53
+ on_settled: Optional[Callable[[BarterHookResult], None]] = None,
54
+ clock: Optional[Callable[[], int]] = None,
55
+ ) -> None:
56
+ # An injected clock, defaulting to wall time, so a suite can move time
57
+ # at the boundaries this code decides on. The compartment passes its own.
58
+ self._clock: Callable[[], int] = clock or (lambda: int(time.time()))
59
+ # What reconciliation could not credit, by utility kind, with the last
60
+ # reason for each. These were `except Exception: pass` around whole
61
+ # loops: a peer that had earned a credit simply never got it, and
62
+ # nothing anywhere said so.
63
+ self.skipped: dict[str, int] = {}
64
+ self.last_skip: dict[str, str] = {}
65
+ self.ledger = ledger
66
+ self.creditor_pk = (creditor_pk or ('b' * 64)).lower()
67
+ self.blob_store = blob_store
68
+ self.rhio_store = rhio_store
69
+ self.on_settled = on_settled
70
+ self._lock = threading.RLock()
71
+ self._seen_proofs: set[str] = set()
72
+ self._attached_bus: Optional[Any] = None
73
+
74
+ def _lookup_blob_size(self, sha256_hex: str) -> int:
75
+ clean = (sha256_hex or '').strip().lower()
76
+ if not clean:
77
+ return 0
78
+ if self.rhio_store is not None:
79
+ if hasattr(self.rhio_store, 'get_blob_meta'):
80
+ try:
81
+ meta = self.rhio_store.get_blob_meta(clean)
82
+ if meta and int(meta.get('size', 0)) > 0:
83
+ return int(meta['size'])
84
+ except Exception as e:
85
+ self._skipped('blob_lookup', e)
86
+ if hasattr(self.rhio_store, 'get_blob'):
87
+ try:
88
+ blob = self.rhio_store.get_blob(clean)
89
+ if blob is not None:
90
+ return len(blob)
91
+ except Exception as e:
92
+ self._skipped('blob_lookup', e)
93
+ if self.blob_store is not None:
94
+ if hasattr(self.blob_store, 'get'):
95
+ try:
96
+ blob = self.blob_store.get(clean)
97
+ if blob is not None:
98
+ return len(blob)
99
+ except Exception as e:
100
+ self._skipped('blob_lookup', e)
101
+ return 64 * 1024 # fallback 64 KB default when blob metadata is remote-only
102
+
103
+ def _apply_credit(
104
+ self,
105
+ *,
106
+ debtor: str,
107
+ utility_kind: Any,
108
+ units: int,
109
+ proof_ref: str,
110
+ now: Optional[int] = None,
111
+ ) -> Optional[BarterHookResult]:
112
+ norm_debtor = normalize_peer_pubkey(debtor)
113
+ if not norm_debtor or units <= 0 or not proof_ref:
114
+ return None
115
+ with self._lock:
116
+ if proof_ref in self._seen_proofs:
117
+ return None
118
+ self._seen_proofs.add(proof_ref)
119
+
120
+ row: BarterCreditRow = self.ledger.credit_barter(
121
+ debtor=norm_debtor,
122
+ creditor=self.creditor_pk,
123
+ utility_kind=utility_kind,
124
+ units=int(units),
125
+ proof_ref=proof_ref,
126
+ now=now,
127
+ )
128
+ summary = self.ledger.debt_summary(norm_debtor)
129
+ res: BarterHookResult = {
130
+ 'credited': True,
131
+ 'utility_kind': str(row['utility_kind']),
132
+ 'debtor': norm_debtor,
133
+ 'units': int(row['units']),
134
+ 'equivalent_transit_bytes': int(row['equivalent_transit_bytes']),
135
+ 'proof_ref': proof_ref,
136
+ 'unsettled_bytes_after': int(summary['unsettled_bytes']),
137
+ 'degraded_audio_only_after': bool(summary['degraded_audio_only']),
138
+ }
139
+ if self.on_settled is not None:
140
+ try:
141
+ self.on_settled(res)
142
+ except Exception as e:
143
+ self._skipped('on_settled', e)
144
+ return res
145
+
146
+ # ---- Explicit Proof Handlers --------------------------------------------
147
+
148
+ def on_blob_shored(
149
+ self,
150
+ peer: str,
151
+ sha256: str,
152
+ size_bytes: Optional[int] = None,
153
+ now: Optional[int] = None,
154
+ ) -> Optional[BarterHookResult]:
155
+ """Credit a peer for mirroring/shoring a content-addressed blob replica."""
156
+ norm_peer = normalize_peer_pubkey(peer)
157
+ clean_sha = (sha256 or '').strip().lower()
158
+ if not norm_peer or not clean_sha:
159
+ return None
160
+ byte_units = int(size_bytes) if (size_bytes is not None and size_bytes > 0) else self._lookup_blob_size(clean_sha)
161
+ proof_ref = f'blob_shoring:{norm_peer}:{clean_sha}'
162
+ return self._apply_credit(
163
+ debtor=norm_peer,
164
+ utility_kind='blob_shoring',
165
+ units=byte_units,
166
+ proof_ref=proof_ref,
167
+ now=now,
168
+ )
169
+
170
+ def on_verified_compute(
171
+ self,
172
+ peer: str,
173
+ job_id: str,
174
+ fuel_units: int = 1,
175
+ now: Optional[int] = None,
176
+ ) -> Optional[BarterHookResult]:
177
+ """Credit a peer for completing a verified compute execution job."""
178
+ norm_peer = normalize_peer_pubkey(peer)
179
+ clean_job = (job_id or '').strip()
180
+ if not norm_peer or not clean_job:
181
+ return None
182
+ units = max(1, int(fuel_units))
183
+ proof_ref = f'verified_compute:{norm_peer}:{clean_job}'
184
+ return self._apply_credit(
185
+ debtor=norm_peer,
186
+ utility_kind='verified_compute',
187
+ units=units,
188
+ proof_ref=proof_ref,
189
+ now=now,
190
+ )
191
+
192
+ def on_dvm_job_completed(
193
+ self,
194
+ peer: str,
195
+ job_id: str,
196
+ units: int = 1,
197
+ fuel: int = 0,
198
+ now: Optional[int] = None,
199
+ ) -> Optional[BarterHookResult]:
200
+ """Credit a peer for completing a NIP-90 DVM job."""
201
+ norm_peer = normalize_peer_pubkey(peer)
202
+ clean_job = (job_id or '').strip()
203
+ if not norm_peer or not clean_job:
204
+ return None
205
+ effective_units = max(1, int(units) + (int(fuel) // 1000))
206
+ proof_ref = f'dvm:{norm_peer}:{clean_job}'
207
+ return self._apply_credit(
208
+ debtor=norm_peer,
209
+ utility_kind='dvm',
210
+ units=effective_units,
211
+ proof_ref=proof_ref,
212
+ now=now,
213
+ )
214
+
215
+ def on_mediation_drained(
216
+ self,
217
+ peer: str,
218
+ count: int = 1,
219
+ batch_id: str = '',
220
+ now: Optional[int] = None,
221
+ ) -> Optional[BarterHookResult]:
222
+ """Credit a peer for DIDComm store-and-forward mediation work."""
223
+ norm_peer = normalize_peer_pubkey(peer)
224
+ if not norm_peer or count <= 0:
225
+ return None
226
+ now_val = self._clock() if now is None else int(now)
227
+ ref_suffix = batch_id or str(now_val)
228
+ proof_ref = f'mediation:{norm_peer}:{ref_suffix}'
229
+ return self._apply_credit(
230
+ debtor=norm_peer,
231
+ utility_kind='mediation',
232
+ units=int(count),
233
+ proof_ref=proof_ref,
234
+ now=now_val,
235
+ )
236
+
237
+ # ---- NodeEventBus Subscription & Compartment Reconciliation -------------
238
+
239
+ def attach_event_bus(self, bus: Any) -> None:
240
+ """Subscribe to `NodeEventBus` topics emitted by `Replication`, `Compute`,
241
+ `DVMGateway`, and `Mediation` so work events settle transit debt in real time.
242
+ """
243
+ if bus is None or not hasattr(bus, 'subscribe') or self._attached_bus is bus:
244
+ return
245
+ self._attached_bus = bus
246
+
247
+ def _on_blob_replicated(**kw: Any) -> None:
248
+ server = str(kw.get('server') or kw.get('peer') or '')
249
+ sha256 = str(kw.get('sha256') or '')
250
+ size = kw.get('size')
251
+ size_int = int(size) if isinstance(size, int) and size > 0 else None
252
+ if server and sha256:
253
+ self.on_blob_shored(peer=server, sha256=sha256, size_bytes=size_int)
254
+
255
+ def _on_job_executed(**kw: Any) -> None:
256
+ peer = str(kw.get('provider') or kw.get('worker') or kw.get('buyer') or kw.get('peer') or '')
257
+ job_id = str(kw.get('job_id') or kw.get('id') or '')
258
+ fuel = int(kw.get('fuel') or kw.get('units') or 1)
259
+ if peer and job_id:
260
+ self.on_verified_compute(peer=peer, job_id=job_id, fuel_units=max(1, fuel))
261
+
262
+ def _on_dvm_succeeded(**kw: Any) -> None:
263
+ peer = str(kw.get('provider') or kw.get('worker') or kw.get('peer') or kw.get('pubkey') or '')
264
+ job_id = str(kw.get('job_id') or '')
265
+ fuel = int(kw.get('fuel') or 0)
266
+ if peer and job_id:
267
+ self.on_dvm_job_completed(peer=peer, job_id=job_id, units=1, fuel=fuel)
268
+
269
+ def _on_messages_drained(**kw: Any) -> None:
270
+ peer = str(kw.get('mediator') or kw.get('peer') or kw.get('leaf') or '')
271
+ count = int(kw.get('count') or 1)
272
+ batch_id = str(kw.get('batch_id') or f'drain-{count}-{self._clock()}')
273
+ if peer and count > 0:
274
+ self.on_mediation_drained(peer=peer, count=count, batch_id=batch_id)
275
+
276
+ bus.subscribe('blob_replicated', _on_blob_replicated)
277
+ bus.subscribe('job_executed', _on_job_executed)
278
+ bus.subscribe('job_succeeded', _on_dvm_succeeded)
279
+ bus.subscribe('messages_drained', _on_messages_drained)
280
+
281
+ def _skipped(self, kind: str, exc: BaseException) -> None:
282
+ """Record a credit this pass could not apply, and why."""
283
+ self.skipped[kind] = self.skipped.get(kind, 0) + 1
284
+ self.last_skip[kind] = '%s: %s' % (type(exc).__name__, exc)
285
+
286
+ def reconcile_compartments(
287
+ self,
288
+ *,
289
+ blob_replication: Any = None,
290
+ compute: Any = None,
291
+ dvm: Any = None,
292
+ now: Optional[int] = None,
293
+ ) -> list[BarterHookResult]:
294
+ """Inspect active MeshNode compartments (`blob_replication`, `compute`, `dvm`)
295
+ and credit any uncredited work proofs for known peers.
296
+ """
297
+ results: list[BarterHookResult] = []
298
+
299
+ # 1. Reconcile held blob replicas in Replication.db
300
+ if blob_replication is not None and hasattr(blob_replication, 'db'):
301
+ try:
302
+ wants = blob_replication.db.wants()
303
+ except Exception as e:
304
+ wants = []
305
+ self._skipped('blob_shoring', e)
306
+ for sha256_hex, _copies in wants:
307
+ # Per item, not per pass: this whole loop used to sit inside
308
+ # one `except Exception: pass`, so a single malformed replica
309
+ # row abandoned every credit after it - silently, and the
310
+ # peers that had earned them were simply never paid.
311
+ try:
312
+ for rep in blob_replication.db.replicas(sha256_hex):
313
+ if rep.get('state') == 'held':
314
+ srv = str(rep.get('server') or '')
315
+ if srv.startswith('peer:') or len(srv) == 64:
316
+ r = self.on_blob_shored(peer=srv, sha256=sha256_hex, now=now)
317
+ if r is not None:
318
+ results.append(r)
319
+ except Exception as e:
320
+ self._skipped('blob_shoring', e)
321
+
322
+ # 2. Reconcile signed compute receipts in Compute.receipts_held
323
+ if compute is not None and hasattr(compute, 'receipts_held'):
324
+ for rec in list(compute.receipts_held):
325
+ try:
326
+ if isinstance(rec, Mapping):
327
+ raw_ev = rec.get('receipt')
328
+ ev_doc: Mapping[str, Any] = raw_ev if isinstance(raw_ev, Mapping) else {}
329
+ raw_payload = rec.get('payload')
330
+ payload_doc: Mapping[str, Any] = raw_payload if isinstance(raw_payload, Mapping) else {}
331
+ peer = str(
332
+ rec.get('pubkey')
333
+ or rec.get('provider')
334
+ or rec.get('buyer')
335
+ or ev_doc.get('pubkey')
336
+ or ''
337
+ )
338
+ if not peer and isinstance(ev_doc.get('tags'), list):
339
+ for tag in ev_doc['tags']:
340
+ if isinstance(tag, (list, tuple)) and len(tag) >= 2 and tag[0] == 'p':
341
+ peer = str(tag[1])
342
+ break
343
+ job_id = str(
344
+ rec.get('id')
345
+ or rec.get('job_id')
346
+ or payload_doc.get('ask')
347
+ or ev_doc.get('id')
348
+ or ''
349
+ )
350
+ fuel = int(
351
+ rec.get('fuel')
352
+ or rec.get('units')
353
+ or payload_doc.get('fuel_used')
354
+ or payload_doc.get('fuel')
355
+ or 1
356
+ )
357
+ if peer and job_id:
358
+ r = self.on_verified_compute(
359
+ peer=peer, job_id=job_id, fuel_units=max(1, fuel), now=now
360
+ )
361
+ if r is not None:
362
+ results.append(r)
363
+ except Exception as e:
364
+ self._skipped('verified_compute', e)
365
+
366
+ # 3. Reconcile DVM job completions in DVMGateway.recent_jobs
367
+ if dvm is not None and hasattr(dvm, 'recent_jobs'):
368
+ for item in list(dvm.recent_jobs):
369
+ try:
370
+ if isinstance(item, tuple) and len(item) >= 3:
371
+ first_str = str(item[0])
372
+ if first_str in ('ok', 'success', 'succeeded', 'completed'):
373
+ status = first_str
374
+ job_id = str(item[1])
375
+ peer = str(item[2])
376
+ fuel = int(item[3]) if len(item) >= 4 and isinstance(item[3], int) else 0
377
+ else:
378
+ job_id = first_str
379
+ status = str(item[1])
380
+ if len(item) >= 4:
381
+ if isinstance(item[2], int):
382
+ fuel = int(item[2])
383
+ peer = str(item[3])
384
+ else:
385
+ peer = str(item[2])
386
+ fuel = int(item[3]) if isinstance(item[3], int) else 0
387
+ elif isinstance(item[2], int):
388
+ fuel = int(item[2])
389
+ peer = ''
390
+ else:
391
+ fuel = 0
392
+ peer = str(item[2])
393
+ if status in ('ok', 'success', 'succeeded', 'completed') and peer and job_id:
394
+ r = self.on_dvm_job_completed(peer=peer, job_id=job_id, units=1, fuel=fuel, now=now)
395
+ if r is not None:
396
+ results.append(r)
397
+ except Exception as e:
398
+ self._skipped('dvm', e)
399
+
400
+ return results
401
+
402
+
403
+ __all__ = [
404
+ 'BarterHookResult',
405
+ 'normalize_peer_pubkey',
406
+ 'BarterSettlementBridge',
407
+ ]
@@ -0,0 +1,158 @@
1
+ """CLI commands for video streaming and transit settlement (Phase 5).
2
+
3
+ Provides the `stream` CLI verb (via plugin CLI hook or standalone provider)
4
+ supporting:
5
+ - `stream start`: create a new stream and genesis manifest in P2PandaLogStore
6
+ - `stream ingest`: package a segment into RhioBlobSync + P2PandaLogStore
7
+ - `stream status`: inspect active streams, manifest heads, and transit IOU debt
8
+ - `stream settle`: credit bartered network utility against unsettled transit debt
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ from typing import Optional, Sequence, cast
16
+
17
+ from .ledger import TransitLedgerStore, UtilityKind
18
+ from .packager import VideoStreamPackager
19
+
20
+
21
+ def _open_stores(node_dir: str) -> tuple[VideoStreamPackager, TransitLedgerStore]:
22
+ os.makedirs(node_dir, exist_ok=True)
23
+ from node.protocols.p2panda.store import P2PandaLogStore
24
+ from node.protocols.rhio.store import RhioBlobSync
25
+
26
+ p2p_db = os.path.join(node_dir, 'p2panda_v1.db')
27
+ rhio_db = os.path.join(node_dir, 'rhio_v1.db')
28
+ rhio_cas = os.path.join(node_dir, 'rhio_cas')
29
+ ledger_db = os.path.join(node_dir, 'video_transit_v1.db')
30
+
31
+ p2p_store = P2PandaLogStore(p2p_db)
32
+ rhio_store = RhioBlobSync(db_path=rhio_db, cas_dir=rhio_cas)
33
+ packager = VideoStreamPackager(p2panda_store=p2p_store, rhio_store=rhio_store)
34
+ ledger = TransitLedgerStore(ledger_db)
35
+ return packager, ledger
36
+
37
+
38
+ def cmd_stream(args: argparse.Namespace) -> int:
39
+ node_dir = str(getattr(args, 'dir', '.'))
40
+ action = str(getattr(args, 'stream_action', 'status'))
41
+ packager, ledger = _open_stores(node_dir)
42
+ try:
43
+ if action == 'start':
44
+ broadcaster = str(getattr(args, 'broadcaster', '') or ('a' * 64))
45
+ genesis, op = packager.create_stream(
46
+ stream_id=str(args.stream_id),
47
+ broadcaster=broadcaster,
48
+ title=str(getattr(args, 'title', 'Live Stream')),
49
+ price_msat_per_mb=int(getattr(args, 'price_msat_per_mb', 0)),
50
+ account_fee_msat=int(getattr(args, 'account_fee_msat', 0)),
51
+ )
52
+ out = {
53
+ 'ok': True,
54
+ 'action': 'start',
55
+ 'stream_id': genesis.stream_id,
56
+ 'broadcaster': genesis.broadcaster,
57
+ 'genesis_op_id': op.id,
58
+ 'seq_num': op.seq_num,
59
+ }
60
+ print(json.dumps(out, indent=2))
61
+ return 0
62
+
63
+ elif action == 'status':
64
+ debtor = getattr(args, 'debtor', None)
65
+ if debtor:
66
+ summary = ledger.debt_summary(str(debtor))
67
+ credits = ledger.list_barter_credits(str(debtor))
68
+ print(json.dumps({'debt': summary, 'barter_credits': credits}, indent=2))
69
+ else:
70
+ print(json.dumps({
71
+ 'p2panda_operations': packager.p2panda_store.count_operations(),
72
+ 'rhio_blobs': packager.rhio_store.count_blobs(),
73
+ }, indent=2))
74
+ return 0
75
+
76
+ elif action == 'settle':
77
+ debtor = str(args.debtor)
78
+ creditor = str(getattr(args, 'creditor', '') or ('b' * 64))
79
+ utility = cast(UtilityKind, str(args.utility))
80
+ units = int(args.units)
81
+ proof_ref = str(args.proof)
82
+ row = ledger.credit_barter(
83
+ debtor=debtor,
84
+ creditor=creditor,
85
+ utility_kind=utility,
86
+ units=units,
87
+ proof_ref=proof_ref,
88
+ )
89
+ summary = ledger.debt_summary(debtor)
90
+ print(json.dumps({'credited': row, 'debt_summary': summary}, indent=2))
91
+ return 0
92
+
93
+ return 1
94
+ finally:
95
+ ledger.close()
96
+ packager.p2panda_store.close()
97
+ packager.rhio_store.close()
98
+
99
+
100
+ def register_stream_cli(subparsers: argparse._SubParsersAction) -> None:
101
+ """Register the `stream` command on an argparse subparsers action."""
102
+ p = subparsers.add_parser(
103
+ 'stream',
104
+ help='manage live video streams, transit handovers, and bartered IOU settlement',
105
+ )
106
+ p.add_argument('dir', metavar='DIR', help='node directory')
107
+ p.add_argument(
108
+ 'stream_action',
109
+ choices=('start', 'status', 'settle'),
110
+ help='stream operation to execute',
111
+ )
112
+ p.add_argument('--stream-id', default='live-default', help='stream identifier')
113
+ p.add_argument('--broadcaster', default='', help='64-char hex broadcaster pubkey')
114
+ p.add_argument('--title', default='Live Stream', help='human-readable stream title')
115
+ p.add_argument('--price-msat-per-mb', type=int, default=0, help='operator egress price in msat/MB')
116
+ p.add_argument('--account-fee-msat', type=int, default=0, help='creator fee in msat per segment')
117
+ p.add_argument('--debtor', default='', help='debtor pubkey for status or barter settlement')
118
+ p.add_argument('--creditor', default='', help='creditor operator pubkey for barter settlement')
119
+ p.add_argument(
120
+ '--utility',
121
+ choices=('blob_shoring', 'verified_compute', 'dvm', 'mediation'),
122
+ default='blob_shoring',
123
+ help='bartered network utility type',
124
+ )
125
+ p.add_argument('--units', type=int, default=0, help='utility units to credit')
126
+ p.add_argument('--proof', default='', help='cryptographic proof or receipt reference')
127
+ p.set_defaults(fn=cmd_stream)
128
+
129
+
130
+ class VideoStreamCommandProvider:
131
+ """CommandProvider for video streaming CLI commands."""
132
+
133
+ cli = (('stream', register_stream_cli),)
134
+
135
+ @classmethod
136
+ def register(cls, subparsers: argparse._SubParsersAction) -> None:
137
+ register_stream_cli(subparsers)
138
+
139
+
140
+ def run_stream_cli(argv: Optional[Sequence[str]] = None) -> int:
141
+ """Standalone CLI entrypoint for invoking `stream` commands in tests or scripts."""
142
+ parser = argparse.ArgumentParser(prog='meshnode')
143
+ sub = parser.add_subparsers(dest='cmd')
144
+ register_stream_cli(sub)
145
+ ns = parser.parse_args(list(argv) if argv is not None else None)
146
+ fn = getattr(ns, 'fn', None)
147
+ if fn is None:
148
+ parser.print_help()
149
+ return 1
150
+ return int(fn(ns))
151
+
152
+
153
+ __all__ = [
154
+ 'cmd_stream',
155
+ 'register_stream_cli',
156
+ 'VideoStreamCommandProvider',
157
+ 'run_stream_cli',
158
+ ]