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
node/channels/bus.py ADDED
@@ -0,0 +1,331 @@
1
+ """Internal publish/subscribe event bus for decoupled node communication.
2
+
3
+ Replaces cyclic cross-subsystem dependencies and monkey-patching with an
4
+ in-process event bus. Standard library only.
5
+
6
+ The bus is synchronous and in-process. The `Tap` is how it reaches outside:
7
+ every emit is also serialised through the topic's wire function in WIRE and
8
+ appended to a ring buffer with a sequence number, and each `Subscriber`
9
+ holds a bounded queue the control socket drains. The emitting thread never
10
+ waits on a reader: a full queue drops its oldest entry and the subscriber
11
+ is told how many went by. Nothing here is durable across a restart and
12
+ nothing claims to be - a reader that needs every event replays the relays.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import collections
17
+ import json
18
+ import queue
19
+ import threading
20
+ import time
21
+ from typing import Any, Callable, Dict, List, Mapping, Optional, TypedDict
22
+
23
+ # Standard event bus topics
24
+ TOPIC_WINDOW_EVALUATED: str = 'window.evaluated'
25
+ TOPIC_WINDOW_REPRICED: str = 'window.repriced'
26
+ TOPIC_SESSION_COMPLETED: str = 'session.completed'
27
+ TOPIC_GRANT_ISSUED: str = 'grant.issued'
28
+ TOPIC_PEER_CONNECTED: str = 'peer.connected'
29
+ TOPIC_REPORT_ADMITTED: str = 'report.admitted'
30
+ TOPIC_META_DROPPED: str = 'meta.dropped' # only on a subscriber's own stream: {count}
31
+
32
+
33
+ class MetaDroppedWire(TypedDict):
34
+ count: int
35
+
36
+
37
+ class LossyWire(TypedDict, total=False):
38
+ lossy: bool
39
+ repr: str
40
+ error: str
41
+
42
+
43
+ class WindowEvaluatedWire(TypedDict, total=False):
44
+ peer: str
45
+ epoch: int
46
+ severity: str
47
+ measured: Optional[Mapping[str, int]]
48
+ claimed: Optional[Mapping[str, int]]
49
+ closed: bool
50
+
51
+
52
+ class WindowRepricedWire(TypedDict):
53
+ peer: str
54
+ epoch: int
55
+ was: str
56
+ now: str
57
+
58
+
59
+ class SessionCompletedWire(TypedDict, total=False):
60
+ peer: str
61
+ role: str
62
+ status: Optional[str]
63
+ counters: Optional[Mapping[str, int | float]]
64
+ findings: int
65
+
66
+
67
+ class GrantIssuedWire(TypedDict):
68
+ peer: str
69
+ cause: str
70
+ envelope: str
71
+ tier: Optional[str]
72
+
73
+
74
+ class PeerConnectedWire(TypedDict):
75
+ peer: str
76
+ role: str
77
+ transport: Optional[str]
78
+
79
+
80
+ class ReportAdmittedWire(TypedDict):
81
+ reporter: str
82
+ target: str
83
+ id: str
84
+ admissible: bool
85
+ contradicted: bool
86
+
87
+
88
+ WirePayload = (
89
+ WindowEvaluatedWire
90
+ | WindowRepricedWire
91
+ | SessionCompletedWire
92
+ | GrantIssuedWire
93
+ | PeerConnectedWire
94
+ | ReportAdmittedWire
95
+ | MetaDroppedWire
96
+ | LossyWire
97
+ | Mapping[str, Any]
98
+ )
99
+
100
+
101
+ class BusEnvelope(TypedDict, total=False):
102
+ seq: Optional[int]
103
+ ts: int
104
+ topic: str
105
+ data: WirePayload
106
+
107
+
108
+ class BusState(TypedDict):
109
+ seq: int
110
+ ring: int
111
+ ring_max: Optional[int]
112
+ subscribers: int
113
+ oldest: Optional[int]
114
+
115
+
116
+ EventHandler = Callable[..., Any]
117
+ WireFn = Callable[..., WirePayload]
118
+
119
+ def _wire_evaluated(rec: Any) -> WindowEvaluatedWire:
120
+ rec = rec if isinstance(rec, dict) else {}
121
+ return {k: rec.get(k) for k in ('peer', 'epoch', 'severity', 'measured', 'claimed', 'closed') if k in rec} # type: ignore[return-value]
122
+
123
+
124
+ def _wire_repriced(peer: str, epoch: int, was: str, now: str) -> WindowRepricedWire:
125
+ return {'peer': peer, 'epoch': epoch, 'was': was, 'now': now}
126
+
127
+
128
+ def _wire_session(peer: str, role: str, result: Any) -> SessionCompletedWire:
129
+ result = result if isinstance(result, dict) else {}
130
+ return {'peer': peer, 'role': role, 'status': result.get('status'),
131
+ 'counters': result.get('counters'), 'findings': len(result.get('findings') or ())}
132
+
133
+
134
+ def _wire_grant(peer: str, cause: str, envelope_id: str, tier: Optional[str]) -> GrantIssuedWire:
135
+ return {'peer': peer, 'cause': cause, 'envelope': envelope_id, 'tier': tier}
136
+
137
+
138
+ def _wire_peer(peer: str, role: str, transport: Optional[str]) -> PeerConnectedWire:
139
+ return {'peer': peer, 'role': role, 'transport': transport}
140
+
141
+
142
+ def _wire_report(reporter: str, target: str, event_id: str, admissible: bool, contradicted: bool) -> ReportAdmittedWire:
143
+ return {'reporter': reporter, 'target': target, 'id': event_id, 'admissible': admissible, 'contradicted': contradicted}
144
+
145
+
146
+ # topic -> how its emit arguments go on the wire. Every topic a node emits
147
+ # has an entry, so what a subscriber sees is a decision made next to the
148
+ # emit, not whatever json.dumps(default=str) makes of a Python object. A
149
+ # topic with no entry is still delivered, as {"lossy": true, "repr": ...},
150
+ # so a missing entry is visible rather than a silent drop. Plugins add
151
+ # their own with register_wire; they may not replace one of these.
152
+ WIRE: Dict[str, WireFn] = {
153
+ TOPIC_WINDOW_EVALUATED: _wire_evaluated,
154
+ TOPIC_WINDOW_REPRICED: _wire_repriced,
155
+ TOPIC_SESSION_COMPLETED: _wire_session,
156
+ TOPIC_GRANT_ISSUED: _wire_grant,
157
+ TOPIC_PEER_CONNECTED: _wire_peer,
158
+ TOPIC_REPORT_ADMITTED: _wire_report,
159
+ }
160
+
161
+
162
+ def register_wire(topic: str, fn: WireFn) -> None:
163
+ """A plugin's own topic, with how it goes on the wire. Adds, never replaces."""
164
+ if topic in WIRE:
165
+ raise ValueError('topic %s already has a wire form; a plugin may add topics, not redefine them' % topic)
166
+ WIRE[topic] = fn
167
+
168
+
169
+ def to_wire(topic: str, args: tuple, kwargs: dict) -> WirePayload:
170
+ fn = WIRE.get(topic)
171
+ if fn is None:
172
+ return {'lossy': True, 'repr': repr((args, kwargs))[:512]}
173
+ try:
174
+ data = fn(*args, **kwargs)
175
+ json.dumps(data) # prove it is wire-safe here, on the emitting side
176
+ return data
177
+ except Exception as e: # a wire function must never take the emitter down
178
+ return {'lossy': True, 'repr': repr((args, kwargs))[:512], 'error': '%s: %s' % (type(e).__name__, e)}
179
+
180
+
181
+ class Subscriber(object):
182
+ """One reader's bounded queue. `next(timeout)` blocks for one envelope or
183
+ returns None; `dropped` is how many were pushed out by a full queue since
184
+ the last one the reader took, reported to it as a meta.dropped envelope."""
185
+
186
+ def __init__(self, topics: Optional[set[str]], maxsize: int) -> None:
187
+ self.topics: Optional[set[str]] = topics # None: every topic
188
+ self.q: queue.Queue[BusEnvelope] = queue.Queue(maxsize=maxsize)
189
+ self.dropped: int = 0
190
+ self._lock = threading.Lock()
191
+
192
+ def wants(self, topic: str) -> bool:
193
+ return self.topics is None or topic in self.topics
194
+
195
+ def push(self, env: BusEnvelope) -> None:
196
+ with self._lock:
197
+ while True:
198
+ try:
199
+ self.q.put_nowait(env)
200
+ return
201
+ except queue.Full:
202
+ try:
203
+ self.q.get_nowait()
204
+ self.dropped += 1
205
+ except queue.Empty:
206
+ return
207
+
208
+ def next(self, timeout: float) -> Optional[BusEnvelope]:
209
+ with self._lock:
210
+ if self.dropped:
211
+ n, self.dropped = self.dropped, 0
212
+ return {'seq': None, 'ts': int(time.time()), 'topic': TOPIC_META_DROPPED, 'data': {'count': n}}
213
+ try:
214
+ return self.q.get(timeout=timeout)
215
+ except queue.Empty:
216
+ return None
217
+
218
+
219
+ class Tap(object):
220
+ """Sequence numbers, a ring of the last `ring` envelopes, and the live subscribers."""
221
+
222
+ def __init__(self, ring: int = 1000, queue_size: int = 1000, clock: Optional[Callable[[], int]] = None) -> None:
223
+ self.seq: int = 0
224
+ self.ring: collections.deque[BusEnvelope] = collections.deque(maxlen=ring)
225
+ self.queue_size: int = queue_size
226
+ self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
227
+ self._subs: List[Subscriber] = []
228
+ self._lock = threading.Lock()
229
+
230
+ def record(self, topic: str, args: tuple, kwargs: dict) -> BusEnvelope:
231
+ data = to_wire(topic, args, kwargs)
232
+ with self._lock:
233
+ self.seq += 1
234
+ env: BusEnvelope = {'seq': self.seq, 'ts': self.clock(), 'topic': topic, 'data': data}
235
+ self.ring.append(env)
236
+ subs = [s for s in self._subs if s.wants(topic)]
237
+ for s in subs:
238
+ s.push(env)
239
+ return env
240
+
241
+ def subscribe(self, topics: Optional[set[str]] = None) -> Subscriber:
242
+ s = Subscriber(topics, self.queue_size)
243
+ with self._lock:
244
+ self._subs.append(s)
245
+ return s
246
+
247
+ def unsubscribe(self, s: Subscriber) -> None:
248
+ with self._lock:
249
+ if s in self._subs:
250
+ self._subs.remove(s)
251
+
252
+ def since(self, seq: int, topics: Optional[set[str]] = None, last: int = 0) -> List[BusEnvelope]:
253
+ """Envelopes after `seq` still in the ring, oldest first; `last` keeps only that many newest."""
254
+ with self._lock:
255
+ out = [e for e in self.ring if (e.get('seq') or 0) > seq and (topics is None or e['topic'] in topics)]
256
+ return out[-last:] if last else out
257
+
258
+ def state(self) -> BusState:
259
+ with self._lock:
260
+ return {'seq': self.seq, 'ring': len(self.ring), 'ring_max': self.ring.maxlen,
261
+ 'subscribers': len(self._subs), 'oldest': self.ring[0]['seq'] if self.ring else None}
262
+
263
+
264
+ class NodeEventBus:
265
+ """Thread-safe synchronous in-process event bus."""
266
+
267
+ def __init__(self) -> None:
268
+ self._subscribers: Dict[str, List[EventHandler]] = collections.defaultdict(list)
269
+ self._lock: threading.RLock = threading.RLock()
270
+ self._emit_counts: collections.Counter[str] = collections.Counter()
271
+ self.tap: Optional[Tap] = None # set by the node; tests may leave it None
272
+
273
+ def subscribe(self, topic: str, handler: EventHandler) -> None:
274
+ """Subscribe a callable to a topic."""
275
+ with self._lock:
276
+ if handler not in self._subscribers[topic]:
277
+ self._subscribers[topic].append(handler)
278
+
279
+ def unsubscribe(self, topic: str, handler: EventHandler) -> bool:
280
+ """Unsubscribe a callable from a topic. Returns True if removed."""
281
+ with self._lock:
282
+ if topic in self._subscribers:
283
+ try:
284
+ self._subscribers[topic].remove(handler)
285
+ return True
286
+ except ValueError:
287
+ return False
288
+ return False
289
+
290
+ def emit(self, topic: str, *args: Any, **kwargs: Any) -> List[Any]:
291
+ """Emit an event to all subscribers of the topic.
292
+
293
+ Exceptions in handlers are caught and isolated so other subscribers still run.
294
+ Returns a list of handler return values.
295
+ """
296
+ with self._lock:
297
+ self._emit_counts[topic] += 1
298
+ handlers = list(self._subscribers.get(topic, ()))
299
+ if self.tap is not None:
300
+ self.tap.record(topic, args, kwargs)
301
+
302
+ results = []
303
+ for h in handlers:
304
+ try:
305
+ res = h(*args, **kwargs)
306
+ results.append(res)
307
+ except Exception:
308
+ results.append(None)
309
+ return results
310
+
311
+ def subscribers(self, topic: str) -> List[EventHandler]:
312
+ """Return a snapshot list of subscribers for a topic."""
313
+ with self._lock:
314
+ return list(self._subscribers.get(topic, ()))
315
+
316
+ def topics(self) -> List[str]:
317
+ """Return all topics that have at least one active subscriber."""
318
+ with self._lock:
319
+ return [t for t, subs in self._subscribers.items() if subs]
320
+
321
+ @property
322
+ def counts(self) -> collections.Counter[str]:
323
+ """Return emission counts by topic."""
324
+ with self._lock:
325
+ return collections.Counter(self._emit_counts)
326
+
327
+ def clear(self) -> None:
328
+ """Remove all subscriptions."""
329
+ with self._lock:
330
+ self._subscribers.clear()
331
+ self._emit_counts.clear()
@@ -0,0 +1,46 @@
1
+ """How a node says things to its journal.
2
+
3
+ These two lived in app.py, where a compartment could not reach them without
4
+ importing the node that imports the compartment. They are not the node's:
5
+ they are how anything in the node writes a line an operator will read.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ import time
11
+ from typing import Any
12
+
13
+
14
+ def line(*parts: Any) -> None:
15
+ """One timestamped line on stderr, flushed.
16
+
17
+ A node runs under systemd, where both streams are the journal, and the
18
+ first deployment of two of these found them completely silent: `journalctl
19
+ -u meshnode` held one line, the one systemd wrote itself. An operator could
20
+ not tell whether the node had peered, pulled, been declined or crashed
21
+ without reading a JSON file it had never been told about. A daemon that
22
+ does work says what it did.
23
+
24
+ It says it on **stderr**, and that is not cosmetic. It used to go to
25
+ stdout, which is also where `meshnode <cmd> --json` writes its answer, and
26
+ a node busy enough to say something while a command was answering put a
27
+ line like
28
+
29
+ 2026-09-05T21:56:24Z < acked-out 67dd799a6be0… accepted
30
+ [{"epoch": 496527, ...}]
31
+
32
+ on one stream. The reader got `Extra data: line 1 column 5`. node/README.md
33
+ offers `--json` and the control socket to "a dashboard, a metrics exporter,
34
+ a fleet script"; a stream that is usually parseable is not an API. So
35
+ stdout is the answer to what was asked, stderr is the running commentary,
36
+ and systemd captures both exactly as before.
37
+ """
38
+ print('%s %s' % (time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
39
+ ' '.join(str(p) for p in parts)), file=sys.stderr, flush=True)
40
+
41
+
42
+ def short(x: Any) -> str:
43
+ """Long hex is unreadable in a log and its first bytes identify it."""
44
+ s = str(x)
45
+ return s[:12] + '…' if len(s) > 16 and all(c in '0123456789abcdef' for c in s) else s
46
+
@@ -0,0 +1,140 @@
1
+ """Pluggable command providers for the meshnode CLI.
2
+
3
+ Each module under node/commands owns a group of subcommands and exposes one
4
+ provider class with a `register(subparsers)` classmethod. PROVIDERS, in the
5
+ order below, is the complete parser: meshnode.main iterates it and registers
6
+ nothing else, and scripts/check-cli.py builds the same parser to hold the
7
+ documentation to it. Command functions are imported from their own module
8
+ (`from node.commands.transport_cmds import cmd_fips`), not re-exported here.
9
+
10
+ The modules that share a name with a top-level package (compute, transport,
11
+ cluster) carry a `_cmds` suffix. Inside this package `from .compute import X`
12
+ and `from ..compute import X` are one character apart and name different
13
+ modules; the suffix keeps the relative import from ever shadowing the package
14
+ it wraps.
15
+
16
+ A plugin (docs/extending.md) may bring verbs of its own. It declares them as
17
+ a class attribute `cli = (('verb', register_fn), ...)`; `register_fn(sub)` is
18
+ handed the same subparsers object every provider gets. `plugin_cli_classes`
19
+ finds the plugins a `meshnode VERB DIR ...` invocation refers to by reading
20
+ DIR's config.json (the first argument that is a node directory), or the
21
+ `FLEETMESH_PLUGINS` environment variable (comma-separated refs) when the
22
+ command has no directory yet; `register_plugin_cli` adds their verbs. A verb
23
+ that collides with a core command is refused by name, like a plugin whose
24
+ compartment name collides.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import importlib
30
+ import os
31
+ from typing import Any, Iterable, Protocol, Sequence, Type
32
+
33
+ from .cluster_cmds import ClusterCommandProvider
34
+ from .compute_cmds import ComputeCommandProvider
35
+ from .core import CoreCommandProvider
36
+ from .gossip_cmds import GossipCommandProvider
37
+ from .nsite_cmds import NsiteCommandProvider
38
+ from .peering import PeeringCommandProvider
39
+ from .plugin_cmds import PluginCommandProvider
40
+ from .sync import SyncCommandProvider
41
+ from ..compartment import Alias, BoundCommand, Command, CommandRecord, declared, every_command
42
+ from .sidecar_cmds import SidecarsCommandProvider
43
+ from .transport_cmds import TransportCommandProvider
44
+ from .video_cmds import VideoCommandProvider
45
+
46
+
47
+ class CommandProvider(Protocol):
48
+ """Protocol for modular CLI command providers."""
49
+
50
+ @classmethod
51
+ def register(cls, subparsers: argparse._SubParsersAction) -> None:
52
+ """Register subcommands and argument definitions."""
53
+ ...
54
+
55
+
56
+ PROVIDERS: tuple[Type[CommandProvider], ...] = (
57
+ CoreCommandProvider,
58
+ PeeringCommandProvider,
59
+ SyncCommandProvider,
60
+ ComputeCommandProvider,
61
+ GossipCommandProvider,
62
+ TransportCommandProvider,
63
+ ClusterCommandProvider,
64
+ NsiteCommandProvider,
65
+ SidecarsCommandProvider,
66
+ PluginCommandProvider,
67
+ )
68
+
69
+ PLUGINS_ENV = 'FLEETMESH_PLUGINS'
70
+
71
+
72
+ def _import_ref(ref: str) -> Any:
73
+ mod_name, _, cls_name = ref.partition(':')
74
+ try:
75
+ mod = importlib.import_module(mod_name)
76
+ except ImportError as e:
77
+ raise ValueError('plugin %s: cannot import %s (%s)' % (ref, mod_name, e)) from e
78
+ cls = getattr(mod, cls_name, None)
79
+ if cls is None:
80
+ raise ValueError('plugin %s: %s has no %s' % (ref, mod_name, cls_name))
81
+ return cls
82
+
83
+
84
+ def plugin_refs_for(argv: Sequence[str] | None) -> list[str]:
85
+ """The plugin refs an invocation refers to: DIR's config.json `plugins`
86
+ block when an argument is a node directory, else FLEETMESH_PLUGINS."""
87
+ refs: list[str] = []
88
+ for arg in (argv or ()):
89
+ if os.path.isfile(os.path.join(str(arg), 'config.json')):
90
+ try:
91
+ from ..app.config import load_config
92
+ raw = load_config(str(arg)).get('plugins') or []
93
+ except Exception:
94
+ raw = []
95
+ for e in raw:
96
+ ref = e if isinstance(e, str) else (e or {}).get('ref')
97
+ if isinstance(ref, str) and ref not in refs:
98
+ refs.append(ref)
99
+ break
100
+ for ref in (os.environ.get(PLUGINS_ENV) or '').split(','):
101
+ ref = ref.strip()
102
+ if ref and ref not in refs:
103
+ refs.append(ref)
104
+ return refs
105
+
106
+
107
+ def plugin_cli_classes(argv: Sequence[str] | None) -> list[Any]:
108
+ """The plugin classes that declare a `cli` hook, for this invocation."""
109
+ out: list[Any] = []
110
+ for ref in plugin_refs_for(argv):
111
+ cls = _import_ref(ref)
112
+ if getattr(cls, 'cli', None):
113
+ out.append(cls)
114
+ return out
115
+
116
+
117
+ def register_plugin_cli(sub: argparse._SubParsersAction, classes: Iterable[Any]) -> list[str]:
118
+ """Add every `cli` verb the classes declare; returns the verbs added."""
119
+ added: list[str] = []
120
+ for cls in classes:
121
+ for verb, register in (getattr(cls, 'cli', None) or ()):
122
+ if verb in sub.choices:
123
+ raise ValueError('plugin %s: command %r already exists' % (getattr(cls, 'name', cls.__name__), verb))
124
+ register(sub)
125
+ added.append(verb)
126
+ return added
127
+
128
+
129
+ # A literal list, not built from splats: pyflakes reads __all__ statically and
130
+ # only a literal tells it these re-exports are used. Same reason
131
+ # node/config/__init__.py keeps one.
132
+ __all__ = [
133
+ # the declarative command surface, re-exported for compartments
134
+ 'Alias', 'BoundCommand', 'Command', 'CommandRecord', 'declared', 'every_command',
135
+ # the CLI providers
136
+ 'CommandProvider', 'PROVIDERS', 'CoreCommandProvider', 'GossipCommandProvider',
137
+ 'NsiteCommandProvider', 'PeeringCommandProvider', 'PluginCommandProvider',
138
+ 'SidecarsCommandProvider', 'SyncCommandProvider', 'TransportCommandProvider',
139
+ 'VideoCommandProvider', 'register_plugin_cli',
140
+ ]