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,135 @@
1
+ """`plugin-check REF`: the conformance check a plugin author runs before an
2
+ operator ever lists the plugin in a config. No node, no keys: the module is
3
+ imported and the class is held to exactly the rules `node/app/core.py`
4
+ applies at startup (`load_plugins`, the PIURI origin rule, `plugin_sidecars`)
5
+ and `node/commands` applies to its verbs - the same functions, not a copy of
6
+ them, so the check cannot drift from the node. Exit 0 when every rule holds,
7
+ 1 when one does not (each failure on its own line), 2 when REF cannot be
8
+ imported at all."""
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ from typing import Optional, TypedDict
14
+
15
+ from .. import compartment as compartment_mod
16
+ from ..planes.services.sidecars.manifest import SidecarDict
17
+ from fleetmesh_core.identity import origin as origin_mod
18
+ from .base import fail
19
+
20
+
21
+ class Declares(TypedDict, total=False):
22
+ builtin: bool
23
+ after: str
24
+ piuris: list[str]
25
+ sidecars: list[SidecarDict]
26
+ cli: list[str]
27
+
28
+
29
+ class Report(TypedDict):
30
+ ref: str
31
+ name: Optional[str]
32
+ ok: bool
33
+ findings: list[str]
34
+ declares: Declares
35
+
36
+ NOT_CONFORMANT = 1
37
+ NOT_IMPORTABLE = 2
38
+
39
+
40
+ def check_plugin(ref: str) -> Report:
41
+ """Every rule, as {'ref', 'ok', 'name', 'findings': [..], 'declares': {...}}.
42
+ Raises ValueError when the ref does not import (nothing else to say)."""
43
+ from ..app import core as core_mod
44
+ from . import PROVIDERS, _import_ref
45
+ cls = _import_ref(ref)
46
+ findings: list[str] = []
47
+ declares: Declares = {}
48
+ # 1. the subsystem/1 declaration, held to the same rules as at startup.
49
+ # A built-in is checked as if it were a plugin spliced in where it ships:
50
+ # the manifest without it is the base, its predecessor the anchor. So
51
+ # "plugin" means who shipped the class, not a different set of rules.
52
+ builtin = cls in core_mod.MANIFEST
53
+ declares['builtin'] = builtin
54
+ base = tuple(c for c in core_mod.MANIFEST if c is not cls)
55
+ after = None
56
+ if builtin:
57
+ i = core_mod.MANIFEST.index(cls)
58
+ after = str(core_mod.MANIFEST[i - 1].name) if i else None
59
+ try:
60
+ from ..config.compartments import PluginSpec
61
+ if builtin and after is None:
62
+ manifest = (cls,) + base
63
+ else:
64
+ manifest = core_mod.load_plugins([PluginSpec(ref=ref, after=after)], base)
65
+ declares['after'] = str(manifest[manifest.index(cls) - 1].name) if manifest.index(cls) else '-'
66
+ except ValueError as e:
67
+ findings.append(str(e))
68
+ api = getattr(cls, 'api', None)
69
+ if api != compartment_mod.Compartment.API:
70
+ findings.append('api is %r; this node speaks %s' % (api, compartment_mod.Compartment.API))
71
+ if not callable(getattr(cls, 'from_context', None)):
72
+ findings.append('no from_context(ctx) constructor')
73
+ for attr in ('requires', 'uses'):
74
+ for dep in getattr(cls, attr, ()) or ():
75
+ if dep not in {c.name for c in core_mod.MANIFEST}:
76
+ findings.append('%s names %r, which is not a compartment' % (attr, dep))
77
+ # 2. dialects: its own origin
78
+ piuris = list(getattr(cls, 'piuris', ()) or ())
79
+ declares['piuris'] = piuris
80
+ builtin = cls in core_mod.MANIFEST
81
+ for piuri in piuris:
82
+ # A built-in speaks fleetmesh.org's protocols by definition; the
83
+ # origin rule is for plugins, which speak their own.
84
+ if not builtin and origin_mod.reserved(piuri, core_mod.validate.FLEETMESH_ORIGIN):
85
+ findings.append('claims the reserved-origin protocol %s; plugins speak their own origin' % piuri)
86
+ if piuris and not callable(getattr(cls, 'handle', None)):
87
+ findings.append('declares piuris but no handle(inbound)')
88
+ # 3. sidecars: manifests that lint, under their own origin
89
+ try:
90
+ declares['sidecars'] = [s.to_dict() for s in core_mod.plugin_sidecars(cls)]
91
+ except ValueError as e:
92
+ findings.append(str(e))
93
+ # 4. CLI verbs: none that a core command owns
94
+ parser = argparse.ArgumentParser(prog='meshnode')
95
+ sub = parser.add_subparsers(dest='cmd')
96
+ for p in PROVIDERS:
97
+ p.register(sub)
98
+ verbs = [v for v, _ in (getattr(cls, 'cli', None) or ())]
99
+ declares['cli'] = verbs
100
+ for verb in verbs:
101
+ if verb in sub.choices:
102
+ findings.append('command %r already exists in meshnode' % verb)
103
+ return {'ref': ref, 'name': getattr(cls, 'name', None), 'ok': not findings,
104
+ 'findings': findings, 'declares': declares}
105
+
106
+
107
+ def cmd_plugin_check(a: argparse.Namespace) -> int:
108
+ try:
109
+ r = check_plugin(a.ref)
110
+ except ValueError as e:
111
+ fail(str(e), NOT_IMPORTABLE)
112
+ if a.json:
113
+ print(json.dumps(r, indent=2))
114
+ else:
115
+ d = r['declares']
116
+ print('%s (%s%s): %s' % (r['ref'], r['name'], ', built-in' if d.get('builtin') else '', 'conformant' if r['ok'] else 'NOT conformant'))
117
+ if 'after' in d:
118
+ print(' ticks after %s' % d['after'])
119
+ print(' dialects %s' % (', '.join(d.get('piuris') or []) or '-'))
120
+ print(' sidecars %s' % (', '.join('%s (%s)' % (s['name'], s['capability']) for s in d.get('sidecars') or []) or '-'))
121
+ print(' cli verbs %s' % (', '.join(d.get('cli') or []) or '-'))
122
+ for f in r['findings']:
123
+ print(' FAIL %s' % f)
124
+ return 0 if r['ok'] else NOT_CONFORMANT
125
+
126
+
127
+ class PluginCommandProvider:
128
+ """Registers plugin-check."""
129
+
130
+ @classmethod
131
+ def register(cls, sub: argparse._SubParsersAction) -> None:
132
+ p = sub.add_parser('plugin-check', help='hold a plugin class to the rules the node applies at startup, with no node (for plugin authors)')
133
+ p.add_argument('ref', help='package.module:Class, importable from here')
134
+ p.add_argument('--json', action='store_true', help='print the result as JSON')
135
+ p.set_defaults(fn=cmd_plugin_check)
@@ -0,0 +1,120 @@
1
+ """Sidecars: what the node fronts (GRASP git, ncontainer OCI, magpie files,
2
+ or anything an operator brings a manifest for), whether each has passed its
3
+ self-tests, and a way to run those tests now. The daemon owns the answer -
4
+ it is the one that declared or withheld the capability on the descriptor.
5
+
6
+ `sidecar-manifest` is the offline half: write a starting manifest, lint one,
7
+ or run its checks against an endpoint with no node at all - what a developer
8
+ does before an operator ever configures it."""
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+
14
+ from ..planes.services.sidecars import manifest as manifest_mod
15
+ from .base import add_command, fail
16
+
17
+ TEMPLATE = {
18
+ 'sidecar': manifest_mod.VERSION,
19
+ 'name': 'NAME',
20
+ 'capability': 'https://YOUR-ORIGIN/NAME/1.0',
21
+ 'endpoint_tag': 'NAME-endpoint',
22
+ 'checks': [
23
+ {'name': 'http', 'method': 'HEAD', 'path': '/'},
24
+ {'name': 'PROTOCOL-ROUTE', 'method': 'GET', 'path': '/THE-ROUTE-THE-SPEC-MANDATES',
25
+ 'expect': [200, 404], 'content_type': 'application/REPLACE-OR-DELETE'},
26
+ ],
27
+ 'docs': 'https://YOUR-ORIGIN/docs/NAME',
28
+ # How a deployer runs it (ops/roles/meshnode reads this; the node does
29
+ # not). Delete the block if the operator always runs it by hand.
30
+ 'run': {'image': 'ghcr.io/YOUR-ORG/NAME:TAG', 'port': 7864,
31
+ 'env': {'NAME_LISTEN': '0.0.0.0:7864'}},
32
+ }
33
+
34
+
35
+ def _show(r: dict) -> None:
36
+ rows = {k: v for k, v in r.items() if isinstance(v, dict)}
37
+ if not any(v.get('enabled') for v in rows.values()):
38
+ print('No sidecars configured.')
39
+ return
40
+ for name, v in rows.items():
41
+ if not v.get('enabled'):
42
+ print('%-11s off' % name)
43
+ continue
44
+ state = 'VERIFIED' if v.get('verified') else 'not verified'
45
+ print('%-11s %-13s %s' % (name, state, v.get('endpoint', '')))
46
+ for c in v.get('checks') or []:
47
+ why = (' - ' + c['why']) if c.get('why') and not c['ok'] else ''
48
+ print(' %-5s %-34s %s%s' % ('ok' if c['ok'] else 'FAIL', c['name'],
49
+ c.get('status') if c.get('status') is not None else 'no answer', why))
50
+ if r.get('redeclared'):
51
+ print('descriptor redeclared')
52
+
53
+
54
+ def _get_sidecar_command(key: str):
55
+ from ..planes.services.sidecars.compartment import Sidecars
56
+ for c in Sidecars.COMMANDS:
57
+ if c.key == key:
58
+ return c
59
+ raise KeyError(f'Sidecar command {key!r} not found')
60
+
61
+
62
+ from .adapter import CommandDispatcher
63
+
64
+ SIDECARS_DISPATCHER = CommandDispatcher('sidecars', default_action='status')
65
+ SIDECARS_DISPATCHER.add_command('status', _get_sidecar_command('sidecars'), human=_show, timeout=10.0)
66
+ SIDECARS_DISPATCHER.add_command('test', _get_sidecar_command('sidecar_test'), human=_show, timeout=60.0)
67
+
68
+
69
+ def cmd_sidecars(a: argparse.Namespace) -> int:
70
+ return SIDECARS_DISPATCHER.dispatch(a)
71
+
72
+
73
+ def cmd_manifest(a: argparse.Namespace) -> int:
74
+ if a.init:
75
+ doc = json.loads(json.dumps(TEMPLATE).replace('NAME', a.init))
76
+ with open(a.file, 'x', encoding='utf-8') as f:
77
+ json.dump(doc, f, indent=2); f.write('\n')
78
+ print('wrote %s - replace every CAPITALISED placeholder, then: meshnode sidecar-manifest %s' % (a.file, a.file))
79
+ return 0
80
+ try:
81
+ # Linted as a third party's: the reserved origin is refused even for
82
+ # a file that happens to sit in the tree.
83
+ from ..planes.services.sidecars.compartment import RESERVED_ORIGIN
84
+ s = manifest_mod.load_file(a.file, origin=RESERVED_ORIGIN)
85
+ except manifest_mod.ManifestError as e:
86
+ fail('not a usable sidecar manifest: %s' % e)
87
+ if not a.endpoint:
88
+ print('%s: ok - %s declares %s with %d check(s): %s' % (
89
+ a.file, s.name, s.capability, len(s.checks), ', '.join(c.name for c in s.checks)))
90
+ return 0
91
+ from ..planes.services.sidecars import compartment as sidecars_mod
92
+ results = [sidecars_mod.run_check(sidecars_mod._http, a.endpoint, c) for c in s.checks]
93
+ ok = all(r.ok for r in results)
94
+ if a.json:
95
+ print(json.dumps({'name': s.name, 'endpoint': a.endpoint, 'verified': ok,
96
+ 'checks': [r._asdict() for r in results]}, indent=2))
97
+ else:
98
+ print('%s at %s: %s' % (s.name, a.endpoint, 'VERIFIED' if ok else 'not verified'))
99
+ for r in results:
100
+ why = (' - ' + r.why) if r.why and not r.ok else ''
101
+ print(' %-5s %-34s %s%s' % ('ok' if r.ok else 'FAIL', r.name,
102
+ r.status if r.status is not None else 'no answer', why))
103
+ return 0 if ok else 1
104
+
105
+
106
+ class SidecarsCommandProvider:
107
+ """Registers the sidecars commands."""
108
+
109
+ @classmethod
110
+ def register(cls, sub: argparse._SubParsersAction) -> None:
111
+ g = add_command(sub, 'sidecars', cmd_sidecars,
112
+ 'fronted sidecars (grasp, ncontainer, magpie, ...): self-test results, or run the tests now', js=True)
113
+ SIDECARS_DISPATCHER.register_arguments(g, action_choices=('status', 'test'))
114
+ m = sub.add_parser('sidecar-manifest',
115
+ help='write, lint or try a sidecar manifest with no node (for sidecar authors)')
116
+ m.add_argument('file', help='the manifest JSON')
117
+ m.add_argument('--init', metavar='NAME', help='write a starting manifest for NAME to FILE and exit')
118
+ m.add_argument('--endpoint', help='also run the checks against this URL, exit 1 if any fails')
119
+ m.add_argument('--json', action='store_true', help='print the check results as JSON')
120
+ m.set_defaults(fn=cmd_manifest)
node/commands/sync.py ADDED
@@ -0,0 +1,388 @@
1
+ """Sync scheduling, shard coverage claims, capacity metering, and strain telemetry."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+ import time
9
+
10
+ from node import app as app_mod
11
+ from node.app import socket as control
12
+ from .base import (
13
+ _ask,
14
+ _live,
15
+ _peer,
16
+ _status_file,
17
+ _table,
18
+ add_command,
19
+ emit,
20
+ fail,
21
+ load_json_arg,
22
+ status_health,
23
+ )
24
+
25
+
26
+ def _strain(st: dict) -> None:
27
+ if not st or not st.get('kinds'):
28
+ print('nothing survived yet. This node has not been stressed, which is')
29
+ print('not the same as being strong: a tree raised without wind grows')
30
+ print('tall and slender and cannot stand.')
31
+ return
32
+ print('survived:')
33
+ for k, n in sorted(st['kinds'].items(), key=lambda kv: -kv[1]):
34
+ print(' %-12s %d' % (k, n))
35
+ print('\n%d directions, %.2f weighted, reserves at %.2fx'
36
+ % (st.get('directions', 0), st.get('weighted_directions', 0),
37
+ st.get('reserve_multiple', 1)))
38
+ print('one peer moves this node by at most one direction, however hard it pushes.')
39
+ if st.get('flag_tree'):
40
+ print('\n!! flag tree: %.0f%% of it is one direction, %s'
41
+ % (100 * st.get('dominant_share', 0),
42
+ ' / '.join(x for x in st['flag_tree']['direction'] if x)[:40]))
43
+ print(' %s' % st['flag_tree']['note'])
44
+ print('\nnone of this is a finding, and none of it crosses the wire.')
45
+
46
+
47
+ def cmd_strain(a: argparse.Namespace) -> int:
48
+ """What this node has survived, and what it holds because of it."""
49
+ st = _live(a, 'strain')
50
+ if st is None:
51
+ st = status_health(a.dir).get('strain') or {}
52
+ return emit(a, st, _strain)
53
+
54
+
55
+ def _subsystems(subs: dict) -> None:
56
+ if not subs:
57
+ print('the node has not run yet')
58
+ return
59
+ for name in sorted(subs):
60
+ st = dict(subs[name])
61
+ ref = st.pop('refusals', {}) or {}
62
+ print('%s' % name)
63
+ for k in sorted(st):
64
+ v = st[k]
65
+ if isinstance(v, (dict, list)) and not v:
66
+ continue
67
+ print(' %-22s %s' % (k, json.dumps(v) if isinstance(v, (dict, list)) else v))
68
+ if ref.get('total'):
69
+ print(' %-22s %d' % ('refused', ref['total']))
70
+ for why, n in sorted((ref.get('by_reason') or {}).items(), key=lambda kv: -kv[1]):
71
+ print(' %6d %s' % (n, why))
72
+ print()
73
+
74
+
75
+ def cmd_subsystems(a: argparse.Namespace) -> int:
76
+ """Every compartment of this node, side by side."""
77
+ subs = _live(a, 'subsystems') or status_health(a.dir).get('subsystems') or {}
78
+ return emit(a, subs, _subsystems)
79
+
80
+
81
+ def _render_event(e: dict) -> str:
82
+ data = e.get('data') or {}
83
+ short = ' '.join('%s=%s' % (k, (v[:12] + '…') if isinstance(v, str) and len(v) > 24 else v) for k, v in data.items())
84
+ return '%s %-20s %s' % (time.strftime('%H:%M:%S', time.gmtime(e.get('ts') or 0)), e.get('topic'), short)
85
+
86
+
87
+ def _events(out: dict) -> None:
88
+ print('seq %s, ring holds %s (oldest %s), %s subscriber(s); topics: %s'
89
+ % (out['seq'], out['ring'], out['oldest'], out['subscribers'], ' '.join(out['topics'])))
90
+ for e in out['events']:
91
+ print(' %6s %s' % (e['seq'], _render_event(e)))
92
+
93
+
94
+ def cmd_events(a: argparse.Namespace) -> int:
95
+ """What the node's bus carried lately, from memory; needs the daemon."""
96
+ out = _live(a, 'events', {'since': a.since, 'topics': a.topics or None, 'last': a.last})
97
+ if out is None:
98
+ fail('events needs a running node; is one up in %s?' % a.dir)
99
+ return emit(a, out, _events)
100
+
101
+
102
+ def cmd_subscribe(a: argparse.Namespace) -> int:
103
+ """Follow the node's bus live: one line per event until interrupted."""
104
+ args = {}
105
+ if a.topics:
106
+ args['topics'] = a.topics
107
+ if a.since is not None:
108
+ args['since'] = a.since
109
+ try:
110
+ it = control.stream(a.dir, 'subscribe', timeout=None, **args)
111
+ ack = next(it)
112
+ if not a.json:
113
+ print('subscribed at seq %s%s' % (ack['seq'], (', replaying %d' % ack['replaying']) if ack.get('replaying') else ''),
114
+ file=sys.stderr)
115
+ for e in it:
116
+ print(json.dumps(e) if a.json else _render_event(e), flush=True)
117
+ except control.ControlError as e:
118
+ fail(str(e))
119
+ except KeyboardInterrupt:
120
+ return 0
121
+ return 0
122
+
123
+
124
+ def _refusals(rows: list) -> None:
125
+ if not rows:
126
+ print('this node has refused nothing it recorded.')
127
+ return
128
+ print('%-10s %-8s %-12s %-16s %s' % ('when', 'from', 'kind', 'subject', 'why'))
129
+ for r in rows:
130
+ print('%-10s %-8s %-12s %-16s %s' % (
131
+ time.strftime('%H:%M:%S', time.gmtime(r['at'])), r['subsystem'][:8],
132
+ r['kind'][:12], (r.get('subject') or '-')[:16], r['why']))
133
+ if r.get('detail'):
134
+ print('%-10s %s' % ('', json.dumps(r['detail'])[:100]))
135
+
136
+
137
+ def cmd_refusals(a: argparse.Namespace) -> int:
138
+ """Every no this node said, in one place."""
139
+ rows = _live(a, 'refusals', {'last': a.last})
140
+ if rows is None:
141
+ subs = status_health(a.dir).get('subsystems') or {}
142
+ rows = []
143
+ for name, sub in subs.items():
144
+ ref = (sub or {}).get('refusals') or {}
145
+ for r in ref.get('recent') or []:
146
+ row = dict(r)
147
+ row['subsystem'] = name
148
+ rows.append(row)
149
+ rows.sort(key=lambda r: r.get('at', 0))
150
+ if a.last:
151
+ rows = rows[-int(a.last):]
152
+ return emit(a, rows, _refusals)
153
+
154
+
155
+ def _capacity(c: dict) -> None:
156
+ if not c:
157
+ print('the node has not run yet, so it has carried nothing')
158
+ return
159
+ if not c.get('declared'):
160
+ print('no aggregate ceiling is declared.')
161
+ print('Every grant bounds one peer and nothing bounds this node, so it will')
162
+ print('accept load until it fails. What it has carried in one window:')
163
+ for k, v in sorted((c.get('peak') or c.get('measured') or {}).items()):
164
+ print(' %-12s %s' % (k, v))
165
+ print('\nPut a total in config.json under "capacity" to turn the shed ladder on.')
166
+ print('The number is yours: a class declares connectivity and not capability,')
167
+ print('so nothing in the protocol can guess what this machine will carry.')
168
+ return
169
+ print('pressure %.0f%%%s' % (100 * (c.get('pressure') or 0),
170
+ (' on %s' % c['binding']) if c.get('binding') else ''))
171
+ print('%-12s %10s %10s %10s' % ('counter', 'declared', 'this window', 'peak'))
172
+ for k in sorted(set(list(c.get('total') or {}) + list(c.get('peak') or {}))):
173
+ print('%-12s %10s %10s %10s' % (k, (c.get('total') or {}).get(k, '-'),
174
+ (c.get('measured') or {}).get(k, 0),
175
+ (c.get('peak') or {}).get(k, 0)))
176
+ if c.get('held'):
177
+ print('\nshedding, in ladder order:')
178
+ for action in c['held']:
179
+ since = (c.get('since') or {}).get(action)
180
+ print(' %-16s%s' % (action, (' since %s UTC' % time.strftime('%H:%M', time.gmtime(since)))
181
+ if since else ''))
182
+ print('none of this is a finding about any peer.')
183
+ else:
184
+ print('\nnothing is being shed.')
185
+
186
+
187
+ def cmd_capacity(a: argparse.Namespace) -> int:
188
+ """The node's own ceiling: what it promised its peers against what the
189
+ machine was told it can do."""
190
+ c = _live(a, 'capacity')
191
+ if c is None:
192
+ c = status_health(a.dir).get('capacity') or {}
193
+ return emit(a, c, _capacity)
194
+
195
+
196
+ def _scheduled(s: dict) -> None:
197
+ pulls = s.get('pulls') or []
198
+ if not pulls:
199
+ print('no pulls scheduled')
200
+ else:
201
+ print('%-16s %-16s %-12s %s' % ('peer', 'shard', 'every', 'last run'))
202
+ for p in pulls:
203
+ last = ('%ds ago' % (int(time.time()) - p['last'])) if p.get('last') else 'never'
204
+ print('%-16s %-16s %-12s %s' % (p['peer'], p['shard'], '%ds' % p.get('every', 900), last))
205
+ if s.get('throttled'):
206
+ print('\nthrottled (rate-limited by peer grant):')
207
+ for t in s['throttled']:
208
+ until = time.strftime('%H:%M:%S UTC', time.gmtime(t['until'])) if t.get('until') else 'unknown'
209
+ print(' %-16s %-16s %s (until %s)' % (t.get('peer', '')[:16], t.get('shard', ''), t.get('why', ''), until))
210
+ if s.get('held'):
211
+ print('\nheld (under local capacity ceiling):')
212
+ for h in s['held']:
213
+ print(' %s' % h)
214
+ if s.get('declined_until'):
215
+ print('\ndeclined (waiting out interval):')
216
+ for ps, until in s['declined_until'].items():
217
+ t_str = time.strftime('%H:%M:%S UTC', time.gmtime(until))
218
+ print(' %-32s until %s' % (ps, t_str))
219
+
220
+
221
+ def cmd_scheduled(a: argparse.Namespace) -> int:
222
+ """Every scheduled pull, when it last ran, and what is holding it back."""
223
+ s = _live(a, 'scheduled')
224
+ if s is None:
225
+ cfg = app_mod.load_config(a.dir)
226
+ st = _status_file(a.dir)
227
+ health = st.get('health') or {}
228
+ subs = health.get('subsystems') or {}
229
+ sched_sub = subs.get('scheduler') or {}
230
+ s = {
231
+ 'pulls': [
232
+ {
233
+ 'peer': (p.get('pubkey') or p.get('peer') or '')[:16],
234
+ 'shard': p.get('shard', ''),
235
+ 'every': p.get('pull_every', 900),
236
+ 'last': None,
237
+ }
238
+ for p in cfg.get('peers') or []
239
+ ],
240
+ 'throttled': sched_sub.get('throttled') or health.get('throttled') or [],
241
+ 'held': sched_sub.get('held') or [],
242
+ 'declined_until': sched_sub.get('declined_until') or {},
243
+ }
244
+ return emit(a, s, _scheduled)
245
+
246
+
247
+ def _coverage(cov: list) -> None:
248
+ if not cov:
249
+ print('no shards claimed')
250
+ return
251
+ _table(('shard', 'completeness', 'filter'),
252
+ [(c['shard'], c.get('completeness', 'best-effort'), json.dumps(c['filter'])) for c in cov])
253
+ print('\n`asserted` is a request: it is honoured only for a shard every event of which arrived at the relay face')
254
+
255
+
256
+ from ..config.parameter import Parameter
257
+ from .adapter import CommandDispatcher
258
+
259
+ PARAM_SHARD = Parameter(key='shard', name='shard', description='shard name', type=str, default='', positional=True, required=False, nargs='?')
260
+ PARAM_FILTER = Parameter(key='filter', name='filter', description='a nostr filter, JSON or @file (add)', type=str, default='')
261
+ PARAM_ASSERTED = Parameter(key='asserted', name='asserted', description='claim completeness (honoured only if every event arrived directly)', type=bool, default=False)
262
+
263
+
264
+ def _coverage_list(a: argparse.Namespace) -> int:
265
+ cfg = app_mod.load_config(a.dir)
266
+ cov = list(cfg.get('coverage') or [])
267
+ return emit(a, cov, _coverage)
268
+
269
+
270
+ def _coverage_remove(a: argparse.Namespace) -> int:
271
+ cfg = dict(app_mod.load_config(a.dir))
272
+ cov = list(cfg.get('coverage') or [])
273
+ if not a.shard:
274
+ fail('remove needs a SHARD')
275
+ kept = [c for c in cov if c['shard'] != a.shard]
276
+ if len(kept) == len(cov):
277
+ fail('%s is not claimed' % a.shard)
278
+ cfg['coverage'] = kept
279
+ app_mod.save_config(a.dir, cfg)
280
+ print('no longer claiming %s; the claim on the relays expires on its own' % a.shard)
281
+ return 0
282
+
283
+
284
+ def _coverage_add(a: argparse.Namespace) -> int:
285
+ cfg = dict(app_mod.load_config(a.dir))
286
+ cov = list(cfg.get('coverage') or [])
287
+ if not a.shard:
288
+ fail('add needs a SHARD')
289
+ if not a.filter:
290
+ fail('add needs --filter')
291
+ filt = load_json_arg(a.filter, '--filter')
292
+ entry = {'shard': a.shard, 'filter': filt, 'completeness': 'asserted' if a.asserted else 'best-effort'}
293
+ cfg['coverage'] = [c for c in cov if c['shard'] != a.shard] + [entry]
294
+ problems = app_mod.validate_config(cfg)
295
+ if problems:
296
+ fail('\n'.join(problems))
297
+ app_mod.save_config(a.dir, cfg)
298
+ print('claiming %s as %s on the next %s' % (a.shard, entry['completeness'], 'tick' if control.running(a.dir) else 'run'))
299
+ return 0
300
+
301
+
302
+ COVERAGE_DISPATCHER = CommandDispatcher('coverage', default_action='list')
303
+ COVERAGE_DISPATCHER.add_custom('list', _coverage_list)
304
+ COVERAGE_DISPATCHER.add_custom('add', _coverage_add, parameters=(PARAM_SHARD, PARAM_FILTER, PARAM_ASSERTED))
305
+ COVERAGE_DISPATCHER.add_custom('remove', _coverage_remove, parameters=(PARAM_SHARD,))
306
+
307
+
308
+ def cmd_coverage(a: argparse.Namespace) -> int:
309
+ return COVERAGE_DISPATCHER.dispatch(a)
310
+
311
+
312
+ def cmd_descriptor(a: argparse.Namespace) -> int:
313
+ dpath = os.path.join(a.dir, app_mod.NodeApp.DESCRIPTOR_FILE)
314
+ if not os.path.exists(dpath):
315
+ fail('no descriptor.json yet; it is mined on the first run')
316
+ with open(dpath, encoding='utf-8') as _fh:
317
+ d = json.load(_fh)
318
+ if a.republish:
319
+ r = _ask(a.dir, 'republish', timeout=60)
320
+
321
+ def human(r: dict) -> None:
322
+ for url, (okay, note) in (r.get('relays') or {}).items():
323
+ print(' %-8s %s %s' % ('sent' if okay else 'FAILED', url, note))
324
+ return emit(a, r, human)
325
+ # The descriptor is JSON whether or not --json was asked for.
326
+ print(json.dumps(d, indent=2))
327
+ return 0
328
+
329
+
330
+ def cmd_pull(a: argparse.Namespace) -> int:
331
+ pk = _peer(a.dir, a.peer)
332
+ filt = None
333
+ if a.filter:
334
+ filt = load_json_arg(a.filter, '--filter')
335
+ else:
336
+ for p in app_mod.load_config(a.dir).get('peers', []):
337
+ if p['pubkey'] == pk and p['shard'] == a.shard:
338
+ filt = p['filter']
339
+ if filt is None:
340
+ fail('no filter for shard %s with this peer; give --filter' % a.shard)
341
+ # The daemon's own pull deadline is sent as a request argument and the
342
+ # client waits a little longer than it, so the node's answer (done, or
343
+ # timed out by name) always arrives before this end gives up.
344
+ r = _ask(a.dir, 'pull', peer=pk, shard=a.shard, filter=filt, force=a.force,
345
+ timeout=a.timeout + 10, request={'timeout': a.timeout})
346
+
347
+ def human(r: dict) -> None:
348
+ c = r.get('counters') or {}
349
+ print('%s: %s events in, %s ranges, %s bytes in, dup ratio %s'
350
+ % (r.get('status'), c.get('events_in', 0), c.get('sync_ranges', 0), c.get('bytes_in', 0), r.get('dup_ratio')))
351
+ for sev, why in r.get('findings') or []:
352
+ print(' finding %s: %s' % (sev, why))
353
+ return emit(a, r, human)
354
+
355
+
356
+ class SyncCommandProvider:
357
+ """Registers sync, shard coverage, capacity, strain, and pull commands."""
358
+
359
+ @classmethod
360
+ def register(cls, sub: argparse._SubParsersAction) -> None:
361
+ add_command(sub, 'strain', cmd_strain, "what this node has survived, by direction, and the reserves it sets", js=True)
362
+ add_command(sub, 'subsystems', cmd_subsystems, 'every compartment of this node, and what each has refused', js=True)
363
+
364
+ rf = add_command(sub, 'refusals', cmd_refusals, 'every no this node said, across every compartment', js=True)
365
+ rf.add_argument('--last', type=int, default=5, help='how many per compartment')
366
+
367
+ ev = add_command(sub, 'events', cmd_events, "the node's recent bus events, from memory", js=True)
368
+ ev.add_argument('--since', type=int, default=0, help='only events after this seq')
369
+ ev.add_argument('--topics', nargs='*', help='keep only these topics')
370
+ ev.add_argument('--last', type=int, default=50, help='at most this many, newest')
371
+ sb = add_command(sub, 'subscribe', cmd_subscribe, "follow the node's bus live, one line per event", js=True)
372
+ sb.add_argument('--since', type=int, help='replay what the node still holds after this seq first')
373
+ sb.add_argument('--topics', nargs='*', help='keep only these topics')
374
+
375
+ add_command(sub, 'capacity', cmd_capacity, "this node's own ceiling, the pressure on it, and what it is shedding", js=True)
376
+ add_command(sub, 'scheduled', cmd_scheduled, 'every scheduled pull, throttles and holds', js=True)
377
+
378
+ c = add_command(sub, 'coverage', cmd_coverage, 'the shards this node claims to hold, as kind 30803', js=True)
379
+ COVERAGE_DISPATCHER.register_arguments(c, action_choices=('list', 'add', 'remove'))
380
+
381
+ d = add_command(sub, 'descriptor', cmd_descriptor, 'the kind 11801 this node published', js=True)
382
+ d.add_argument('--republish', action='store_true', help='send it to the relays again (needs the node running)')
383
+
384
+ pl = add_command(sub, 'pull', cmd_pull, 'pull a shard from a peer now (needs the node running)', peer=True, js=True)
385
+ pl.add_argument('shard')
386
+ pl.add_argument('--filter', help='a nostr filter, JSON or @file; default: the one configured for this peer and shard')
387
+ pl.add_argument('--force', action='store_true', help='pull even when the grant does not cover a session now')
388
+ pl.add_argument('--timeout', type=float, default=60.0, help='seconds the node gives the session (default 60, max 600)')