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,499 @@
1
+ """A sidecar, and the bridge compartment that is the node's only way to reach it.
2
+
3
+ Three words, kept apart because the node had one word for two things (audit
4
+ 2026-10-07-3, finding "two things called sidecar"):
5
+
6
+ compartment in-process. Built, wired, started, ticked, restarted and
7
+ retired by the Registry; its whole lifecycle is the node's.
8
+
9
+ sidecar a separate process the node reaches but does not contain:
10
+ fips (Rust), libp2p (sixty-three packages, trio, will not
11
+ install on 3.14), bacalhau, grasp, strfry. Kept out because its
12
+ codebase, its dependencies, its runtime or its licence do not
13
+ belong in `node/` - which is standard library plus
14
+ `cryptography`, so that a second implementer never inherits
15
+ any of it. The node never starts or supervises one
16
+ (`supervised` is False, and stays False until code honours it).
17
+
18
+ bridge the ONE compartment that fronts one sidecar. Everything on the
19
+ node that needs a sidecar goes through its bridge - names it in
20
+ `requires`/`uses` and calls `bridge.call()` or a method the
21
+ bridge offers - and nothing else opens the sidecar's socket.
22
+ So the sidecar's settings (PARAMS: endpoint, timeout), what it
23
+ says (CHANNELS), what it measures (METRICS), what an operator
24
+ can ask of it (COMMANDS) and how it fails (LIFECYCLE) are
25
+ declared once, on the bridge, the way every other compartment
26
+ declares them.
27
+
28
+ What a bridge gets from this base, so that every sidecar is reached the same way:
29
+
30
+ - `SIDECAR`, a manifest: the protocol it speaks (`family/major`), what it
31
+ wraps upstream, the command that runs it standalone, and the request that
32
+ proves it is there.
33
+ - a link state - unknown, up, down, incompatible - moved only by `call()`,
34
+ said once each way on three declared channels and counted:
35
+ `<name>.sidecar.reachable|unreachable|incompatible`.
36
+ - the version gate (audit S1): every reply carries the protocol the sidecar
37
+ speaks, and a different family or major is refused - a bridge that read a
38
+ `/2` reply as `/1` would be wrong silently. On a link that is not up the
39
+ manifest's `probe` goes first as a handshake, so a sidecar that speaks
40
+ another major is refused before it is sent anything that carries data.
41
+ - two commands on the control socket: `<name>.sidecar` (the manifest and the
42
+ link now) and `<name>.probe` (ask the sidecar now).
43
+ - `Lifecycle(restart=True)`: a bridge holds a link and nothing more, so a
44
+ restart forgets the link and the next call finds out afresh. A wedged link
45
+ that trips the breaker is restarted by its quarantine trial.
46
+ - trouble when the sidecar is wanted and down or incompatible, naming what
47
+ this node loses (`CONSEQUENCE`).
48
+
49
+ Two ways a sidecar runs, both described by the same manifest:
50
+
51
+ bridge mode the sidecar is already running - ours, or one on the market
52
+ (a strfry, a bacalhau) - and the bridge compartment in the
53
+ node talks to it.
54
+ standalone `SIDECAR.standalone` is the command that runs it on its own,
55
+ beside the node or on another host; the bridge does not care
56
+ who started it, only that its endpoint answers.
57
+
58
+ The transport is a unix socket carrying one JSON object per line, which is what
59
+ announce, p2p and fips already speak. HTTP (the class-A manifests in
60
+ node/sidecars: grasp, ncontainer, magpie) arrives with the first bridge that
61
+ needs it; `TRANSPORTS` says what is implemented and a manifest naming another
62
+ is refused at import rather than at the first call.
63
+
64
+ Named `sidecar_bridge` because `node/bridge` is the Universal Nostr Relay
65
+ Bridge, a compartment called `bridge` that fronts relays rather than a sidecar.
66
+ A package rather than a root module for the reason node/lifecycle is one.
67
+ Standard library only.
68
+ """
69
+ from __future__ import annotations
70
+
71
+ import dataclasses
72
+ import json
73
+ import socket
74
+ import threading
75
+ import time
76
+ from typing import Any, Callable, Mapping, Optional, TypedDict
77
+
78
+ from .. import channels as ch_mod
79
+ from .. import metrics as mx_mod
80
+ from . import lifecycle as lifecycle_mod
81
+ from .base import Compartment
82
+ from .command import Command
83
+
84
+ TRANSPORTS: tuple[str, ...] = ('unix',)
85
+ MAX_LINE: int = 1 << 20
86
+
87
+ # The link, as the bridge last found it.
88
+ UNKNOWN = 'unknown' # not asked since start (or restart)
89
+ UP = 'up' # the last call was answered, in a protocol this bridge speaks
90
+ DOWN = 'down' # the last call was not answered, or was refused
91
+ INCOMPATIBLE = 'incompatible' # answered, in a protocol family or major this bridge does not speak
92
+ LINKS: tuple[str, ...] = (UNKNOWN, UP, DOWN, INCOMPATIBLE)
93
+
94
+
95
+ class SidecarError(Exception):
96
+ """No sidecar, a sidecar that will not answer, or one that says no."""
97
+
98
+
99
+ class SidecarIncompatible(SidecarError):
100
+ """A sidecar that answered in a protocol this bridge does not speak."""
101
+
102
+
103
+ class SidecarRecord(TypedDict):
104
+ name: str
105
+ protocol: str
106
+ upstream: str
107
+ transport: str
108
+ standalone: str
109
+ probe: str
110
+ supervised: bool
111
+
112
+
113
+ class LinkRecord(TypedDict):
114
+ sidecar: SidecarRecord
115
+ endpoint: str
116
+ link: str
117
+ since: int
118
+ protocol: str # what the sidecar last said it speaks; '' when it did not say
119
+ last_error: str
120
+ calls: int
121
+ failures: int
122
+
123
+
124
+ class ProbeRecord(TypedDict, total=False):
125
+ """What `<name>.probe` answers: the link, and the status or the error."""
126
+ link: str
127
+ protocol: str
128
+ status: object
129
+ error: str
130
+
131
+
132
+ @dataclasses.dataclass(frozen=True)
133
+ class Sidecar:
134
+ """What a bridge fronts. Declared on the bridge as `SIDECAR`.
135
+
136
+ name the sidecar's name; also the bridge compartment's
137
+ protocol `family/major` this bridge speaks, e.g. 'fleetmesh-announce/1'
138
+ upstream what the sidecar wraps, for a person: 'py-libp2p gossipsub'
139
+ transport how it is reached; one of TRANSPORTS
140
+ standalone the command that runs it on its own, for the docs and the
141
+ `sidecar` command; '' when it has none
142
+ probe the request that proves it is there and returns its status
143
+ supervised False: the node depends on it and never starts or restarts
144
+ it. Refused when True until the code that would honour it
145
+ exists - a declared supervisor nothing runs is worse than none.
146
+ """
147
+ name: str
148
+ protocol: str
149
+ upstream: str
150
+ transport: str = 'unix'
151
+ standalone: str = ''
152
+ probe: str = 'status'
153
+ supervised: bool = False
154
+
155
+ def __post_init__(self) -> None:
156
+ family, _, major = self.protocol.rpartition('/')
157
+ if not family or not major.isdigit():
158
+ raise ValueError('sidecar %s: protocol is family/major, got %r' % (self.name, self.protocol))
159
+ if self.transport not in TRANSPORTS:
160
+ raise ValueError('sidecar %s: transport %r is not implemented (have: %s)'
161
+ % (self.name, self.transport, ', '.join(TRANSPORTS)))
162
+ if self.supervised:
163
+ raise ValueError('sidecar %s: supervised=True is not honoured yet; the node depends on a '
164
+ 'sidecar and never starts it' % self.name)
165
+
166
+ @property
167
+ def family(self) -> str:
168
+ return self.protocol.rpartition('/')[0]
169
+
170
+ @property
171
+ def major(self) -> str:
172
+ return self.protocol.rpartition('/')[2]
173
+
174
+ def compatible(self, said: str) -> bool:
175
+ """Whether a sidecar that says it speaks `said` speaks this bridge's
176
+ protocol. Same family, same major; a minor (`/1.3`) is additive. An
177
+ unstated protocol is accepted - the sidecars predate the field - and
178
+ the link records that it was not stated."""
179
+ if not said:
180
+ return True
181
+ family, _, version = said.rpartition('/')
182
+ return family == self.family and version.split('.')[0] == self.major
183
+
184
+ def describe(self) -> SidecarRecord:
185
+ return {'name': self.name, 'protocol': self.protocol, 'upstream': self.upstream,
186
+ 'transport': self.transport, 'standalone': self.standalone, 'probe': self.probe,
187
+ 'supervised': self.supervised}
188
+
189
+
190
+ def jsonline_call(path: str, cmd: str, timeout: float = 5.0, **args: Any) -> tuple[Any, str]:
191
+ """One request, one reply, one connection, over a unix socket.
192
+
193
+ Returns (result, the protocol the reply said it speaks, or ''). Raises
194
+ SidecarError for all of no sidecar, a sidecar that will not answer, and a
195
+ sidecar that says no. The wire was announce's; it is every unix sidecar's.
196
+ """
197
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
198
+ s.settimeout(timeout)
199
+ try:
200
+ try:
201
+ s.connect(path)
202
+ except OSError as e:
203
+ raise SidecarError('no sidecar at %s (%s)' % (path, e.strerror or e))
204
+ try:
205
+ s.sendall((json.dumps({'cmd': cmd, 'args': args}) + '\n').encode('utf-8'))
206
+ buf = b''
207
+ while not buf.endswith(b'\n'):
208
+ chunk = s.recv(65536)
209
+ if not chunk:
210
+ break
211
+ buf += chunk
212
+ if len(buf) > MAX_LINE:
213
+ raise SidecarError('the sidecar answered with more than %d bytes' % MAX_LINE)
214
+ except socket.timeout:
215
+ raise SidecarError('the sidecar did not answer within %.0fs' % timeout)
216
+ except OSError as e:
217
+ raise SidecarError('the sidecar closed the socket (%s)' % (e.strerror or e))
218
+ finally:
219
+ s.close()
220
+ if not buf:
221
+ raise SidecarError('the sidecar closed the socket without answering')
222
+ try:
223
+ reply = json.loads(buf.decode('utf-8'))
224
+ except (ValueError, UnicodeDecodeError) as e:
225
+ raise SidecarError('the sidecar answered something that is not JSON (%s)' % e)
226
+ if not isinstance(reply, dict):
227
+ raise SidecarError('a reply is an object')
228
+ said = str(reply.get('protocol') or '')
229
+ if not reply.get('ok'):
230
+ raise SidecarError(str(reply.get('error') or 'refused'))
231
+ return reply.get('result'), said
232
+
233
+
234
+ def probe_unix_socket(path: str, request: Mapping[str, Any], timeout: float = 1.0) -> Optional[Mapping[str, Any]]:
235
+ """Send one JSON line over a unix socket and return the parsed JSON object, or None on error."""
236
+ try:
237
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
238
+ s.settimeout(timeout)
239
+ s.connect(path)
240
+ s.sendall(json.dumps(dict(request)).encode('utf-8') + b'\n')
241
+ from ..transport import lineproto
242
+ resp = lineproto.read_json_line(s, cap=MAX_LINE)
243
+ s.close()
244
+ if isinstance(resp, dict):
245
+ return resp
246
+ except Exception:
247
+ pass
248
+ return None
249
+
250
+
251
+ # ---- what every bridge declares, derived from its manifest ---------------------
252
+ def bridge_channels(name: str) -> tuple[ch_mod.Channel, ...]:
253
+ return (
254
+ ch_mod.Channel(
255
+ name='%s.sidecar.reachable' % name,
256
+ description='The %s sidecar answered, in a protocol this bridge speaks.' % name,
257
+ level=ch_mod.INFO, wire=lambda endpoint, protocol: {'endpoint': endpoint, 'protocol': protocol},
258
+ say='sidecar up at {endpoint} ({protocol})', tags=(name, 'sidecar', 'bridge')),
259
+ ch_mod.Channel(
260
+ name='%s.sidecar.unreachable' % name,
261
+ description='The %s sidecar did not answer, or refused; what needs it waits.' % name,
262
+ level=ch_mod.NOTE, wire=lambda why, endpoint: {'why': why, 'endpoint': endpoint},
263
+ say='sidecar not reachable at {endpoint} ({why})', tags=(name, 'sidecar', 'bridge')),
264
+ ch_mod.Channel(
265
+ name='%s.sidecar.incompatible' % name,
266
+ description='The %s sidecar answered in a protocol this bridge does not speak.' % name,
267
+ level=ch_mod.TROUBLE, wire=lambda said, wants: {'said': said, 'wants': wants},
268
+ say='sidecar speaks {said}; this bridge speaks {wants}', tags=(name, 'sidecar', 'bridge')),
269
+ )
270
+
271
+
272
+ def bridge_metrics(name: str) -> tuple[mx_mod.Metric, ...]:
273
+ return (
274
+ mx_mod.Metric(name='%s_sidecar_reachable' % name,
275
+ description='Times the %s sidecar was found reachable.' % name,
276
+ kind=mx_mod.COUNTER, counts='%s.sidecar.reachable' % name, tags=(name, 'sidecar')),
277
+ mx_mod.Metric(name='%s_sidecar_unreachable' % name,
278
+ description='Times the %s sidecar was found unreachable.' % name,
279
+ kind=mx_mod.COUNTER, counts='%s.sidecar.unreachable' % name, tags=(name, 'sidecar')),
280
+ mx_mod.Metric(name='%s_sidecar_incompatible' % name,
281
+ description='Times the %s sidecar answered in a protocol this bridge does not speak.' % name,
282
+ kind=mx_mod.COUNTER, counts='%s.sidecar.incompatible' % name, tags=(name, 'sidecar')),
283
+ )
284
+
285
+
286
+ def bridge_commands(sc: Sidecar) -> tuple[Command, ...]:
287
+ return (
288
+ Command(
289
+ key='sidecar', name='%s sidecar' % sc.name.capitalize(),
290
+ description='the sidecar this bridge fronts - what it wraps, the protocol it must speak, how to '
291
+ 'run it standalone - and the link to it as last found',
292
+ tags=(sc.name, 'sidecar', 'bridge'), permission='%s/sidecar' % sc.name,
293
+ run='bridge_state',
294
+ reply='{sidecar, endpoint, link, since, protocol, last_error, calls, failures}'),
295
+ Command(
296
+ key='probe', name='Probe %s sidecar' % sc.name,
297
+ description='ask the sidecar now (its `%s` request) rather than wait for the next tick' % sc.probe,
298
+ tags=(sc.name, 'sidecar', 'bridge'), permission='%s/probe' % sc.name,
299
+ process=(sc.name,), run='probe',
300
+ reply='{link, protocol, status | error}'),
301
+ )
302
+
303
+
304
+ def _merged(base: tuple[Any, ...], own: tuple[Any, ...], key: Callable[[Any], str]) -> tuple[Any, ...]:
305
+ """The bridge's own declarations, then the base's it did not declare itself."""
306
+ have = {key(x) for x in own}
307
+ return tuple(own) + tuple(x for x in base if key(x) not in have)
308
+
309
+
310
+ class Bridge(Compartment, abstract=True):
311
+ """The compartment that fronts one sidecar. Subclass, declare `SIDECAR`,
312
+ implement `endpoint()`, and reach the sidecar only through `call()`.
313
+
314
+ Optional hooks: `wanted()` (False turns the link checks and trouble off,
315
+ e.g. a sidecar the config has not enabled) and `forget()` (what the bridge
316
+ must forget when its link is reset, at start and on every restart).
317
+ """
318
+
319
+ SIDECAR: Optional[Sidecar] = None
320
+ # What this node loses while the sidecar is down, for the trouble line.
321
+ CONSEQUENCE: str = 'what needs it waits'
322
+ LIFECYCLE: lifecycle_mod.Lifecycle = lifecycle_mod.Lifecycle(restart=True)
323
+ timeout: float = 5.0
324
+
325
+ def __init_subclass__(cls, **kw: Any) -> None:
326
+ sc = cls.__dict__.get('SIDECAR')
327
+ if isinstance(sc, Sidecar):
328
+ if cls.name and cls.name != sc.name:
329
+ raise TypeError('bridge %s fronts sidecar %s: a bridge is named for its sidecar'
330
+ % (cls.name, sc.name))
331
+ name = sc.name
332
+ cls.CHANNELS = _merged(bridge_channels(name), tuple(cls.__dict__.get('CHANNELS', ())),
333
+ lambda c: c.name)
334
+ cls.METRICS = _merged(bridge_metrics(name), tuple(cls.__dict__.get('METRICS', ())),
335
+ lambda m: m.name)
336
+ cls.COMMANDS = _merged(bridge_commands(sc), tuple(cls.__dict__.get('COMMANDS', ())),
337
+ lambda c: c.key)
338
+ super().__init_subclass__(**kw)
339
+
340
+ def __init__(self, node: Any, clock: Optional[Callable[[], int]] = None) -> None:
341
+ super().__init__(node, clock)
342
+ self._link_lock: threading.Lock = threading.Lock()
343
+ self._reset_link()
344
+
345
+ # ---- what a subclass provides ---------------------------------------------
346
+ @property
347
+ def sidecar(self) -> Sidecar:
348
+ if self.SIDECAR is None:
349
+ raise TypeError('%s declares no SIDECAR' % type(self).__name__)
350
+ return self.SIDECAR
351
+
352
+ def endpoint(self) -> str:
353
+ raise NotImplementedError('%s must say where its sidecar listens' % type(self).__name__)
354
+
355
+ def wanted(self) -> bool:
356
+ return True
357
+
358
+ def forget(self) -> None:
359
+ """What a reset link must take with it. Nothing, by default."""
360
+
361
+ # ---- the link --------------------------------------------------------------
362
+ def _reset_link(self) -> None:
363
+ self.link: str = UNKNOWN
364
+ self.link_since: int = self._now()
365
+ self.link_protocol: str = ''
366
+ self.link_error: str = ''
367
+ self.calls: int = 0
368
+ self.call_failures: int = 0
369
+ self._up: Optional[bool] = None # the reachability the journal was last told
370
+ self._said: Optional[str] = None # the last unreachable reason it was told
371
+
372
+ def start(self) -> None:
373
+ # A fresh link: the next call finds out. Nothing is dialled here, so a
374
+ # sidecar that hangs cannot hold the node's start.
375
+ with self._link_lock:
376
+ self._reset_link()
377
+ self.forget()
378
+
379
+ def stop(self) -> Any:
380
+ with self._link_lock:
381
+ self._reset_link()
382
+ return None
383
+
384
+ def _now(self) -> int:
385
+ return int(getattr(self, 'clock', time.time)())
386
+
387
+ def call(self, cmd: str, **args: Any) -> Any:
388
+ """The only way to the sidecar. Moves the link and says so once each way.
389
+
390
+ On a link that is not up, the manifest's `probe` request goes first,
391
+ as a handshake: the version gate must refuse BEFORE a request carrying
392
+ data reaches a sidecar that would misread it, not after. One extra
393
+ round trip per (re)connection, none while the link is up.
394
+
395
+ Raises SidecarError (SidecarIncompatible for a protocol this bridge
396
+ does not speak); the caller decides what a missing sidecar costs it.
397
+ """
398
+ if self.link != UP and cmd != self.sidecar.probe:
399
+ self._exchange(self.sidecar.probe, {})
400
+ return self._exchange(cmd, args)
401
+
402
+ def _exchange(self, cmd: str, args: Mapping[str, Any]) -> Any:
403
+ now = self._now()
404
+ kw = dict(args)
405
+ to = float(kw.pop('timeout', self.timeout))
406
+ try:
407
+ result, said = jsonline_call(self.endpoint(), cmd, timeout=to, **kw)
408
+ except SidecarError as e:
409
+ self.call_failures += 1
410
+ self._down(str(e), now)
411
+ raise
412
+ if not self.sidecar.compatible(said):
413
+ self.call_failures += 1
414
+ self._incompatible(said, now)
415
+ raise SidecarIncompatible('the %s sidecar speaks %s; this bridge speaks %s'
416
+ % (self.sidecar.name, said, self.sidecar.protocol))
417
+ self.calls += 1
418
+ self.link_protocol = said
419
+ self._reachable()
420
+ return result
421
+
422
+ def probe(self) -> ProbeRecord:
423
+ """Ask the sidecar now. Never raises: the answer says what happened."""
424
+ try:
425
+ status = self.call(self.sidecar.probe)
426
+ except SidecarError as e:
427
+ return {'link': self.link, 'protocol': self.link_protocol, 'error': str(e)}
428
+ return {'link': self.link, 'protocol': self.link_protocol, 'status': status}
429
+
430
+ def _move_link(self, to: str, now: int, error: str = '') -> bool:
431
+ with self._link_lock:
432
+ moved = to != self.link
433
+ if moved:
434
+ self.link, self.link_since = to, now
435
+ self.link_error = error
436
+ return moved
437
+
438
+ def _down(self, why: str, now: int) -> None:
439
+ self._move_link(DOWN, now, why)
440
+ if self._up is not False or self._said != why:
441
+ self.say('%s.sidecar.unreachable' % self.sidecar.name, why, self.endpoint())
442
+ self._said = why
443
+ self.refusals.refuse('sidecar', 'not reachable', subject=self.endpoint(),
444
+ detail={'error': why}, at=now)
445
+ self._up = False
446
+ self.forget() # it must be told again what it was told before
447
+
448
+ def _incompatible(self, said: str, now: int) -> None:
449
+ if self._move_link(INCOMPATIBLE, now, 'speaks %s' % said):
450
+ self.say('%s.sidecar.incompatible' % self.sidecar.name, said, self.sidecar.protocol)
451
+ self.refusals.refuse('sidecar', 'incompatible', subject=self.endpoint(),
452
+ detail={'said': said, 'wants': self.sidecar.protocol}, at=now)
453
+ self._up = False
454
+ self.forget()
455
+
456
+ def _reachable(self) -> None:
457
+ self._move_link(UP, self._now())
458
+ if self._up is not True:
459
+ self.say('%s.sidecar.reachable' % self.sidecar.name, self.endpoint(),
460
+ self.link_protocol or 'protocol unstated')
461
+ self._up = True
462
+ self._said = None
463
+
464
+ @property
465
+ def available(self) -> bool:
466
+ """Whether a compartment that depends on this sidecar should try now."""
467
+ return self.link == UP
468
+
469
+ # ---- being watched ---------------------------------------------------------
470
+ def bridge_state(self) -> LinkRecord:
471
+ return {'sidecar': self.sidecar.describe(), 'endpoint': self.endpoint(), 'link': self.link,
472
+ 'since': self.link_since, 'protocol': self.link_protocol, 'last_error': self.link_error,
473
+ 'calls': self.calls, 'failures': self.call_failures}
474
+
475
+ def sections(self) -> Mapping[str, Any]:
476
+ return {'bridge': self.bridge_state()}
477
+
478
+ def trouble(self) -> list[str]:
479
+ if not self.wanted():
480
+ return []
481
+ if self.link == INCOMPATIBLE:
482
+ return ['the %s sidecar at %s %s; this bridge speaks %s - upgrade one of them'
483
+ % (self.sidecar.name, self.endpoint(), self.link_error, self.sidecar.protocol)]
484
+ if self.link == DOWN:
485
+ return ['the %s sidecar is not reachable at %s; %s'
486
+ % (self.sidecar.name, self.endpoint(), self.CONSEQUENCE)]
487
+ return []
488
+
489
+
490
+ def bridges(compartments: Any) -> list[Bridge]:
491
+ """The bridges among a node's compartments: every sidecar it reaches."""
492
+ return [c for c in compartments if isinstance(c, Bridge)]
493
+
494
+
495
+ __all__ = [
496
+ 'Bridge', 'DOWN', 'INCOMPATIBLE', 'LINKS', 'LinkRecord', 'MAX_LINE', 'ProbeRecord', 'Sidecar', 'SidecarError',
497
+ 'SidecarIncompatible', 'SidecarRecord', 'TRANSPORTS', 'UNKNOWN', 'UP', 'bridge_channels',
498
+ 'bridge_commands', 'bridge_metrics', 'bridges', 'jsonline_call',
499
+ ]