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,204 @@
1
+ """What this node has survived, and the reserves it sets.
2
+
3
+ `fleetmesh_core/strain.py` is the arithmetic and states why this is safe to act on. This is
4
+ the part that records and remembers.
5
+
6
+ Everything here was already being thrown away. A restart, a peer that went quiet
7
+ mid-session, a relay that dropped its subscription, a clock that drifted past
8
+ tolerance - the node correctly refuses to make any of them a finding about a
9
+ peer, and then discards them, so the information goes out with the accusation.
10
+
11
+ Nothing recorded here crosses the wire and nothing here is a finding. What it
12
+ does is thicken what this node holds in hand: how long it keeps accounting a
13
+ vanished peer might still need, and how long it goes on trying a door that was
14
+ closed. A node that has weathered a thing becomes better prepared for that thing,
15
+ which is the only kind of adaptation an attacker gains nothing by provoking.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Callable, Optional
20
+
21
+
22
+ from .types import FlagTreeReport, StrainStateReport
23
+
24
+ import dataclasses
25
+ from fleetmesh_core.accounting import strain as ref_strain
26
+ from fleetmesh_core.wire.interfaces import StrainDirection
27
+ from node.compartment import stores as st
28
+ from node.channels import Channel, NOTE
29
+ from node.metrics import COUNTER, Metric
30
+ from node.compartment import Command, Compartment, Need
31
+ from node.config.parameter import Parameter, ParamBlock
32
+
33
+ # `ref_strain.direction` normalises None to '' so a direction is always three
34
+ # strings; the ledger keys on the same shape so the ref arithmetic can take
35
+ # its dicts unchanged.
36
+ DirectionTuple = StrainDirection
37
+
38
+ # fleetmesh_core/strain.py annotates KINDS as tuple[int, ...] but constants.json lists
39
+ # strings; keep a correctly-typed view here so the membership check below is
40
+ # checked as str-in-str. Fix belongs in fleetmesh_core/ (not owned here).
41
+ _KINDS: frozenset[str] = frozenset(str(k) for k in ref_strain.KINDS)
42
+
43
+
44
+ @dataclasses.dataclass(frozen=True)
45
+ class StrainConfig(ParamBlock):
46
+ key = 'strain'
47
+ name = 'strain'
48
+ DERIVED: bool = False
49
+ PARAMS = (
50
+ Parameter(key='max_directions', name='Max Directions', description='Maximum directions tracked in strain ledger', type=int, default=4096, tags=('strain', 'reserves')),
51
+ )
52
+ max_directions: int = 4096
53
+
54
+
55
+ class StrainLedger(Compartment):
56
+ """What this node has survived, as a compartment the operator can watch.
57
+
58
+ It reads no config and changes nothing about any peer: what it holds is the
59
+ node's own reserves. Its trouble is the flag tree - a node whose strain is
60
+ nearly all one direction is structurally dependent on that peer and will
61
+ snap when it leaves, and nothing else in the protocol says so.
62
+ """
63
+
64
+ name: str = 'strain'
65
+ config_cls = StrainConfig
66
+ CHANNELS = (
67
+ Channel('strain_recorded', 'Strain event recorded in ledger', level=NOTE, say='strain event {kind} recorded', tags=('strain', 'reserves')),
68
+ )
69
+ METRICS = (
70
+ Metric(name='strain_events_recorded', description='Times strain events were recorded.', kind=COUNTER, counts='strain_recorded', tags=('strain', 'reserves')),
71
+ )
72
+ NEEDS = (
73
+ Need('accounts', 'stores.accounts'),
74
+ Need('ledger', 'ledger'),
75
+ )
76
+ COMMANDS = (
77
+ Command(
78
+ key='show',
79
+ name='Strain',
80
+ description='what this node has survived, by direction, and the reserves it sets',
81
+ tags=('strain', 'status'),
82
+ permission='strain/show',
83
+ run='state',
84
+ reply='{kinds, directions, weighted_directions, reserve_multiple, dominant_share, flag_tree}',
85
+ ),
86
+ )
87
+ STORES = (
88
+ st.Store(key='counts', description='strain counts per direction and kind',
89
+ kind=st.TABLE, shape=dict, bound=4096, keyed_by='direction', evict=st.LRU),
90
+ st.Store(key='last', description='strain last timestamp per direction and kind',
91
+ kind=st.TABLE, shape=dict, bound=4096, keyed_by='direction', evict=st.LRU),
92
+ )
93
+
94
+ def __init__(
95
+ self,
96
+ accounts: Optional[Any] = None,
97
+ clock: Optional[Callable[[], int]] = None,
98
+ tier_of: Optional[Callable[[str], str]] = None,
99
+ node: Optional[Any] = None,
100
+ ledger: Optional[Any] = None,
101
+ ) -> None:
102
+ super().__init__(node, clock)
103
+ self.accounts: Optional[Any] = accounts
104
+ if tier_of is None and ledger is not None:
105
+ self.tier_of: Callable[[str], str] = lambda pk: getattr(ledger, 'tier', {}).get(pk, 'probation')
106
+ else:
107
+ self.tier_of = tier_of or (lambda _pk: 'probation')
108
+ self.counts: st.Table[DirectionTuple, dict[str, int]] # direction -> kind -> n
109
+ self.last: st.Table[DirectionTuple, dict[str, int]] # direction -> kind -> when
110
+ if accounts is not None:
111
+ for peer, shard, transport, kind, n, at in accounts.strain():
112
+ d = ref_strain.direction(peer, shard, transport)
113
+ self.counts.setdefault(d, {})[kind] = n
114
+ self.last.setdefault(d, {})[kind] = at
115
+
116
+ def record(
117
+ self,
118
+ kind: str,
119
+ peer: Optional[str] = None,
120
+ shard: Optional[str] = None,
121
+ transport: Optional[str] = None,
122
+ at: Optional[int] = None,
123
+ n: int = 1,
124
+ ) -> Optional[DirectionTuple]:
125
+ """One thing survived. Silently ignores a kind the digest does not name,
126
+ so a caller cannot invent a category that no reserve reads."""
127
+ if kind not in _KINDS:
128
+ return None
129
+ at_val = at if at is not None else self.clock()
130
+ d = ref_strain.direction(peer, shard, transport)
131
+ self.counts.setdefault(d, {})[kind] = self.counts.get(d, {}).get(kind, 0) + n
132
+ self.last.setdefault(d, {})[kind] = at_val
133
+ if self.accounts is not None:
134
+ self.accounts.put_strain(peer, shard, transport, kind, at_val, n)
135
+ return d
136
+
137
+ # ---- the direction rule ---------------------------------------------------
138
+ def by_direction(self, kind: Optional[str] = None) -> dict[DirectionTuple, int]:
139
+ """direction -> how much stress came from it. Magnitude is collected
140
+ here and deliberately thrown away by `ref_strain.weigh`, which caps each
141
+ direction: one peer moves this node by at most one direction's worth."""
142
+ out: dict[DirectionTuple, int] = {}
143
+ for d, kinds in self.counts.items():
144
+ n = kinds.get(kind, 0) if kind else sum(kinds.values())
145
+ if n:
146
+ out[d] = n
147
+ return out
148
+
149
+ def weight(self, kind: Optional[str] = None) -> float:
150
+ return ref_strain.weigh(self.by_direction(kind), self.tier_of)
151
+
152
+ def multiple(self, kind: Optional[str] = None) -> float:
153
+ return ref_strain.multiple(self.weight(kind))
154
+
155
+ def reserve(self, base: float | int, kind: Optional[str] = None) -> float:
156
+ return ref_strain.reserve(base, self.by_direction(kind), self.tier_of)
157
+
158
+ # ---- the compartment contract -------------------------------------------------
159
+ def tick(self, now: int) -> list[tuple[Any, ...]]:
160
+ return [] # it records what happens to it; it does not act
161
+
162
+ def trouble(self) -> list[str]:
163
+ ft = self.state().get('flag_tree')
164
+ if not ft:
165
+ return []
166
+ return [
167
+ '%.0f%% of the strain this node has seen is one direction (%s): it is '
168
+ 'structurally dependent on it and will snap when it leaves'
169
+ % (
170
+ 100 * self.state().get('dominant_share', 0),
171
+ ' / '.join(x for x in (ft.get('direction') or []) if x)[:40],
172
+ )
173
+ ]
174
+
175
+ # ---- the operator's view --------------------------------------------------
176
+ def flag_tree(self) -> FlagTreeReport:
177
+ key, share = ref_strain.flag_tree(self.by_direction())
178
+ return {'direction': list(key) if key else None, 'share': round(share, 4)}
179
+
180
+ def state(self) -> StrainStateReport:
181
+ by = self.by_direction()
182
+ totals: dict[str, int] = {}
183
+ for kinds in self.counts.values():
184
+ for k, n in kinds.items():
185
+ totals[k] = totals.get(k, 0) + n
186
+ key, share = ref_strain.flag_tree(by)
187
+ st: StrainStateReport = {
188
+ 'kinds': totals,
189
+ 'directions': len(by),
190
+ 'weighted_directions': round(self.weight(), 4),
191
+ 'reserve_multiple': round(self.multiple(), 3),
192
+ 'dominant_share': round(share, 4),
193
+ }
194
+ if key is not None:
195
+ st['flag_tree'] = {
196
+ 'direction': list(key),
197
+ 'note': (
198
+ 'nearly all the strain this node has seen comes from one direction, '
199
+ 'so it is structurally dependent on it and will snap when it leaves. '
200
+ 'This is not a finding about that peer, which has done nothing but be '
201
+ 'the only one there'
202
+ ),
203
+ }
204
+ return st
@@ -0,0 +1,21 @@
1
+ """Types and records for the Strain compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, TypedDict
5
+
6
+
7
+ class FlagTreeReport(TypedDict, total=False):
8
+ """Dominant strain direction and its share."""
9
+ direction: Optional[list[str]]
10
+ share: float
11
+ note: Optional[str]
12
+
13
+
14
+ class StrainStateReport(TypedDict, total=False):
15
+ """The report Strain.state() gives of itself."""
16
+ kinds: dict[str, int]
17
+ directions: int
18
+ weighted_directions: float
19
+ reserve_multiple: float
20
+ dominant_share: float
21
+ flag_tree: Optional[FlagTreeReport]
@@ -0,0 +1,9 @@
1
+ """The operator's own decisions about peers, carried out once each.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+ """
5
+ from .compartment import Trust
6
+ from .types import TrustDecisionDetail, TrustStandingRow, TrustStateReport
7
+
8
+ __all__ = ['Trust', 'TrustDecisionDetail', 'TrustStandingRow', 'TrustStateReport']
9
+
@@ -0,0 +1,239 @@
1
+ """The operator's own decisions about peers, carried out once each.
2
+
3
+ A tier is only ever raised by a person. The ladder earns standing with observed
4
+ time; the specification says an operator MAY seed a tier for a node it knows out
5
+ of band, recorded as a signed event and reversible. `meshnode trust` writes that
6
+ decision to the config file and this compartment is what acts on it.
7
+
8
+ It ran inside the node's tick before, which meant a seed that could not be
9
+ applied - no peering yet, or a regrant that came back empty - was skipped in
10
+ silence. An operator who had written a decision and watched nothing happen had
11
+ no way to ask why. Every skip is a refusal here.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import dataclasses
16
+ from typing import Any, Callable, Mapping, Optional
17
+
18
+ from node.storage import grants as grants_mod
19
+ from node.compartment import stores as st
20
+ from node.channels import Channel, NOTE, TROUBLE
21
+ from node.metrics import COUNTER, Metric
22
+ from node.compartment import Command, Compartment, Need
23
+ from node.config.parameter import Parameter, ParamBlock
24
+
25
+
26
+ from .types import (
27
+ TrustStandingRow,
28
+ TrustStateReport,
29
+ )
30
+
31
+
32
+ def decision(entry: Any) -> tuple[str, int]:
33
+ """A trust entry is {'tier': t, 'at': unix} - or, from before decisions
34
+ were dated, the bare tier, which counts as decided at time zero."""
35
+ if isinstance(entry, dict):
36
+ return entry.get('tier', 'probation'), int(entry.get('at') or 0)
37
+ return entry, 0
38
+
39
+
40
+ def tiers(trust: Optional[Mapping[str, Any]]) -> dict[str, str]:
41
+ """The policy's view: peer -> tier."""
42
+ return {pk: decision(e)[0] for pk, e in (trust or {}).items()}
43
+
44
+
45
+ @dataclasses.dataclass(frozen=True)
46
+ class TrustConfig(ParamBlock):
47
+ key = 'trust'
48
+ name = 'trust'
49
+ DERIVED: bool = False
50
+ PARAMS = (
51
+ Parameter(key='max_decisions', name='Max Decisions', description='Maximum trust decisions remembered', type=int, default=4096, live=True, tags=('trust', 'policy')),
52
+ )
53
+ max_decisions: int = 4096
54
+
55
+
56
+ class Trust(Compartment):
57
+
58
+ name: str = 'trust'
59
+ reads: tuple[str, ...] = ('trust',)
60
+ live: tuple[str, ...] = ('trust',)
61
+ config_cls = TrustConfig
62
+ CHANNELS = (
63
+ Channel('trust_applied', 'Trust decision applied for peer', level=NOTE, say='trust decision applied for peer {peer}', tags=('trust', 'policy')),
64
+ Channel('trust_refused', 'Trust decision refused for peer', level=TROUBLE, say='trust decision refused for peer {peer}: {reason}', tags=('trust', 'policy')),
65
+ )
66
+ METRICS = (
67
+ Metric(name='trust_decisions_applied', description='Times trust decisions were applied.', kind=COUNTER, counts='trust_applied', tags=('trust', 'policy')),
68
+ Metric(name='trust_decisions_refused', description='Times trust decisions were refused.', kind=COUNTER, counts='trust_refused', tags=('trust', 'policy')),
69
+ )
70
+ NEEDS = (
71
+ Need('grant_store', 'stores.grants'),
72
+ Need('policy', 'policy'),
73
+ Need('regrant', 'regrant'),
74
+ )
75
+ COMMANDS = (
76
+ Command(
77
+ key='show',
78
+ name='Trust',
79
+ description='the decisions this operator has made, and which of them are still in '
80
+ 'force: carried out is not the same as true, and the ladder can move a '
81
+ 'seeded peer',
82
+ tags=('trust', 'status'),
83
+ permission='trust/show',
84
+ run='state',
85
+ reply='{decided, in_force, pending, overridden, waiting}',
86
+ ),
87
+ )
88
+ STORES = (
89
+ st.Store(key='_said', description='refused decisions said once, per peer',
90
+ kind=st.TABLE, shape=tuple, bound=4096, keyed_by='peer', evict=st.LRU),
91
+ )
92
+
93
+ def __init__(
94
+ self,
95
+ grant_store: grants_mod.GrantStore,
96
+ policy: Any,
97
+ regrant: Callable[[str, str, int], Any],
98
+ clock: Optional[Callable[[], int]] = None,
99
+ node: Optional[Any] = None,
100
+ ) -> None:
101
+ super().__init__(node, clock)
102
+ self.grants: grants_mod.GrantStore = grant_store
103
+ self.policy: Any = policy
104
+ self.regrant: Callable[[str, str, int], Any] = regrant
105
+ self._said: st.Table[str, tuple[tuple[str, int], str]] # peer -> the decision already refused, so a wait is said once
106
+
107
+ def tick(self, now: int) -> list[tuple[Any, ...]]:
108
+ """Carry out each dated decision in the config once.
109
+
110
+ A decision is compared with the record of decisions applied, kept
111
+ beside the grants, and not with the grant's tier: comparing tiers made
112
+ the seed re-issue `trusted` on the tick after every demotion the ladder
113
+ earned, so the ladder never held and every re-issue was one more
114
+ amend. A decision written while the node was down is still found on
115
+ the first tick back, because the record is on disk too. Applying a
116
+ seed also drops any grant the ladder had waiting for the boundary: the
117
+ operator's word is the later one.
118
+ """
119
+ did: list[tuple[Any, ...]] = []
120
+ trust = self.config('trust') or {}
121
+ self.policy.tiers = tiers(trust)
122
+ for peer_pk, entry in trust.items():
123
+ tier, at = decision(entry)
124
+ if self.grants.seed_applied(peer_pk) == (tier, at):
125
+ continue
126
+ row = self.grants.get(peer_pk, grants_mod.ISSUED)
127
+ if row is None or row['status'] != 'live':
128
+ # No peering yet: the policy seeds the next one. Recorded once
129
+ # per decision and not once a tick - a refusal repeated every
130
+ # thirty seconds rolls the buffer that holds the real ones, and
131
+ # makes trouble() shout about a wait that is not a fault.
132
+ self._say(peer_pk, (tier, at), 'no live peering to amend', now)
133
+ continue
134
+ if row['payload'].get('tier') == tier and self.grants.pending(peer_pk) is None:
135
+ self.grants.put_seed(peer_pk, tier, at) # already so; nothing to re-issue
136
+ continue
137
+ if self.regrant(peer_pk, tier, now) is not None:
138
+ self.grants.put_seed(peer_pk, tier, at)
139
+ self._said.pop(peer_pk, None)
140
+ did.append(('granted', peer_pk, tier))
141
+ else:
142
+ self._say(peer_pk, (tier, at), 'the grant could not be re-issued', now)
143
+ return did
144
+
145
+ def _say(self, peer_pk: str, decided: tuple[str, int], why: str, now: int) -> None:
146
+ if self._said.get(peer_pk) == (decided, why):
147
+ return
148
+ self._said[peer_pk] = (decided, why)
149
+ self.refusals.refuse('seed', why, subject=peer_pk,
150
+ detail={'tier': decided[0], 'at': decided[1]}, at=now)
151
+
152
+ def standing(self) -> list[TrustStandingRow]:
153
+ """Each decision, and what has become of it.
154
+
155
+ Three states, not two. `applied` used to mean "this dated decision was
156
+ carried out at some point", and an operator read it as "the tier I
157
+ chose is the tier in force". Those diverge the moment the ladder
158
+ demotes a seeded peer: the seed is recorded as applied, this compartment
159
+ correctly declines to re-issue it - re-issuing on every demotion is what
160
+ made the ladder never hold, and the dated record is the fix for that -
161
+ and the operator's decision quietly stops being true.
162
+
163
+ Seen on the mesh. A node seeded `trusted` was demoted to probation in a
164
+ run of S1 windows, and twelve hours later `trust` still said
165
+ `applied: 1, waiting: []` while every window ran under a probation
166
+ ceiling the operator had never chosen. Nothing was wrong with the
167
+ mechanism. The report was answering a different question from the one
168
+ being asked of it.
169
+
170
+ waiting decided, not carried out yet
171
+ pending carried out, and published at the next window boundary
172
+ in force carried out, and the grant says so
173
+ overridden carried out, and the ladder has since moved it
174
+
175
+ Only the last needs a person, which is why only the last is trouble.
176
+ `pending` exists because a decision no longer lands the moment it is
177
+ made: publishing a grant inside a running window breaks the subject's
178
+ receipt chain for the rest of it, so every decision waits for the
179
+ boundary. Without this state, the hour between deciding and landing
180
+ would read as overridden and shout for a person who need do nothing.
181
+ """
182
+ out: list[TrustStandingRow] = []
183
+ for pk, entry in (self.config('trust') or {}).items():
184
+ tier, at = decision(entry)
185
+ row = self.grants.get(pk, grants_mod.ISSUED)
186
+ held = row['payload'].get('tier') if row else None
187
+ pend = self.grants.pending(pk)
188
+ if self.grants.seed_applied(pk) != (tier, at):
189
+ state = 'waiting'
190
+ elif held == tier:
191
+ state = 'in force'
192
+ elif pend is not None and (pend.get('payload') or {}).get('tier') == tier:
193
+ state = 'pending'
194
+ else:
195
+ state = 'overridden'
196
+ out.append({'peer': pk, 'tier': tier, 'at': at, 'held': held, 'state': state})
197
+ return out
198
+
199
+ def state(self) -> TrustStateReport:
200
+ rows = self.standing()
201
+ return {
202
+ 'decided': {
203
+ r['peer'][:16]: {
204
+ 'tier': r['tier'],
205
+ 'at': r['at'],
206
+ 'held': r['held'],
207
+ 'state': r['state'],
208
+ }
209
+ for r in rows
210
+ },
211
+ 'in_force': sum(1 for r in rows if r['state'] == 'in force'),
212
+ 'pending': sorted(r['peer'][:16] for r in rows if r['state'] == 'pending'),
213
+ 'overridden': sorted(r['peer'][:16] for r in rows if r['state'] == 'overridden'),
214
+ 'waiting': sorted(r['peer'][:16] for r in rows if r['state'] == 'waiting'),
215
+ }
216
+
217
+ def trouble(self) -> list[str]:
218
+ """A decision waiting on a peering that does not exist yet is not
219
+ trouble - it is what a seed written before the peering looks like, and
220
+ the policy will apply it to the next handshake. A decision that reached
221
+ a live peering and could not be written is. So is one the ladder has
222
+ since overridden, because that one looks applied and is not in force,
223
+ and only a person can decide which of the two answers should stand.
224
+ """
225
+ out: list[str] = []
226
+ stuck = sorted({(pk or '')[:16] for pk, (_, why) in self._said.items()
227
+ if why == 'the grant could not be re-issued'})
228
+ if stuck:
229
+ out.append('%d trust decision(s) reached a live peering and could not be written: %s. '
230
+ 'The tier the operator decided on is not the tier in force'
231
+ % (len(stuck), ', '.join(stuck)))
232
+ gone = [r for r in self.standing() if r['state'] == 'overridden']
233
+ if gone:
234
+ out.append('%d trust decision(s) the ladder has overridden: %s. '
235
+ 'Each was carried out and is no longer in force; re-deciding means '
236
+ 'writing the tier again with a later date'
237
+ % (len(gone), ', '.join('%s decided %s, holds %s'
238
+ % (r['peer'][:16], r['tier'], r['held']) for r in gone)))
239
+ return out
@@ -0,0 +1,30 @@
1
+ """Types and records for the Trust compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, TypedDict
5
+
6
+
7
+ class TrustStandingRow(TypedDict, total=False):
8
+ """One standing row from trust decisions."""
9
+ peer: str
10
+ tier: str
11
+ at: int
12
+ held: Optional[str]
13
+ state: str
14
+
15
+
16
+ class TrustDecisionDetail(TypedDict, total=False):
17
+ """Decision detail for an evaluated peer."""
18
+ tier: str
19
+ at: int
20
+ held: Optional[str]
21
+ state: str
22
+
23
+
24
+ class TrustStateReport(TypedDict, total=False):
25
+ """The report Trust.state() gives of itself."""
26
+ decided: dict[str, TrustDecisionDetail]
27
+ in_force: int
28
+ pending: list[str]
29
+ overridden: list[str]
30
+ waiting: list[str]
@@ -0,0 +1,33 @@
1
+ """Services plane: applications, compute, settlement, paid fetch, sidecars, and data vending.
2
+
3
+ The 8 compartments of the services plane:
4
+ - gossip: group-topic epidemic fanout over the transport ladder
5
+ - compute: node-side lifecycle, offer publication and job handling
6
+ - compute_wire: routes compute/1.0 and 1.1 envelopes to ComputeSession
7
+ - settle: multi-rail settlement (Lightning hold invoices & Cashu Nutzap)
8
+ - paidfetch: pricing and payment gating for Blossom blob fetches
9
+ - sidecars: external service bridge and health monitoring
10
+ - dvm: NIP-90 Data Vending Machine compute gateway
11
+ - nsite: static site hosting and Blossom mirroring
12
+ """
13
+ from .gossip import GossipMesh
14
+ from .compute import Compute
15
+ from .compute_wire import ComputeWire
16
+ from .settle import Settlement
17
+ from .paidfetch import PaidFetch
18
+ from .sidecars import Sidecars
19
+ from .dvm import DVMGateway
20
+ from .nsite import NsiteGateway
21
+ from .video import VideoTransitCompartment
22
+
23
+ __all__ = [
24
+ 'GossipMesh',
25
+ 'Compute',
26
+ 'ComputeWire',
27
+ 'Settlement',
28
+ 'PaidFetch',
29
+ 'Sidecars',
30
+ 'DVMGateway',
31
+ 'NsiteGateway',
32
+ 'VideoTransitCompartment',
33
+ ]