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,198 @@
1
+ """The handshake over the wire: the proposer (`propose`), the acceptor
2
+ (`handle`, one inbound peering/1.0 message) and the loop that serves them.
3
+ What each message *means* is fleetmesh_core/peering.py's; this is only the carrying."""
4
+ from __future__ import annotations
5
+
6
+ import queue
7
+ import threading
8
+ import time
9
+ from typing import Any, Mapping, Optional
10
+
11
+ from fleetmesh_core.peering import peering
12
+ from fleetmesh_core.storage import replication
13
+ from fleetmesh_core.wire.wire_types import PeeringAcceptBody
14
+ from fleetmesh_core.identity.did import did_nostr
15
+ from node.storage import grants as grants_mod
16
+ from .envelope import PeeringError
17
+ from .shape import PeeringShape
18
+
19
+
20
+ from .types import PeeringResultDetail
21
+
22
+
23
+ class HandshakeMixin:
24
+ # ---- proposer ----------------------------------------------------------
25
+ def propose(
26
+ self: PeeringShape,
27
+ peer_pk: str,
28
+ want: Any,
29
+ offer: Optional[list[Any]] = None,
30
+ budget_self: Optional[Mapping[str, Any]] = None,
31
+ timeout: float = 30.0,
32
+ ) -> tuple[str, PeeringResultDetail | Mapping[str, Any]]:
33
+ """Run the whole handshake against one peer. Returns ('peered', detail) or ('declined', body)."""
34
+ budget_self = budget_self or {'events_in': 600, 'bytes_in': 8388608,
35
+ 'window': peering.CONSTANTS['grant']['window_seconds_default']}
36
+ want_list = want if isinstance(want, list) else [want]
37
+ proposal = self.host.propose(did_nostr(peer_pk), want_list, offer or [], budget_self,
38
+ pow_event=self.descriptor['id'])
39
+ reply = self.wire_client.request(peer_pk, proposal, timeout=timeout)
40
+ if reply is None:
41
+ raise PeeringError('no answer from %s within %.0fs' % (peer_pk[:16], timeout))
42
+ msg = reply.message
43
+ if msg['type'].endswith('/decline'):
44
+ self.log.append(('declined by', peer_pk, msg['body']))
45
+ return 'declined', msg['body']
46
+ if not msg['type'].endswith('/offer'):
47
+ raise PeeringError('expected an offer, got %s' % msg['type'])
48
+ held = self.grants.get(peer_pk, grants_mod.ISSUED)
49
+ if held is not None and held['status'] == 'live':
50
+ # Already granting this peer: proposing again is a check that the
51
+ # peering stands, not a reason to reissue at the opening tier and
52
+ # throw away what the peer's clean windows earned.
53
+ acc_body: PeeringAcceptBody = {'grant_envelope': held['envelope_id'],
54
+ 'selected_transport': msg['body']['transports'][0]}
55
+ # The row from get() never carried the event - it lives in the
56
+ # history table - so this used to read a key that was never there
57
+ # and the re-accept always went out without the envelope.
58
+ held_event = self.grants.envelope_event(peer_pk, grants_mod.ISSUED, held['envelope_id'])
59
+ if held_event:
60
+ acc_body['envelope_event'] = held_event
61
+ accept = self.host._msg('accept', acc_body,
62
+ did_nostr(peer_pk), thid=proposal['id'])
63
+ published: dict[str, dict[str, tuple[bool, str]]] = {}
64
+ else:
65
+ accept = dict(self.host.accept(msg, did_nostr(peer_pk), want_list, self.clock()))
66
+ published = self._flush_envelopes()
67
+ accept_body = dict(accept.get('body', {}))
68
+ if self.host.envelopes.get(peer_pk):
69
+ accept_body['envelope_event'] = self.host.envelopes[peer_pk]
70
+ accept['body'] = accept_body
71
+ mine = self.host.envelopes.get(peer_pk)
72
+ self.grants.put(peer_pk, grants_mod.ISSUED, self.host.issued[peer_pk], accept['body']['grant_envelope'],
73
+ self.clock(), event=mine, env_created_at=mine['created_at'] if mine else None)
74
+ self.wire_client.send(peer_pk, accept)
75
+ received = self._take_received(msg['body']['grant_envelope'], peer_pk, msg['body'].get('envelope_event'))
76
+ issued = self.host.issued.get(peer_pk) or (held['payload'] if held else None)
77
+ if held is None or held['status'] != 'live':
78
+ for cb in self.on_grant_issued:
79
+ cb(peer_pk, 'accept', accept['body']['grant_envelope'], (issued or {}).get('tier'))
80
+ for peered in self.on_peered:
81
+ peered(peer_pk, 'proposer', accept['body']['selected_transport'])
82
+ return 'peered', {'issued': issued, 'received': received,
83
+ 'transport': accept['body']['selected_transport'], 'published': published}
84
+
85
+ # ---- acceptor ----------------------------------------------------------
86
+ def handle_propose(self: PeeringShape, inbound: Any, sender: str) -> tuple[Any, ...]:
87
+ msg = getattr(inbound, 'message', inbound)
88
+ body = msg.get('body')
89
+ if not isinstance(body, dict):
90
+ return ('ignored', sender, 'malformed body')
91
+ held = self.grants.get(sender, grants_mod.ISSUED)
92
+ if held is not None and held['status'] == 'live':
93
+ theirs = {tr: i for i, tr in enumerate(body.get('transports', []))}
94
+ ladder = replication.negotiate(self.host.policy.transports, theirs) or [next(iter(self.host.policy.transports))]
95
+ self.host.threads[sender] = msg['id']
96
+ reply = self.host._msg('offer', {'tier': held['payload']['tier'], 'limits': held['payload']['limits'],
97
+ 'transports': ladder, 'grant_envelope': held['envelope_id']},
98
+ msg['from'], thid=msg['id'])
99
+ self.wire_client.send(sender, reply)
100
+ return ('re-offered', sender, held['envelope_id'])
101
+ if self.capacity is not None and self.capacity.refusing():
102
+ reply = self.host._msg('decline',
103
+ {'code': peering.CAPACITY,
104
+ 'message': self.capacity.refuse_peering(sender)},
105
+ msg['from'], thid=msg['id'])
106
+ self.wire_client.send(sender, reply)
107
+ return ('declined', sender, reply['body'])
108
+ peer_desc = self.peers.descriptor(sender)
109
+ reply = self.host.offer(msg, peer_desc, self.clock())
110
+ published = self._flush_envelopes()
111
+ if not reply['type'].endswith('/offer'):
112
+ self.wire_client.send(sender, reply)
113
+ return ('declined', sender, reply['body'])
114
+ reply = dict(reply)
115
+ reply_body = dict(reply.get('body', {}))
116
+ if self.host.envelopes.get(sender):
117
+ reply_body['envelope_event'] = self.host.envelopes[sender]
118
+ reply['body'] = reply_body
119
+ mine = self.host.envelopes.get(sender)
120
+ self.grants.put(sender, grants_mod.ISSUED, self.host.issued[sender],
121
+ reply['body']['grant_envelope'], self.clock(),
122
+ event=mine, env_created_at=mine['created_at'] if mine else None)
123
+ self.wire_client.send(sender, reply)
124
+ for cb in self.on_grant_issued:
125
+ cb(sender, 'offer', reply['body']['grant_envelope'], (self.host.issued.get(sender) or {}).get('tier'))
126
+ return ('offered', sender, published)
127
+
128
+ def handle_accept(self: PeeringShape, inbound: Any, sender: str) -> tuple[Any, ...]:
129
+ msg = getattr(inbound, 'message', inbound)
130
+ body = msg.get('body')
131
+ if not isinstance(body, dict):
132
+ return ('ignored', sender, 'malformed body')
133
+ held = self.grants.get(sender, grants_mod.ISSUED)
134
+ if held is None or held['status'] != 'live':
135
+ return ('ignored', sender, 'accept from a node this node did not offer to')
136
+ grant = self._take_received(msg['body']['grant_envelope'], sender, msg['body'].get('envelope_event'))
137
+ for peered in self.on_peered:
138
+ peered(sender, 'acceptor', msg['body'].get('selected_transport'))
139
+ return ('peered', sender, grant)
140
+
141
+ def handle_amend(self: PeeringShape, inbound: Any, sender: str) -> tuple[Any, ...]:
142
+ msg = getattr(inbound, 'message', inbound)
143
+ body = msg.get('body')
144
+ if not isinstance(body, dict):
145
+ return ('ignored', sender, 'malformed body')
146
+ new_id = msg['body'].get('new_grant_envelope')
147
+ if not new_id:
148
+ self._refuse('amend', 'no new grant named', sender)
149
+ return ('ignored', sender, 'amend without a new grant')
150
+ held = self.grants.get(sender, grants_mod.RECEIVED)
151
+ if held is None or held['status'] != 'live':
152
+ self._refuse('amend', 'from a node whose grant this node does not hold', sender,
153
+ {'named': new_id[:16]})
154
+ return ('ignored', sender, 'amend from a node whose grant this node does not hold')
155
+ was = held['envelope_id']
156
+ try:
157
+ grant = self._take_received(new_id, sender, msg['body'].get('envelope_event'))
158
+ except Exception as e:
159
+ self._refuse('amend', 'the grant it names could not be taken up', sender,
160
+ {'named': new_id[:16], 'error': '%s: %s' % (type(e).__name__, e)})
161
+ return ('ignored', sender, 'amend names a grant this node could not take up: %s' % e)
162
+ now_held = self.grants.get(sender, grants_mod.RECEIVED)
163
+ if now_held is not None and now_held['envelope_id'] == was and was != new_id:
164
+ self._refuse('amend', 'refused as older than the grant already held', sender,
165
+ {'named': new_id[:16], 'holding': was[:16]})
166
+ return ('ignored', sender, 'amend is older than the grant held')
167
+ return ('amended', sender, grant)
168
+
169
+ def handle_terminate(self: PeeringShape, inbound: Any, sender: str) -> tuple[Any, ...]:
170
+ msg = getattr(inbound, 'message', inbound)
171
+ body = msg.get('body')
172
+ if not isinstance(body, dict):
173
+ return ('ignored', sender, 'malformed body')
174
+ self.grants.zero(sender, self.clock())
175
+ if sender in self.host.issued:
176
+ self.host.terminate(sender, reason='peer terminated')
177
+ return ('terminated', sender, msg['body'])
178
+
179
+ def serve(self: PeeringShape, stop: threading.Event, poll: float = 0.5) -> None:
180
+ """Handle inbound peering messages until stop is set; others go back to the inbox."""
181
+ while not stop.is_set():
182
+ inbound = self.transport.receive(timeout=poll)
183
+ if inbound is None:
184
+ continue
185
+ try:
186
+ done = self.handle(inbound)
187
+ except Exception as e:
188
+ sender = getattr(inbound, 'sender', '') if inbound is not None else ''
189
+ self.log.append(('error', sender, str(e)))
190
+ continue
191
+ if done is None:
192
+ try:
193
+ self.transport.inbox.put_nowait(inbound) # not peering; leave it for whoever is
194
+ except queue.Full:
195
+ pass
196
+ time.sleep(poll)
197
+ else:
198
+ self.log.append(done)
@@ -0,0 +1,80 @@
1
+ """The shape of a Peering as its mixins see it.
2
+
3
+ The one class is split across core.py and handshake.py, the same way
4
+ node/reporter/ is: each mixin method annotates `self` as this Protocol so
5
+ the checker knows what is on it without the mixins inheriting from one
6
+ another. A declaration only; Peering satisfies it structurally.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Callable, Mapping, Optional, Protocol, Sequence
11
+
12
+ from fleetmesh_core.wire.interfaces import GrantPayload, NostrEvent, UnsignedNostrEvent
13
+
14
+ from node.compartment import stores as st
15
+ from node.storage import grants as grants_mod
16
+ from node.compartment import Refusals
17
+
18
+
19
+ class PeeringHost(Protocol):
20
+ """What the wire needs from the peering state machine - exactly this.
21
+
22
+ fleetmesh_core.peering.Node satisfies it, and so does WireNode, which is
23
+ what Peering builds when none is given. The members are the whole of the
24
+ reach: the four tables the handshake reads and writes, the five message
25
+ writers, and the one method WireNode makes real (`_publish_envelope`).
26
+ A test may hand in anything with these members; nothing else on the
27
+ core Node is touched (review R11).
28
+ """
29
+ policy: Any
30
+ issued: dict[str, GrantPayload]
31
+ envelopes: dict[str, NostrEvent | UnsignedNostrEvent]
32
+ threads: dict[str, str]
33
+ digest_mismatch: dict[str, str]
34
+ published: list[NostrEvent]
35
+
36
+ def _msg(self, kind: str, body: Mapping[str, Any], to: str, thid: Optional[str] = None) -> Mapping[str, Any]: ...
37
+ def propose(self, to_did: str, want: Sequence[Mapping[str, Any]], offer: Sequence[Mapping[str, Any]],
38
+ budget_self: Mapping[str, Any], pow_event: Optional[str] = None,
39
+ pow_bits: Optional[int] = None) -> Mapping[str, Any]: ...
40
+ def offer(self, proposal: Mapping[str, Any], peer_descriptor: Optional[NostrEvent], now: int) -> Mapping[str, Any]: ...
41
+ def accept(self, offer: Mapping[str, Any], peer_did: str, want: Sequence[Mapping[str, Any]], now: int) -> Mapping[str, Any]: ...
42
+ def terminate(self, peer_pubkey: str, reason: str = 'operator', grace_seconds: int = 0) -> Mapping[str, Any]: ...
43
+ def disclose(self, query: Optional[list[dict[str, str]]] = None) -> dict[str, list[dict[str, str]]]: ...
44
+ def _publish_envelope(self, subject: str, grant: GrantPayload, now: Optional[int]) -> NostrEvent | UnsignedNostrEvent: ...
45
+
46
+
47
+ class PeeringShape(Protocol):
48
+ TROUBLE_WINDOW: int
49
+ refusals: Refusals
50
+ capacity: Optional[Any]
51
+ _reconciled: Any
52
+ ks: Any
53
+ descriptor: Any
54
+ peers: Any
55
+ transport: Any
56
+ wire_client: Any
57
+ relays: list[str]
58
+ grants: grants_mod.GrantStore
59
+ clock: Callable[[], int]
60
+ timeout: float
61
+ host: PeeringHost
62
+ log: st.Ring[tuple[Any, ...]]
63
+ on_grant_issued: list[Callable[[str, str, str, Optional[str]], Any]]
64
+ on_peered: list[Callable[[str, str, Optional[str]], Any]]
65
+
66
+ # core.py
67
+ def _flush_envelopes(self) -> dict[str, dict[str, tuple[bool, str]]]: ...
68
+ def _fetch_envelope(self, envelope_id: Optional[str], issuer_pk: str) -> NostrEvent: ...
69
+ def _refuse(self, kind: str, why: str, subject: Optional[str] = None,
70
+ detail: Optional[Mapping[str, Any]] = None) -> Any: ...
71
+ def _take_received(self, envelope_id: Optional[str], issuer_pk: str,
72
+ envelope_event: Optional[Mapping[str, Any]] = None) -> GrantPayload: ...
73
+
74
+ # handshake.py
75
+ def handle(self, inbound: Any) -> Optional[tuple[Any, ...]]: ...
76
+ def handle_discover(self, inbound: Any) -> Optional[tuple[Any, ...]]: ...
77
+ def handle_propose(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
78
+ def handle_accept(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
79
+ def handle_amend(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
80
+ def handle_terminate(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
@@ -0,0 +1,39 @@
1
+ """Types and records for the Peering compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional, TypedDict
5
+
6
+ from fleetmesh_core.wire.interfaces import GrantPayload, NostrEvent
7
+
8
+
9
+ class Issued(TypedDict):
10
+ """What `Peering.issue` hands back: the signed kind 30801, the grant it
11
+ wraps, what each relay said to it, and why it was written."""
12
+ envelope: NostrEvent
13
+ payload: GrantPayload
14
+ published: dict[str, dict[str, tuple[bool, str]]]
15
+ cause: str
16
+
17
+
18
+ class PeeringDecision(TypedDict, total=False):
19
+ """Result of deciding a grant pending next epoch boundary."""
20
+ payload: Mapping[str, Any]
21
+ effective_from: int
22
+
23
+
24
+ class PeeringStateReport(TypedDict, total=False):
25
+ """The report Peering.state() gives of itself."""
26
+ peers: int
27
+ reconciled: dict[str, int]
28
+ refused: int
29
+ digest_mismatch: dict[str, str]
30
+
31
+
32
+ class PeeringResultDetail(TypedDict, total=False):
33
+ """Result of running propose against a peer."""
34
+ envelope: Optional[NostrEvent]
35
+ payload: Optional[GrantPayload]
36
+ published: dict[str, dict[str, tuple[bool, str]]]
37
+ cause: str
38
+ selected_transport: Optional[str]
39
+ reason: Optional[str]
@@ -0,0 +1,42 @@
1
+ """Reports and standing over the wire.
2
+
3
+ One rule shapes all of this and is worth stating first: a report a reader will
4
+ discard must not be published. The specification makes a kind 30802 admissible
5
+ only if every evidence tag resolves to an event the subject itself signed, and
6
+ readers count an inadmissible report as unsupported against the reporter. So
7
+ publish() checks its own report against fleetmesh_core.peering.reports.admissible() before it goes
8
+ out, and keeps it local if it fails. See node/README.md for what that rules
9
+ out today, which is most of it.
10
+
11
+ Findings come from two places: a window the budget evaluated, and a session
12
+ the sync layer ran. Each is a verdict of fleetmesh_core/'s arithmetic against one peer
13
+ under one grant this node issued. What happens next is the specification's
14
+ complaint rule, executed: S1 and S2 are presented to the subject as a
15
+ complaint/1.0/notice and published only after a window has passed; S3 and S4
16
+ are published at once. The report is the kind 30802 fleetmesh_core.peering.reports.build()
17
+ makes, signed by this node and put on the relays with its evidence.
18
+
19
+ The other side of the same coin is reading: fetch the reports about a node,
20
+ keep the admissible ones, weight each by what this node itself knows of the
21
+ reporter, and fold them into a view with fleetmesh_core.accounting.window. A reader that holds no
22
+ grant to the reporter weights it at zero, so a stranger's report moves
23
+ nothing.
24
+
25
+ standing/1.0 rides here too: an ask goes out through the transport, a tell
26
+ comes back from the peer's ledger, and inbound asks are answered from ours
27
+ only about nodes we hold a grant to.
28
+
29
+ Split the way node/budget/ and node/compute/ are: core.py holds construction
30
+ and the Compartment contract, findings.py/publish.py/reading.py are the mixins
31
+ for each concern, and shape.py is the structural Protocol the checker types
32
+ `self` against inside each mixin.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ from .constants import FINDING_WINDOWS, KEEP_UNPUBLISHABLE, LIMIT_METRICS, PUBLISH_TRIES, REPORT_TTL
37
+ from .core import Reporter
38
+ from .finding import Finding
39
+
40
+ __all__ = [
41
+ 'FINDING_WINDOWS', 'KEEP_UNPUBLISHABLE', 'LIMIT_METRICS', 'PUBLISH_TRIES', 'REPORT_TTL', 'Reporter', 'Finding',
42
+ ]
@@ -0,0 +1,29 @@
1
+ """The reporter's fixed numbers, in one place so the mixins and core import
2
+ them without importing each other.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ REPORT_TTL: int = 30 * 86400
7
+ # A finding that never became a report is dropped when the accounting under it
8
+ # is gone. `Budget.KEEP_WINDOWS` is how long a window's receipts, meters and
9
+ # evaluation are kept, and past that this node cannot substantiate a finding
10
+ # about that window to anyone, including itself. The same number, stated here
11
+ # rather than imported, because budget imports nothing from this module and the
12
+ # dependency must not start running the other way.
13
+ FINDING_WINDOWS: int = 24 * 7
14
+ # How many times a finding that could not be published is tried again, and how
15
+ # many of those refusals are kept for the operator to read.
16
+ PUBLISH_TRIES: int = 3
17
+ KEEP_UNPUBLISHABLE: int = 200
18
+ # What the reporter keeps of what it signed: the newest KEEP_SIGNED of each
19
+ # kind (30802 reports, 30385 relay scores, 1985 labels). The relays hold them;
20
+ # these copies are for `state` and the operator. KEEP_THREADS complaint
21
+ # threads of each side, the bound notices_in was cut to by hand. KEEP_LOG of
22
+ # the log, as many as the node's own.
23
+ KEEP_SIGNED: int = 256
24
+ KEEP_THREADS: int = 1000
25
+ KEEP_LOG: int = 500
26
+ LIMIT_METRICS: tuple[str, ...] = (
27
+ 'conn_rate', 'req', 'events_in', 'bytes_in', 'bytes_out',
28
+ 'blob_bytes', 'sync_ranges', 'errors', 'dup_ratio'
29
+ )