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,20 @@
1
+ """Types and records for the Mediation compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional, TypedDict
5
+
6
+
7
+ class DeliveredMessage(TypedDict):
8
+ """Message unwrapped and delivered via mediation pickup."""
9
+ id: str
10
+ recipient_key: str
11
+ sender: str
12
+ attachments: list[Any]
13
+ enqueued_at: int
14
+
15
+
16
+ class MediationGrantInfo(TypedDict):
17
+ """Information regarding a granted mediation contract."""
18
+ routing_keys: list[str]
19
+ endpoint: Optional[str]
20
+ granted_at: int
@@ -0,0 +1,7 @@
1
+ """Direct libp2p P2P transport, reached through a sidecar, as a compartment.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+ """
5
+ from .compartment import P2PTransport
6
+
7
+ __all__ = ['P2PTransport']
@@ -0,0 +1,349 @@
1
+ """Direct libp2p P2P transport, reached through a sidecar, as a compartment.
2
+
3
+ Protocol: `/fleetmesh/didcomm/1.0.0` as declared in `schema/messages.json`.
4
+ One stream carries one JWE; the other side answers one ack line once it has
5
+ queued the message for its node. The libp2p process is `sidecar/fleetmesh_p2p.py`,
6
+ talked to over `p2p.sock` in the node's directory, and it holds the Ed25519
7
+ transport key: its PeerId *is* the `key transport` Multikey the descriptor
8
+ publishes, which is how a dial is bound to a descriptor and not to whatever
9
+ multiaddr the descriptor happened to carry (see `target_of`).
10
+
11
+ What this compartment does with a stream it drained is what the FIPS path
12
+ does with a datagram: hands the bytes to the router's one envelope-opening
13
+ rule (`didcomm_inbound.open_inbound`). The sidecar's word on who wrote a
14
+ stream is a hint the router checks against the sender's descriptor, never a
15
+ proof of who sent the message; that proof is the ECDH-1PU agreement or nothing.
16
+
17
+ Like `node/announce.py`, if the sidecar is absent the node operates over the
18
+ relays, which is the rung the specification says always works.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import json
24
+ import os
25
+ import tempfile
26
+ from typing import Any, Callable, Mapping, Optional
27
+
28
+
29
+ from node.compartment import stores as st
30
+ from node.compartment import Command, Need
31
+ from node.config import P2PConfig
32
+ from node.config.parameter import Parameter
33
+ from node.compartment.lifecycle import Lifecycle
34
+ from node.compartment.sidecar import Bridge, Sidecar, SidecarError, jsonline_call
35
+ from fleetmesh_core.crypto import keys
36
+ from fleetmesh_core.identity.did import b58encode as _b58 # base58btc, written once (audit D9)
37
+
38
+ SOCK: str = 'p2p.sock'
39
+ PROTOCOL: str = 'fleetmesh-p2p/1'
40
+ STREAM_PROTOCOL: str = '/fleetmesh/didcomm/1.0.0'
41
+ SCHEME: str = 'libp2p'
42
+ MAX_LINE: int = 1 << 20
43
+ DRAIN_MAX: int = 64
44
+
45
+
46
+
47
+ def socket_path(directory: str) -> str:
48
+ """Derive the unix socket path for the p2p sidecar."""
49
+ real = os.path.realpath(directory)
50
+ p = os.path.join(real, SOCK)
51
+ if len(p.encode('utf-8')) < 100:
52
+ return p
53
+ return os.path.join(
54
+ tempfile.gettempdir(),
55
+ 'fleetmesh-p2p-%s.sock' % hashlib.sha256(real.encode('utf-8')).hexdigest()[:16]
56
+ )
57
+
58
+
59
+ def peer_id_of(raw_ed25519: bytes) -> str:
60
+ """The libp2p PeerId of an Ed25519 public key: the key itself, inlined.
61
+
62
+ A 32-byte key fits under the identity multihash, so the id is the
63
+ protobuf `PublicKey{Type: Ed25519, Data: key}` wrapped as
64
+ `multihash(0x00, len, pb)` and base58btc-encoded - the `12D3KooW...`
65
+ form. No hashing, so a descriptor's `key transport` Multikey and a
66
+ stream's remote PeerId are the same 32 bytes spelled two ways.
67
+ """
68
+ if len(raw_ed25519) != 32:
69
+ raise ValueError('an Ed25519 public key is 32 bytes')
70
+ pb = b'\x08\x01\x12\x20' + raw_ed25519
71
+ return _b58(b'\x00' + bytes([len(pb)]) + pb)
72
+
73
+
74
+ def transport_key_of(desc: Optional[Mapping[str, Any]]) -> Optional[bytes]:
75
+ """The raw Ed25519 transport key a descriptor publishes, or None."""
76
+ if desc is None:
77
+ return None
78
+ for t in desc.get('tags', []):
79
+ if len(t) > 2 and t[0] == 'key' and t[1] == 'transport':
80
+ try:
81
+ kind, raw = keys.parse_multikey(t[2])
82
+ except ValueError:
83
+ return None
84
+ return raw if kind == 'transport' else None
85
+ return None
86
+
87
+
88
+ def target_of(desc: Optional[Mapping[str, Any]]) -> tuple[Optional[str], Optional[str]]:
89
+ """The multiaddr to dial for a descriptor, or why there is none.
90
+
91
+ Returns (target, None) or (None, reason). The target always ends in
92
+ `/p2p/<PeerId>` derived from the descriptor's own `key transport`: the
93
+ libp2p handshake then refuses any host at that address that does not
94
+ hold the key the descriptor signed over. A descriptor whose endpoint
95
+ already names a PeerId must name the same one; a different one is a
96
+ descriptor that contradicts itself, and is not dialled.
97
+ """
98
+ if desc is None:
99
+ return None, 'no descriptor'
100
+ addr = None
101
+ for t in desc.get('tags', []):
102
+ if len(t) > 2 and t[0] == 'endpoint' and t[1] == SCHEME:
103
+ addr = t[2]
104
+ break
105
+ if not addr:
106
+ return None, 'no libp2p endpoint'
107
+ raw = transport_key_of(desc)
108
+ if raw is None:
109
+ return None, 'no transport key'
110
+ pid = peer_id_of(raw)
111
+ if '/p2p/' in addr:
112
+ head, _, named = addr.rpartition('/p2p/')
113
+ if named != pid:
114
+ return None, 'endpoint names a PeerId that is not the transport key'
115
+ addr = head
116
+ return '%s/p2p/%s' % (addr.rstrip('/'), pid), None
117
+
118
+
119
+ class P2PError(SidecarError):
120
+ """Raised when communication with the p2p sidecar fails."""
121
+
122
+
123
+ def ask_sidecar(directory: str, cmd: str, timeout: float = 5.0, **args: Any) -> Any:
124
+ """Send one request to the p2p sidecar over its unix domain socket."""
125
+ path = socket_path(directory)
126
+ try:
127
+ result, _ = jsonline_call(path, cmd, timeout=timeout, **args)
128
+ return result
129
+ except SidecarError as e:
130
+ raise P2PError(str(e)) from e
131
+
132
+
133
+ from .types import P2PState, SendResult
134
+
135
+
136
+ class P2PTransport(Bridge):
137
+
138
+ optional: bool = True
139
+ enabled_by: Optional[str] = 'p2p'
140
+ """The direct libp2p transport compartment."""
141
+
142
+ name: str = 'p2p'
143
+ reads: tuple[str, ...] = ('p2p', 'endpoints')
144
+ config_cls = P2PConfig
145
+
146
+ SIDECAR = Sidecar(
147
+ name='p2p',
148
+ protocol=PROTOCOL,
149
+ upstream='py-libp2p stream transport on %s' % STREAM_PROTOCOL,
150
+ standalone='fleetmesh-p2p NODE_DIR',
151
+ probe='status',
152
+ )
153
+ CONSEQUENCE: str = 'direct libp2p connections are unavailable; falling back to relays'
154
+ LIFECYCLE = Lifecycle(restart=True)
155
+
156
+ # Declared by the bridge from SIDECAR (p2p_sidecar_reachable / _unreachable / _incompatible).
157
+ SIDECAR_REACHABLE: str = 'p2p.sidecar.reachable'
158
+ SIDECAR_UNREACHABLE: str = 'p2p.sidecar.unreachable'
159
+ SIDECAR_INCOMPATIBLE: str = 'p2p.sidecar.incompatible'
160
+
161
+ NEEDS = (
162
+ Need('directory', 'dir'),
163
+ Need('peers', 'peers'),
164
+ Need('transport', 'transport'),
165
+ Need('ks', 'ks'),
166
+ )
167
+ STORES = (
168
+ st.Store(key='egress_queue', description='outbound frames queued for libp2p sidecar',
169
+ kind=st.RING, shape=bytes, bound=1024),
170
+ )
171
+ COMMANDS = (
172
+ Command(
173
+ key='status',
174
+ name='P2P Status',
175
+ description='display libp2p sidecar connectivity and stream counters',
176
+ tags=('p2p', 'status'),
177
+ permission='p2p/status',
178
+ run='state',
179
+ reply='dict',
180
+ ),
181
+ Command(
182
+ key='drain',
183
+ name='Drain P2P Streams',
184
+ description='drain inbound direct P2P messages from the sidecar',
185
+ tags=('p2p', 'drain'),
186
+ permission='p2p/drain',
187
+ run='drain_cmd',
188
+ reply='dict',
189
+ ),
190
+ Command(
191
+ key='send',
192
+ name='Send P2P Message',
193
+ description='dispatch one already-sealed JWE over a libp2p stream',
194
+ tags=('p2p', 'send'),
195
+ parameters=(
196
+ Parameter(key='target', name='target', description='target multiaddr', type=str, default='', required=True),
197
+ Parameter(key='payload', name='payload', description='sealed JWE payload', type=dict, default=dict, required=True),
198
+ ),
199
+ permission='p2p/send',
200
+ run='send_cmd',
201
+ reply='dict',
202
+ ),
203
+ )
204
+
205
+ def __init__(self, directory: str, peers: Any = None, transport: Any = None, clock: Optional[Callable[[], int]] = None, node: Any = None,
206
+ ks: Any = None) -> None:
207
+ super().__init__(node, clock)
208
+ # The keystore whose transport key the sidecar's peer id is checked against.
209
+ self.ks: Any = ks if ks is not None else getattr(node, 'ks', None)
210
+ self.dir: str = directory
211
+ self.peers: Any = peers
212
+ self.transport: Any = transport
213
+ self.connected: bool = False
214
+ self.sent_count: int = 0
215
+ self.recv_count: int = 0
216
+ self.refused_count: int = 0
217
+ self.last_drain_at: Optional[int] = None
218
+ self.peer_id: Optional[str] = None
219
+ self.addrs: list[str] = [] # not a store: the local node's p2p multiaddrs from sidecar
220
+ self._said_missing: bool = False
221
+ if transport is not None and hasattr(transport, 'p2p'):
222
+ transport.p2p = self
223
+
224
+ # ---- the bridge -----------------------------------------------------------
225
+ def endpoint(self) -> str:
226
+ return socket_path(self.dir)
227
+
228
+ def wanted(self) -> bool:
229
+ return bool(self.config('p2p', False))
230
+
231
+ def forget(self) -> None:
232
+ self.connected = False
233
+ self.peer_id = None
234
+ self.addrs = [] # not a store: cleared on forget
235
+
236
+ def state(self) -> P2PState:
237
+ st: P2PState = {
238
+ 'enabled': bool(self.config('p2p', True)),
239
+ 'socket': self.endpoint(),
240
+ 'connected': self.connected,
241
+ 'peer_id': self.peer_id,
242
+ 'addrs': self.addrs,
243
+ 'sent_count': self.sent_count,
244
+ 'recv_count': self.recv_count,
245
+ 'refused_count': self.refused_count,
246
+ 'last_drain_at': self.last_drain_at,
247
+ }
248
+ if self.connected:
249
+ try:
250
+ st['sidecar'] = self.call('status', timeout=1.0)
251
+ except Exception:
252
+ pass
253
+ return st
254
+
255
+ def sections(self) -> Mapping[str, Any]:
256
+ return {
257
+ 'bridge': self.bridge_state(),
258
+ 'p2p': self.state(),
259
+ }
260
+
261
+ def trouble(self) -> list[str]:
262
+ out = list(super().trouble())
263
+ if self.connected and self.peer_id and self.ks is not None:
264
+ mine = peer_id_of(keys.raw_public(self.ks.transport_pub))
265
+ if mine != self.peer_id:
266
+ out.append('p2p sidecar holds a different transport key (%s) than this node publishes (%s)'
267
+ % (self.peer_id[:16], mine[:16]))
268
+ return out
269
+
270
+ def tick(self, now: int) -> list[tuple[Any, ...]]:
271
+ did: list[tuple[Any, ...]] = []
272
+ if not self.config('p2p', True):
273
+ return did
274
+
275
+ was_connected = self.connected
276
+ # Check sidecar liveness
277
+ try:
278
+ res = self.call('status', timeout=1.0)
279
+ self.peer_id = res.get('peer_id')
280
+ self.addrs = list(res.get('addrs') or []) # not a store: the local node's p2p multiaddrs from sidecar
281
+ if not was_connected:
282
+ self.connected = True
283
+ self._said_missing = False
284
+ did.append(('p2p sidecar connected', (self.peer_id or 'unknown')[:16]))
285
+ except (P2PError, SidecarError) as e:
286
+ if was_connected:
287
+ self.connected = False
288
+ did.append(('p2p sidecar dropped', str(e)))
289
+ elif not self._said_missing and self.config('p2p', False):
290
+ self._said_missing = True
291
+ return did
292
+
293
+ # Drain inbound streams and hand each to the router's envelope rule.
294
+ try:
295
+ res = self.call('drain', timeout=2.0, max_items=DRAIN_MAX)
296
+ except (P2PError, SidecarError) as e:
297
+ self.refusals.refuse('p2p', 'drain failed', detail={'error': str(e)})
298
+ return did
299
+ items = res.get('items') or [] if isinstance(res, dict) else []
300
+ for reason, n in ((res.get('refused') or {}) if isinstance(res, dict) else {}).items():
301
+ self.refusals.refuse('p2p', 'sidecar refused a stream: %s' % reason, detail={'count': n})
302
+ if items:
303
+ self.recv_count += len(items)
304
+ self.last_drain_at = now
305
+ did.append(('p2p drained', len(items)))
306
+ for it in items:
307
+ why = self.deliver(it)
308
+ if why is not None:
309
+ self.refused_count += 1
310
+ self.refusals.refuse('p2p', why, detail={'from_peer': str(it.get('from_peer', ''))[:24]})
311
+ return did
312
+
313
+ def deliver(self, item: Mapping[str, Any]) -> Optional[str]:
314
+ """One drained stream to the router. Returns None, or why it was refused.
315
+
316
+ `injected` items are what the sidecar's test hook queued: no stream
317
+ was authenticated, so no origin key is asserted for them.
318
+ """
319
+ msg = item.get('message')
320
+ if not isinstance(msg, dict):
321
+ return 'stream carried no object'
322
+ origin = None if item.get('injected') else item.get('from_key')
323
+ if self.transport is None or not hasattr(self.transport, 'deliver_direct'):
324
+ return 'no router to deliver to'
325
+ wrap_id = 'libp2p-%s' % hashlib.sha256(
326
+ json.dumps(msg, sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()[:16]
327
+ result: Optional[str] = self.transport.deliver_direct(SCHEME, msg, wrap_id, origin=origin)
328
+ return result
329
+
330
+ def send_direct(self, target_multiaddr: str, payload: Mapping[str, Any]) -> tuple[bool, str]:
331
+ """Deliver one sealed JWE to `target_multiaddr`; True only after the peer acked."""
332
+ try:
333
+ res = ask_sidecar(self.dir, 'send', timeout=6.0, target=target_multiaddr, payload=payload)
334
+ except (P2PError, SidecarError) as e:
335
+ self.refusals.refuse('p2p', 'direct send failed', detail={'target': target_multiaddr[:80], 'error': str(e)})
336
+ return False, str(e)
337
+ if not isinstance(res, dict) or not res.get('accepted'):
338
+ return False, 'sidecar did not report an acknowledged delivery'
339
+ self.sent_count += 1
340
+ return True, 'delivered via libp2p stream to %s' % res.get('peer_id', target_multiaddr)[:24]
341
+
342
+ def drain_cmd(self) -> Any:
343
+ return self.call('drain')
344
+
345
+ def send_cmd(self, args: Mapping[str, Any]) -> SendResult:
346
+ ok, note = self.send_direct(str(args['target']), dict(args['payload']))
347
+ return {'accepted': ok, 'note': note}
348
+
349
+
@@ -0,0 +1,22 @@
1
+ """Types and records for the P2P transport compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional, TypedDict
5
+
6
+
7
+ class P2PState(TypedDict, total=False):
8
+ enabled: bool
9
+ socket: str
10
+ connected: bool
11
+ peer_id: Optional[str]
12
+ addrs: list[str]
13
+ sent_count: int
14
+ recv_count: int
15
+ refused_count: int
16
+ last_drain_at: Optional[int]
17
+ sidecar: Any
18
+
19
+
20
+ class SendResult(TypedDict):
21
+ accepted: bool
22
+ note: str
@@ -0,0 +1,277 @@
1
+ """Pluggable protocol suites and package registry for Fleetmesh node.
2
+
3
+ Manages dynamic discovery, configuration, lifecycle, and multiplexing
4
+ across all supported open protocol families:
5
+ - Nostr & Blossom (`fleetmesh-protocol-nostr`)
6
+ - AT Protocol (`fleetmesh-protocol-atproto`)
7
+ - ActivityPub & ActivityStreams 2.0 (`fleetmesh-protocol-activitypub`)
8
+ - Secure Scuttlebutt (`fleetmesh-protocol-ssb`)
9
+ - p2panda (`fleetmesh-protocol-p2panda`)
10
+ - Rhio (`fleetmesh-protocol-rhio`)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import importlib
15
+ import logging
16
+ from typing import Any, Callable, Mapping, Optional
17
+
18
+ from fleetmesh_core.wire.interfaces import (
19
+ ProtocolHealth,
20
+ ProtocolHostRouter,
21
+ ProtocolPackage,
22
+ )
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+ # Registry of built-in protocol packages
27
+ BUILTIN_PACKAGE_LOADERS: dict[str, tuple[str, str]] = {
28
+ 'nostr': ('node.protocols.nostr.package', 'NostrProtocolPackage'),
29
+ 'atproto': ('node.protocols.atproto.package', 'AtprotoProtocolPackage'),
30
+ 'activitypub': ('node.protocols.activitypub.package', 'ActivityPubProtocolPackage'),
31
+ 'ssb': ('node.protocols.ssb.package', 'SSBProtocolPackage'),
32
+ 'p2panda': ('node.protocols.p2panda.package', 'P2PandaProtocolPackage'),
33
+ 'rhio': ('node.protocols.rhio.package', 'RhioProtocolPackage'),
34
+ }
35
+
36
+ _CUSTOM_PACKAGES: dict[str, type[ProtocolPackage]] = {}
37
+
38
+
39
+ def register_protocol(name: str, package_cls: type[ProtocolPackage]) -> None:
40
+ """Register a custom or third-party protocol package class."""
41
+ clean_name = name.lower().strip()
42
+ _CUSTOM_PACKAGES[clean_name] = package_cls
43
+
44
+
45
+ def get_available_protocols() -> tuple[str, ...]:
46
+ """Return all currently registered protocol names."""
47
+ return tuple(sorted(set(BUILTIN_PACKAGE_LOADERS.keys()) | set(_CUSTOM_PACKAGES.keys())))
48
+
49
+
50
+ def load_protocol_package_class(name: str) -> type[ProtocolPackage]:
51
+ """Dynamically load and return the ProtocolPackage class for a given name."""
52
+ clean_name = name.lower().strip()
53
+ if clean_name in _CUSTOM_PACKAGES:
54
+ return _CUSTOM_PACKAGES[clean_name]
55
+
56
+ if clean_name in BUILTIN_PACKAGE_LOADERS:
57
+ mod_name, cls_name = BUILTIN_PACKAGE_LOADERS[clean_name]
58
+ try:
59
+ mod = importlib.import_module(mod_name)
60
+ cls = getattr(mod, cls_name)
61
+ return cls
62
+ except Exception as e:
63
+ raise ImportError(f"Failed to load protocol package '{clean_name}' from {mod_name}.{cls_name}: {e}") from e
64
+
65
+ raise KeyError(f"Unknown protocol package '{name}'. Available: {get_available_protocols()}")
66
+
67
+
68
+ def create_protocol_package(name: str, **kwargs) -> ProtocolPackage:
69
+ """Instantiate a protocol package by name with provided arguments."""
70
+ cls = load_protocol_package_class(name)
71
+ return cls(**kwargs)
72
+
73
+
74
+ class ProtocolManager:
75
+ """Orchestrates configuration, lifecycle, and routing across all protocol packages."""
76
+
77
+ def __init__(
78
+ self,
79
+ node_dir: Optional[str] = None,
80
+ keystore: Optional[Any] = None,
81
+ clock: Optional[Callable[[], int]] = None,
82
+ name: str = 'meshnode',
83
+ ):
84
+ self.node_dir = node_dir
85
+ self.keystore = keystore
86
+ self.clock = clock
87
+ self.node_name = name
88
+
89
+ self._packages: dict[str, ProtocolPackage] = {}
90
+ self._mounted_host: Optional[ProtocolHostRouter] = None
91
+
92
+ @property
93
+ def packages(self) -> Mapping[str, ProtocolPackage]:
94
+ return dict(self._packages)
95
+
96
+ def get_package(self, name: str) -> Optional[ProtocolPackage]:
97
+ return self._packages.get(name.lower().strip())
98
+
99
+ def load_suites_config(
100
+ self,
101
+ suites_config: Any,
102
+ backend_store: Optional[Any] = None,
103
+ blob_store: Optional[Any] = None,
104
+ grants: Optional[Any] = None,
105
+ node_instance: Optional[Any] = None,
106
+ private_key: Optional[Any] = None,
107
+ strain_ledger: Optional[Any] = None,
108
+ ) -> dict[str, ProtocolPackage]:
109
+ """Initialize protocol packages based on ProtocolSuitesConfig or mapping."""
110
+ suites_dict: Mapping[str, Any] = {}
111
+ if hasattr(suites_config, 'to_dict'):
112
+ suites_dict = suites_config.to_dict()
113
+ elif hasattr(suites_config, 'as_dict'):
114
+ suites_dict = suites_config.as_dict()
115
+ elif hasattr(suites_config, '__dict__'):
116
+ suites_dict = {
117
+ k: getattr(suites_config, k).to_dict() if hasattr(getattr(suites_config, k), 'to_dict') else (getattr(suites_config, k).as_dict() if hasattr(getattr(suites_config, k), 'as_dict') else getattr(suites_config, k))
118
+ for k in ('nostr', 'atproto', 'activitypub', 'ssb', 'p2panda', 'rhio')
119
+ if getattr(suites_config, k, None) is not None
120
+ }
121
+ elif isinstance(suites_config, Mapping):
122
+ suites_dict = dict(suites_config)
123
+
124
+ if self._packages:
125
+ self.stop_all()
126
+ self._packages.clear()
127
+
128
+ # 1. Nostr
129
+ cfg_nostr = suites_dict.get('nostr')
130
+ if cfg_nostr is not None and (isinstance(cfg_nostr, Mapping) and cfg_nostr.get('enabled', True)):
131
+ from node.protocols.nostr.package import NostrProtocolPackage
132
+ pkg_nostr = NostrProtocolPackage(
133
+ backend=backend_store,
134
+ blob_store=blob_store,
135
+ grants=grants,
136
+ node=node_instance,
137
+ clock=self.clock,
138
+ name=self.node_name,
139
+ pubkey=getattr(self.keystore, 'node_pk', None) if self.keystore else None,
140
+ )
141
+ pkg_nostr.configure(cfg_nostr if isinstance(cfg_nostr, Mapping) else {})
142
+ self._packages['nostr'] = pkg_nostr
143
+
144
+ # 2. ATProto
145
+ cfg_atp = suites_dict.get('atproto')
146
+ if cfg_atp is not None and (isinstance(cfg_atp, Mapping) and cfg_atp.get('enabled', True)):
147
+ from node.protocols.atproto.package import AtprotoProtocolPackage
148
+ pkg_atp = AtprotoProtocolPackage(
149
+ blob_store=blob_store,
150
+ keystore=self.keystore,
151
+ )
152
+ pkg_atp.configure(cfg_atp if isinstance(cfg_atp, Mapping) else {})
153
+ self._packages['atproto'] = pkg_atp
154
+
155
+ # 3. ActivityPub
156
+ cfg_ap = suites_dict.get('activitypub')
157
+ if cfg_ap is not None and (isinstance(cfg_ap, Mapping) and cfg_ap.get('enabled', True)):
158
+ from node.protocols.activitypub.package import ActivityPubProtocolPackage
159
+ domain = cfg_ap.get('domain', 'node.fleetmesh.local') if isinstance(cfg_ap, Mapping) else 'node.fleetmesh.local'
160
+ pkg_ap = ActivityPubProtocolPackage(
161
+ domain=domain,
162
+ username="node",
163
+ private_key=private_key or getattr(self.keystore, 'ed25519_sk', None),
164
+ blob_store=blob_store,
165
+ strain_ledger=strain_ledger,
166
+ keystore=self.keystore,
167
+ )
168
+ pkg_ap.configure(cfg_ap if isinstance(cfg_ap, Mapping) else {})
169
+ self._packages['activitypub'] = pkg_ap
170
+
171
+ # 4. SSB
172
+ cfg_ssb = suites_dict.get('ssb')
173
+ if cfg_ssb is not None and (isinstance(cfg_ssb, Mapping) and cfg_ssb.get('enabled', True)):
174
+ from node.protocols.ssb.package import SSBProtocolPackage
175
+ pkg_ssb = SSBProtocolPackage(
176
+ node_dir=self.node_dir or "/tmp/fleetmesh-ssb",
177
+ private_key=private_key or getattr(self.keystore, 'ed25519_sk', None),
178
+ )
179
+ pkg_ssb.configure(cfg_ssb if isinstance(cfg_ssb, Mapping) else {})
180
+ self._packages['ssb'] = pkg_ssb
181
+
182
+ # 5. P2Panda
183
+ cfg_p2p = suites_dict.get('p2panda')
184
+ if cfg_p2p is not None and (isinstance(cfg_p2p, Mapping) and cfg_p2p.get('enabled', False)):
185
+ if 'p2panda' in _CUSTOM_PACKAGES or 'p2panda' in BUILTIN_PACKAGE_LOADERS:
186
+ try:
187
+ pkg_p2p = create_protocol_package(
188
+ 'p2panda',
189
+ node_dir=self.node_dir,
190
+ keystore=self.keystore,
191
+ blob_store=blob_store,
192
+ )
193
+ pkg_p2p.configure(cfg_p2p if isinstance(cfg_p2p, Mapping) else {})
194
+ self._packages['p2panda'] = pkg_p2p
195
+ except Exception as e:
196
+ log.warning("Could not instantiate p2panda protocol package: %s", e)
197
+
198
+ # 6. Rhio
199
+ cfg_rhio = suites_dict.get('rhio')
200
+ if cfg_rhio is not None and (isinstance(cfg_rhio, Mapping) and cfg_rhio.get('enabled', False)):
201
+ if 'rhio' in _CUSTOM_PACKAGES or 'rhio' in BUILTIN_PACKAGE_LOADERS:
202
+ try:
203
+ pkg_rhio = create_protocol_package(
204
+ 'rhio',
205
+ node_dir=self.node_dir,
206
+ keystore=self.keystore,
207
+ blob_store=blob_store,
208
+ )
209
+ pkg_rhio.configure(cfg_rhio if isinstance(cfg_rhio, Mapping) else {})
210
+ self._packages['rhio'] = pkg_rhio
211
+ except Exception as e:
212
+ log.warning("Could not instantiate rhio protocol package: %s", e)
213
+
214
+ return dict(self._packages)
215
+
216
+ def mount_ingress(self, host: ProtocolHostRouter) -> None:
217
+ """Mount all active protocol packages onto the shared host multiplexer."""
218
+ self._mounted_host = host
219
+ for name, pkg in self._packages.items():
220
+ try:
221
+ pkg.mount_ingress(host)
222
+ log.info("Mounted ingress for protocol '%s'", name)
223
+ except Exception as e:
224
+ log.exception("Failed to mount ingress for protocol '%s': %s", name, e)
225
+
226
+ def mount_transport_rungs(self, router: Any) -> list[Any]:
227
+ """Mount outbound rungs into TransportRouter ladder."""
228
+ mounted = []
229
+ for name, pkg in self._packages.items():
230
+ try:
231
+ rung = pkg.transport_rung(router)
232
+ if rung is not None:
233
+ if hasattr(router, 'register_rung'):
234
+ router.register_rung(rung)
235
+ elif hasattr(router, 'add_rung'):
236
+ router.add_rung(rung)
237
+ mounted.append(rung)
238
+ except Exception as e:
239
+ log.exception("Failed to build transport rung for protocol '%s': %s", name, e)
240
+ return mounted
241
+
242
+ def start_all(self) -> None:
243
+ """Start all configured protocol packages."""
244
+ for name, pkg in self._packages.items():
245
+ try:
246
+ pkg.start()
247
+ except Exception as e:
248
+ log.exception("Error starting protocol package '%s': %s", name, e)
249
+
250
+ def stop_all(self) -> None:
251
+ """Stop all configured protocol packages and unmount ingress."""
252
+ for name, pkg in self._packages.items():
253
+ try:
254
+ pkg.stop()
255
+ except Exception as e:
256
+ log.exception("Error stopping protocol package '%s': %s", name, e)
257
+ if self._mounted_host is not None:
258
+ for name in self._packages:
259
+ try:
260
+ self._mounted_host.unmount_all(name)
261
+ except Exception:
262
+ pass
263
+ self._mounted_host = None
264
+
265
+ def health(self) -> dict[str, ProtocolHealth]:
266
+ """Collect health telemetry across all loaded protocol packages."""
267
+ return {name: pkg.health() for name, pkg in self._packages.items()}
268
+
269
+ def descriptor_capabilities(self) -> tuple[tuple[str, str], ...]:
270
+ """Aggregate descriptor capabilities from all active packages."""
271
+ caps: list[tuple[str, str]] = []
272
+ for pkg in self._packages.values():
273
+ try:
274
+ caps.extend(pkg.descriptor_capabilities())
275
+ except Exception:
276
+ pass
277
+ return tuple(caps)