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,310 @@
1
+ """The compartment. Optional; `"updates": true` in config.json turns it on."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable, Mapping, Optional, Sequence
5
+
6
+ from node import channels as ch_mod
7
+ from node import metrics as mx_mod
8
+ from node import release as release_mod
9
+ from node.compartment import stores as st
10
+ from node.protocols.nostr import client as relay_mod
11
+ from node.config import UpdatesConfig
12
+ from node.config.parameter import Parameter
13
+ from node.interfaces import NostrEvent
14
+ from node.compartment import Command, Compartment, Every, Need
15
+ from fleetmesh_core.peering.release import RELEASE_KIND, REVOKE_KIND, ReleaseInfo, parse_revocation, select
16
+ from .store import UpdateStore
17
+
18
+
19
+ from .types import UpdateStateReport
20
+
21
+
22
+ Fetcher = Callable[[Sequence[str], Sequence[Mapping[str, Any]]], list[NostrEvent]]
23
+
24
+
25
+ def _relay_fetch(urls: Sequence[str], filters: Sequence[Mapping[str, Any]]) -> list[NostrEvent]:
26
+ return relay_mod.query_all(urls, filters, timeout=10.0)
27
+
28
+
29
+ RELEASE_AVAILABLE: str = 'release.available'
30
+ RELEASE_REVOKED: str = 'release.revoked'
31
+
32
+
33
+ class Updates(Compartment):
34
+ """Check-and-report: the newest release this node could run, said once.
35
+
36
+ `trouble()` speaks when a newer release is within what the operator's
37
+ policy would apply - which in this tier is never, so it speaks about any
38
+ newer compatible patch or minor and stays quiet about a major: an
39
+ operator who chose `pinned` is told what they could move to without a
40
+ protocol or API change, not shouted at about a jump they would have to
41
+ opt into. `updates.ack <version>` silences one version; a newer one
42
+ speaks again.
43
+ """
44
+
45
+ name = 'updates'
46
+ optional = True
47
+ enabled_by = 'updates'
48
+ reads = ('updates', 'relays')
49
+ live = ('updates',)
50
+ config_cls = UpdatesConfig
51
+ SCHEMA = UpdateStore.SCHEMA
52
+ STORE_CLS = UpdateStore
53
+ NEEDS = (
54
+ Need('db_path', path='updates.sqlite'),
55
+ Need('cfg', block=UpdatesConfig, getter=True),
56
+ Need('relays', 'relays'),
57
+ )
58
+ STORES = (
59
+ st.Store(key='releases', description='recent release announcements seen',
60
+ kind=st.RING, shape=dict, bound=256),
61
+ )
62
+
63
+ # What this compartment measures. A counter naming a channel is fed by
64
+ # that channel being raised, so the line and the count come from one
65
+ # declaration and cannot disagree.
66
+ METRICS = (
67
+ mx_mod.Metric(
68
+ name='release_available',
69
+ description='Times a newer compatible release was announced by this node.',
70
+ kind=mx_mod.COUNTER, counts=RELEASE_AVAILABLE,
71
+ tags=('updates', 'release')),
72
+ mx_mod.Metric(
73
+ name='release_revoked',
74
+ description='Times a release this node could run was found revoked.',
75
+ kind=mx_mod.COUNTER, counts=RELEASE_REVOKED,
76
+ tags=('updates', 'release')),
77
+ mx_mod.Metric(
78
+ name='updates_check',
79
+ description='How long a release check took, end to end.',
80
+ kind=mx_mod.HISTOGRAM, unit='seconds', buckets=mx_mod.LATENCY_BUCKETS,
81
+ tags=('updates', 'latency')),
82
+ )
83
+
84
+ # Channel names as constants, not strings at the raise site. A mistyped
85
+ # string is an undeclared channel found at runtime by a counter; a
86
+ # mistyped constant is a NameError at import. eventbus.py's TOPIC_* names
87
+ # already work this way.
88
+ RELEASE_AVAILABLE: str = 'release.available'
89
+ RELEASE_REVOKED: str = 'release.revoked'
90
+
91
+ # What this compartment says. Before this the same event left as an
92
+ # unnamed positional tuple - ('release', 'available', version, kind) -
93
+ # which control.py joined with spaces for the journal and nothing
94
+ # structured ever saw. The tuple still goes back from tick(); the channel
95
+ # is what gives it a name, a level and one rendering per audience.
96
+ CHANNELS = (
97
+ ch_mod.Channel(
98
+ name=RELEASE_AVAILABLE,
99
+ description='A release newer than the one this node runs is published and '
100
+ 'compatible with the operator policy. Said once per version; '
101
+ '`updates.ack <version>` silences one.',
102
+ level=ch_mod.NOTE,
103
+ wire=lambda version, kind, running: {
104
+ 'version': version, 'kind': kind, 'running': running},
105
+ say='release {version} available ({kind}) - running {running}',
106
+ tags=('updates', 'release')),
107
+ ch_mod.Channel(
108
+ name=RELEASE_REVOKED,
109
+ description='A release this node could run was revoked by the signing key. '
110
+ 'Trouble rather than a note: a revoked release is one nobody '
111
+ 'should move to.',
112
+ level=ch_mod.TROUBLE,
113
+ wire=lambda version, reason: {'version': version, 'reason': reason},
114
+ say='release {version} revoked: {reason}',
115
+ tags=('updates', 'release', 'trouble')),
116
+ )
117
+ COMMANDS = (
118
+ Command(
119
+ key='check',
120
+ name='Check Updates',
121
+ description='ask the channel now; the state after',
122
+ tags=('updates', 'check'),
123
+ permission='updates/check',
124
+ run='check',
125
+ reply='dict',
126
+ ),
127
+ Command(
128
+ key='show',
129
+ name='Updates Status',
130
+ description='the newest compatible release as parsed, and every one seen',
131
+ tags=('updates', 'status'),
132
+ permission='updates/show',
133
+ run='show_cmd',
134
+ reply='dict',
135
+ ),
136
+ Command(
137
+ key='ack',
138
+ name='Acknowledge Update',
139
+ description='stop mentioning this version in trouble',
140
+ tags=('updates', 'ack'),
141
+ parameters=(
142
+ Parameter(key='version', name='version', description='semver version to acknowledge', type=str, default='', required=True),
143
+ ),
144
+ permission='updates/ack',
145
+ run='ack_cmd',
146
+ reply='dict',
147
+ ),
148
+ )
149
+ EVERY = (
150
+ Every(
151
+ # 0 is off (Every.is_off): checked only on command.
152
+ seconds=lambda self: float(max(0, self.settings().check_every)),
153
+ run='periodic_check',
154
+ name='check_updates',
155
+ ),
156
+ )
157
+
158
+ def __init__(
159
+ self,
160
+ db_path: str = ':memory:',
161
+ cfg: Optional[Callable[[], UpdatesConfig]] = None,
162
+ relays: Optional[Sequence[str]] = None,
163
+ clock: Optional[Callable[[], int]] = None,
164
+ fetch: Fetcher = _relay_fetch,
165
+ running: Optional[release_mod.Release] = None,
166
+ node: Any = None,
167
+ db: Optional[Any] = None,
168
+ ) -> None:
169
+ if db is not None:
170
+ if not isinstance(db, UpdateStore):
171
+ # Said here, by name, rather than as an AttributeError on the
172
+ # first store method - which is how B13 went unnoticed.
173
+ raise TypeError('updates needs an UpdateStore as db, not %s' % type(db).__name__)
174
+ store: Any = db
175
+ elif db_path:
176
+ store = UpdateStore(db_path)
177
+ else:
178
+ store = UpdateStore(':memory:')
179
+ super().__init__(node, clock, db=store)
180
+ self.store: Any = store
181
+ self.db = store
182
+ self._cfg: Callable[[], UpdatesConfig] = cfg or (lambda: UpdatesConfig())
183
+ self._relays: list[str] = list(relays or []) # not a store: the configured relays
184
+ self._fetch = fetch
185
+ self.running: release_mod.Release = running or release_mod.running()
186
+ self.newest: Optional[ReleaseInfo] = None # newest compatible, newer than running
187
+ self.incompatible: Optional[ReleaseInfo] = None # newest that speaks another protocol
188
+ self._closed = False
189
+ last = self.store.last_check()
190
+ if last is not None:
191
+ self._every_last['check_updates'] = last
192
+
193
+ # ---- configuration ----------------------------------------------------------------
194
+ def settings(self) -> UpdatesConfig:
195
+ return self._cfg()
196
+
197
+ def channel(self) -> list[str]:
198
+ return list(self.settings().channel) or self._relays
199
+
200
+ def version(self) -> str:
201
+ """What this node runs, by the operator's word if they gave one."""
202
+ return self.settings().pinned or self.running.version
203
+
204
+ def policy_for(self, compartment: str = '*') -> str:
205
+ pol = self.settings().policy
206
+ return str(pol.get(compartment) or pol.get('*') or 'pinned')
207
+
208
+ # ---- the check ----------------------------------------------------------------------
209
+ def check(self, now: Optional[int] = None) -> UpdateStateReport:
210
+ now = self.clock() if now is None else now
211
+ s = self.settings()
212
+ urls = self.channel()
213
+ # kind + author only: relays index single-letter tags (NIP-01), so a
214
+ # `#name` filter matches nothing anywhere. parse_release_event keeps
215
+ # the name check; releases of other packages are skipped, not counted.
216
+ filters = [{'kinds': [RELEASE_KIND, REVOKE_KIND], 'authors': [s.release_key], 'limit': 100}]
217
+ reachable: dict[str, bool] = {}
218
+ events: list[NostrEvent] = []
219
+ for url in urls:
220
+ try:
221
+ events.extend(self._fetch([url], filters))
222
+ reachable[url] = True
223
+ except Exception as e: # a relay down is state, not a crash
224
+ reachable[url] = False
225
+ self.refusals.refuse('channel', 'unreachable', subject=url, detail={'error': str(e)[:120]}, at=now)
226
+ # the pure half - revocations first whichever order relays hand
227
+ # events back, then the newest compatible / incompatible newer -
228
+ # is fleetmesh_core.peering.release.select, the rule the conformance vectors pin
229
+ sel = select(events, s.release_key, self.version(), self.running.protocol, self.running.name,
230
+ already_revoked=self.store.revoked())
231
+ for ev in events:
232
+ rv = parse_revocation(ev, s.release_key)
233
+ if rv is not None:
234
+ self.store.revoke(rv[0], rv[1], now)
235
+ for eid, why in sel.refused:
236
+ self.refusals.refuse('release', why, subject=eid[:16], at=now)
237
+ revoked = set(sel.revoked)
238
+ # a revocation can arrive after the release was already the newest;
239
+ # a previous check's pick stays unless this check found a newer one
240
+ newest = None if self.newest and self.newest.event_id in revoked else self.newest
241
+ incompatible = None if self.incompatible and self.incompatible.event_id in revoked else self.incompatible
242
+ if sel.newest_compatible and (newest is None or release_mod.compare(sel.newest_compatible.version, newest.version) > 0):
243
+ newest = sel.newest_compatible
244
+ if sel.incompatible_newer and (incompatible is None
245
+ or release_mod.compare(sel.incompatible_newer.version, incompatible.version) > 0):
246
+ incompatible = sel.incompatible_newer
247
+ self.newest, self.incompatible = newest, incompatible
248
+ if newest is not None:
249
+ self.store.saw(newest.version, newest.event_id, newest.sha256, now)
250
+ self.store.checked(now, reachable)
251
+ return self.state()
252
+
253
+ def periodic_check(self, now: int) -> list[tuple[Any, ...]]:
254
+ if self.settings().check_every <= 0:
255
+ return []
256
+ before = self.newest.version if self.newest else None
257
+ self.check(now)
258
+ after = self.newest.version if self.newest else None
259
+ return [('release', 'available', after, release_mod.classify(self.version(), after))] if after and after != before else []
260
+
261
+ # ---- the compartment contract ---------------------------------------------------------------
262
+ def state(self) -> UpdateStateReport:
263
+ if self._closed:
264
+ return {'stopped': True}
265
+ n = self.newest
266
+ cfg = self.settings() # parsed once, not three times (audit E7)
267
+ return {
268
+ 'running': dict(self.running.as_dict(), pinned=cfg.pinned),
269
+ 'release_key': cfg.release_key,
270
+ 'policy': dict(cfg.policy),
271
+ 'channel': self.channel(),
272
+ 'reachable': self.store.reachable(),
273
+ 'last_check': self.store.last_check(),
274
+ 'newest_compatible': dict(n.as_dict(), **{'class': release_mod.classify(self.version(), n.version),
275
+ 'acked': self.store.acked(n.version)}) if n else None,
276
+ 'incompatible_newer': self.incompatible.as_dict() if self.incompatible else None,
277
+ 'revoked': self.store.revoked(),
278
+ }
279
+
280
+ def trouble(self) -> list[str]:
281
+ out: list[str] = []
282
+ n = self.newest
283
+ if n is not None and not self.store.acked(n.version):
284
+ cls = release_mod.classify(self.version(), n.version)
285
+ if cls in ('patch', 'minor'):
286
+ out.append('%s %s available (%s); you run %s; policy %s - `meshnode updates.show` for the release'
287
+ % (n.name, n.version, cls, self.version(), self.policy_for()))
288
+ if self.running.source == 'checkout' and n is not None and not self.store.acked(n.version):
289
+ out.append('this node runs from a checkout; updates are reported, never applied here')
290
+ reach = self.store.reachable()
291
+ if reach and not any(reach.values()):
292
+ out.append('update channel unreachable: %s' % ', '.join(sorted(reach)))
293
+ return out
294
+
295
+ def show_cmd(self) -> Mapping[str, Any]:
296
+ return {'newest_compatible': self.newest.as_dict() if self.newest else None,
297
+ 'incompatible_newer': self.incompatible.as_dict() if self.incompatible else None,
298
+ 'seen': self.store.seen(), 'revoked': self.store.revoked()}
299
+
300
+ def ack_cmd(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
301
+ v = str(args.get('version', ''))
302
+ if release_mod.parse_semver(v) is None:
303
+ raise ValueError('updates.ack: version must be a semver')
304
+ return {'version': v, 'acked': self.store.ack(v)}
305
+
306
+ def stop(self) -> Any:
307
+ self._closed = True
308
+ self.store.close()
309
+ super().stop()
310
+ return None
@@ -0,0 +1,79 @@
1
+ """What the update check has seen and what the operator has acknowledged.
2
+
3
+ Durable so a restart does not re-announce a release the operator already
4
+ read, and so `last_check` survives to be reported honestly.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from typing import Any, Mapping, Optional
10
+
11
+ from node.compartment.schema import Column, Table
12
+ from node.storage import SqliteStore
13
+
14
+
15
+ class UpdateStore(SqliteStore):
16
+ SCHEMA = (
17
+ Table('kv', (
18
+ Column('k', 'TEXT', primary_key=True),
19
+ Column('v', 'TEXT', not_null=True),
20
+ )),
21
+ Table('seen', (
22
+ Column('version', 'TEXT', primary_key=True),
23
+ Column('event_id', 'TEXT', not_null=True),
24
+ Column('sha256', 'TEXT', not_null=True),
25
+ Column('first_seen', 'INTEGER', not_null=True),
26
+ Column('acked', 'INTEGER', not_null=True, default=0),
27
+ )),
28
+ Table('revoked', (
29
+ Column('event_id', 'TEXT', primary_key=True),
30
+ Column('reason', 'TEXT', not_null=True),
31
+ Column('seen', 'INTEGER', not_null=True),
32
+ )),
33
+ )
34
+
35
+ def _get(self, k: str) -> Optional[Any]:
36
+ row = self._one('SELECT v FROM kv WHERE k=?', (k,))
37
+ return json.loads(row[0]) if row else None
38
+
39
+ def _set(self, k: str, v: Any) -> None:
40
+ with self.transaction() as cur:
41
+ cur.execute('INSERT OR REPLACE INTO kv (k, v) VALUES (?, ?)', (k, json.dumps(v)))
42
+
43
+ def last_check(self) -> Optional[int]:
44
+ v = self._get('last_check')
45
+ return int(v) if v is not None else None
46
+
47
+ def checked(self, now: int, reachable: dict[str, bool]) -> None:
48
+ self._set('last_check', now)
49
+ self._set('reachable', reachable)
50
+
51
+ def reachable(self) -> dict[str, bool]:
52
+ return dict(self._get('reachable') or {})
53
+
54
+ def saw(self, version: str, event_id: str, sha256: str, now: int) -> bool:
55
+ """Record a release; True if this is the first time."""
56
+ with self.transaction() as cur:
57
+ cur.execute('INSERT OR IGNORE INTO seen (version, event_id, sha256, first_seen) VALUES (?,?,?,?)',
58
+ (version, event_id, sha256, now))
59
+ return cur.rowcount == 1
60
+
61
+ def ack(self, version: str) -> bool:
62
+ with self.transaction() as cur:
63
+ cur.execute('UPDATE seen SET acked=1 WHERE version=?', (version,))
64
+ return cur.rowcount == 1
65
+
66
+ def acked(self, version: str) -> bool:
67
+ row = self._one('SELECT acked FROM seen WHERE version=?', (version,))
68
+ return bool(row and row[0])
69
+
70
+ def seen(self) -> list[Mapping[str, Any]]:
71
+ rows = self._all('SELECT version, event_id, sha256, first_seen, acked FROM seen ORDER BY first_seen')
72
+ return [dict(version=r[0], event_id=r[1], sha256=r[2], first_seen=r[3], acked=bool(r[4])) for r in rows]
73
+
74
+ def revoke(self, event_id: str, reason: str, now: int) -> None:
75
+ with self.transaction() as cur:
76
+ cur.execute('INSERT OR IGNORE INTO revoked (event_id, reason, seen) VALUES (?,?,?)', (event_id, reason, now))
77
+
78
+ def revoked(self) -> dict[str, str]:
79
+ return {r[0]: r[1] for r in self._all('SELECT event_id, reason FROM revoked')}
@@ -0,0 +1,20 @@
1
+ """Types and records for the Updates compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional, TypedDict
5
+
6
+ from fleetmesh_core.peering.release import ReleaseDict
7
+
8
+
9
+ class UpdateStateReport(TypedDict, total=False):
10
+ """The report Updates.state() gives of itself."""
11
+ stopped: bool
12
+ running: Mapping[str, Any]
13
+ release_key: str
14
+ policy: Mapping[str, str]
15
+ channel: list[str]
16
+ reachable: Mapping[str, bool]
17
+ last_check: Optional[int]
18
+ newest_compatible: Optional[Mapping[str, Any]]
19
+ incompatible_newer: Optional[ReleaseDict]
20
+ revoked: list[str]
@@ -0,0 +1,28 @@
1
+ """Peering plane: trust, strain, peering, budget, capacity, reporter."""
2
+ from . import budget, capacity, peering, reporter, strain, trust
3
+ from .budget.core import Budget, BudgetConfig
4
+ from .capacity.compartment import Capacity
5
+ from .peering.core import Peering, PolicyConfig
6
+ from .reporter.core import Reporter, ReporterConfig
7
+ from .strain.compartment import StrainConfig, StrainLedger
8
+ from .trust.compartment import Trust, TrustConfig
9
+
10
+ __all__ = [
11
+ 'Budget',
12
+ 'BudgetConfig',
13
+ 'Capacity',
14
+ 'Peering',
15
+ 'PolicyConfig',
16
+ 'Reporter',
17
+ 'ReporterConfig',
18
+ 'StrainConfig',
19
+ 'StrainLedger',
20
+ 'Trust',
21
+ 'TrustConfig',
22
+ 'budget',
23
+ 'capacity',
24
+ 'peering',
25
+ 'reporter',
26
+ 'strain',
27
+ 'trust',
28
+ ]
@@ -0,0 +1,43 @@
1
+ """budget/1.0 over the wire, and the window it feeds.
2
+
3
+ The grant is a promise about what a subject may do to an issuer. Both sides
4
+ count it. The issuer meters what the peer actually did (bytes it sent,
5
+ events it delivered, requests it made); the subject meters the same
6
+ quantities from its own side and states them, cumulatively, in a chained
7
+ receipt on the cadence the grant names. The issuer answers every receipt
8
+ with an ack carrying the head it holds. When a window closes and the grace
9
+ has passed, the issuer evaluates measured against claimed with fleetmesh_core/window.py - under
10
+ the grant that governed that window - and applies the ladder's remedy with
11
+ the reference's AIMD. The grant it decides on waits for the next window
12
+ boundary; there it is written, published as a new kind 30801 under the same
13
+ blinded d tag, and the subject is told with peering/1.0/amend. The standing
14
+ ledger records the window.
15
+
16
+ The wait is not a nicety. Published the moment the grace period ended, an
17
+ amend landed six minutes into the next window, and a receipt is a statement
18
+ about one window under one grant: the issuer rejected every receipt still
19
+ naming the grant it had just replaced, the subject restarted its chain at
20
+ seq 0 on the one it had just taken up, and every window after the first
21
+ closed on a claim from its first ten minutes. Both nodes filed S2 against an
22
+ honest peer every hour, and one published a kind 30802 about it. A chain is
23
+ per window and survives the grant changing under it; a receipt names the
24
+ grant the subject held when it wrote it, and the issuer accepts the grant
25
+ that governed the window, the one before it, and the one current now.
26
+
27
+ Every number here is in the issuer's frame, which is the grant's: events_in
28
+ is events into the issuer, bytes_out is bytes the issuer sent. That is the
29
+ frame the specification's tier ceilings are written in and the frame
30
+ fleetmesh_core/badpeer.py measures in.
31
+ """
32
+ from __future__ import annotations
33
+ from .meter import ( # noqa: E402
34
+ AMEND, C, KEEP_FINDINGS, METERED, PIURI, SYNC, TOLERANCE, ZERO, Meter, _observe, _ranges, metered_bytes,
35
+ )
36
+ from .core import Budget # noqa: E402
37
+ from .chain import OutChain # noqa: E402
38
+ from .evaluation import Evaluation, Repricing # noqa: E402
39
+
40
+ __all__ = [
41
+ 'AMEND', 'Budget', 'C', 'Evaluation', 'KEEP_FINDINGS', 'METERED', 'Meter', 'OutChain', 'PIURI', 'Repricing', 'SYNC', 'TOLERANCE', 'ZERO',
42
+ '_observe', '_ranges', 'metered_bytes',
43
+ ]
@@ -0,0 +1,43 @@
1
+ """The outbound receipt chain this node keeps per (peer, window).
2
+
3
+ One per window this node has sent a receipt in. `seq` and `prev` are what
4
+ the next receipt will carry; `heads` is every head this node produced, in
5
+ sequence order, so an ack naming one says how far behind the receiver is;
6
+ `final` marks the chain closed by the window's final receipt. Written to
7
+ the AccountStore before the receipt leaves (node/budget/receipts.py says
8
+ why), and read back whole on restart.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import asdict, dataclass, field
14
+ from typing import Any, Mapping, Optional, cast
15
+
16
+ from .meter import ZERO
17
+
18
+ __all__ = ['OutChain', 'OutChainDict']
19
+
20
+
21
+ from .types import OutChainDict
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class OutChain:
26
+ grant: str # envelope id of the grant governing the window
27
+ seq: int = 0
28
+ prev: str = ZERO # the head the first receipt chains from
29
+ sent_at: Optional[int] = None
30
+ last: Optional[dict[str, int]] = None # the counters the latest receipt claimed
31
+ heads: list[str] = field(default_factory=list)
32
+ final: bool = False
33
+
34
+ def to_dict(self) -> OutChainDict:
35
+ return cast(OutChainDict, asdict(self))
36
+
37
+ @classmethod
38
+ def from_dict(cls, body: Mapping[str, Any]) -> OutChain:
39
+ """A chain row as the AccountStore stored it. Rows written before
40
+ `heads` and `final` were kept read as empty and open."""
41
+ return cls(grant=body.get('grant') or '', seq=int(body.get('seq', 0)), prev=body.get('prev') or ZERO,
42
+ sent_at=body.get('sent_at'), last=body.get('last'),
43
+ heads=list(body.get('heads') or []), final=bool(body.get('final', False)))