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,342 @@
1
+ """The node's own ceiling, held across ticks, and the shed ladder it drives.
2
+
3
+ `fleetmesh_core/capacity.py` is the arithmetic and states why there is no default total.
4
+ This is the part that remembers: which rungs are engaged and since when, what a
5
+ grant's limits were before a shed so they can be handed back, and the largest
6
+ window this machine has actually carried so an operator has a number to start
7
+ from rather than a blank.
8
+
9
+ The total is read from config under `capacity` and is live. An operator may set
10
+ it, raise it or delete it while the node runs and the next tick follows the
11
+ file, which is the same contract every other live key has: the file is the
12
+ interface and the daemon follows it.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Callable, Mapping, Optional, cast
17
+
18
+ from fleetmesh_core.accounting import capacity as ref_capacity, window
19
+ from node import channels as ch_mod
20
+ from node.storage import grants as GRANTS
21
+ from node.compartment import lifecycle as lifecycle_mod
22
+ from node import metrics as mx_mod
23
+ from node.compartment import stores as st
24
+ from node.config import CapacityConfig
25
+ from node.compartment import Command, Compartment, Need
26
+
27
+
28
+ from .types import RungChange, CapacityStateReport
29
+
30
+
31
+ class Capacity(Compartment):
32
+ requires: tuple[str, ...] = ('budget',)
33
+ """The node's own ceiling, as a compartment the operator can watch.
34
+
35
+ What it declares: the config key `capacity`, live; one control command; a
36
+ state an operator reads; the trouble it is in; and every peering it turned
37
+ away, with the reason, in the refusal channel every compartment has. Before
38
+ that channel existed a full node declined a stranger and recorded nothing,
39
+ so the only trace of the first shed rung was a line in somebody's journal.
40
+ """
41
+
42
+ name: str = 'capacity'
43
+ reads: tuple[str, ...] = ('capacity',)
44
+ config_cls = CapacityConfig
45
+ live: tuple[str, ...] = ('capacity',)
46
+ NEEDS = (
47
+ Need('budget'),
48
+ Need('total', block=CapacityConfig, field='total'),
49
+ Need('grants', 'stores.grants'),
50
+ Need('amend', 'amend'),
51
+ )
52
+ COMMANDS = (
53
+ Command(
54
+ key='show',
55
+ name='Capacity',
56
+ description="the declared total, the pressure on it, the rungs engaged and the largest window carried",
57
+ tags=('capacity', 'status'),
58
+ permission='capacity/show',
59
+ run='show',
60
+ reply='{declared, total, pressure, binding, rungs, held, since, measured, peak, suggested, note}',
61
+ ),
62
+ )
63
+
64
+ # What it keeps. The ladder's own state (`held`, `since`) is a handful of
65
+ # rung names and is not a store: it does not grow with the outside world.
66
+ STORES = (
67
+ st.Store(key='log', description='rungs engaged and released, newest last',
68
+ kind=st.RING, shape=RungChange, bound=200),
69
+ # REFUSE, not OLDEST: dropping a peer's remembered limits would leave
70
+ # it shed for good when the pressure lifts. A peer that cannot be
71
+ # remembered is not cut instead (shed_grants).
72
+ #
73
+ # SNAPSHOT: a restart in the middle of a shed came back with an empty
74
+ # ladder and an empty table - the peers stayed cut, and nothing knew
75
+ # what to give back. Carried, it pairs with the reconcile in tick():
76
+ # whatever is still held back while amend_down is off is returned.
77
+ st.Store(key='before', description='the limits each peer held before a shed, to give back',
78
+ kind=st.TABLE, shape=dict, bound=4096, evict=st.REFUSE, keyed_by='peer',
79
+ persist=st.SNAPSHOT),
80
+ # Carried across a restart: "the largest window this machine has
81
+ # actually carried" is the number an operator sizes a total from,
82
+ # and an upgrade used to reset it to nothing.
83
+ st.Store(key='peak', description='the largest window carried, per counter',
84
+ kind=st.TABLE, shape=int, bound=64, evict=st.REFUSE, keyed_by='counter',
85
+ persist=st.SNAPSHOT),
86
+ )
87
+ CHANNELS = (
88
+ ch_mod.Channel('rung_engaged', 'A capacity shed ladder rung engaged under pressure',
89
+ level=ch_mod.TROUBLE, say='capacity shed rung {rung} engaged at {pressure}% pressure on {binding}',
90
+ tags=('capacity', 'shed')),
91
+ ch_mod.Channel('rung_released', 'A capacity shed ladder rung released as pressure eased',
92
+ level=ch_mod.NOTE, say='capacity shed rung {rung} released',
93
+ tags=('capacity', 'shed')),
94
+ )
95
+ METRICS = (
96
+ mx_mod.Metric(name='capacity_rungs_engaged', description='Times a capacity shed rung engaged.',
97
+ kind=mx_mod.COUNTER, counts='rung_engaged', tags=('capacity', 'shed')),
98
+ mx_mod.Metric(name='capacity_rungs_released', description='Times a capacity shed rung released.',
99
+ kind=mx_mod.COUNTER, counts='rung_released', tags=('capacity', 'shed')),
100
+ )
101
+ # v1 is `peak` and `before`. `before` joined v1 before v1 ever left this
102
+ # repository, so no deployed snapshot has the shape without it; a local
103
+ # one that does is refused by the restore ("no store before") and the
104
+ # compartment starts fresh, which is the honest outcome.
105
+ LIFECYCLE = lifecycle_mod.Lifecycle(snapshot=1)
106
+
107
+ def __init__(
108
+ self,
109
+ budget: Any,
110
+ total: Optional[Mapping[str, float | int]] = None,
111
+ clock: Optional[Callable[[], int]] = None,
112
+ node: Optional[Any] = None,
113
+ grants: Optional[Any] = None,
114
+ amend: Optional[Callable[..., tuple[Any, Any]]] = None,
115
+ ) -> None:
116
+ super().__init__(node, clock)
117
+ self.budget: Any = budget
118
+ # The grant store a shed reads live grants from and the node call
119
+ # that amends one; a fake node in a suite supplies both.
120
+ self.grants: Optional[Any] = grants if grants is not None else getattr(node, 'grants', None)
121
+ self.amend: Callable[..., tuple[Any, Any]] = amend or (lambda *a: getattr(node, '_amend')(*a))
122
+ self.total: dict[str, float | int] = dict(total or {}) # not a store: the operator's declared ceilings, from config
123
+ self.held: list[str] = [] # not a store: the rungs engaged, at most the four of the ladder
124
+ self.since: dict[str, int] = {} # not a store: when each held rung engaged, one per rung
125
+ # The stores (`log`, `before`, `peak`) were built and bound by the base.
126
+ self.log: st.Ring[RungChange]
127
+ self.before: st.Table[str, Mapping[str, Any]]
128
+ self.peak: st.Table[str, int]
129
+
130
+ # ---- the number the operator declares -------------------------------------
131
+ def set_total(self, total: Optional[Mapping[str, Any]]) -> bool:
132
+ """Take up a new declaration. Returns True if it changed."""
133
+ new = {k: v for k, v in (total or {}).items() if isinstance(v, (int, float)) and v > 0}
134
+ if new == self.total:
135
+ return False
136
+ self.total = new
137
+ return True
138
+
139
+ def declared(self) -> bool:
140
+ return bool(self.total)
141
+
142
+ # ---- what is happening ----------------------------------------------------
143
+ def measure(self, now: Optional[int] = None) -> dict[str, int]:
144
+ """The open window, summed across every peer. See Budget.aggregate."""
145
+ m: dict[str, int] = self.budget.aggregate(now)
146
+ for k, v in m.items():
147
+ if v > self.peak.get(k, 0):
148
+ self.peak[k] = v
149
+ return m
150
+
151
+ def pressure(self, now: Optional[int] = None) -> tuple[float, Optional[str]]:
152
+ return ref_capacity.pressure(self.measure(now), self.total)
153
+
154
+ def transitions(self, now: Optional[int] = None) -> list[tuple[str, str]]:
155
+ """Recompute and return the rungs that changed, as (action, what).
156
+
157
+ The caller acts on the transitions rather than on the state, because a
158
+ shed and a restore each cost the peers an amend and neither should
159
+ happen twice for one crossing.
160
+ """
161
+ now_val = now if now is not None else self.clock()
162
+ if not self.declared():
163
+ was, self.held = self.held, []
164
+ return [(a, 'released') for a in was]
165
+ p, _binding = self.pressure(now_val)
166
+ want = ref_capacity.engaged(p, self.held)
167
+ changed = ([(a, 'engaged') for a in want if a not in self.held]
168
+ + [(a, 'released') for a in self.held if a not in want])
169
+ for action, what in changed:
170
+ self.log.append({'at': now_val, 'action': action, 'what': what, 'pressure': round(p, 4)})
171
+ if what == 'engaged':
172
+ self.since[action] = now_val
173
+ self.say('rung_engaged', rung=action, pressure=round(p * 100), binding=_binding or 'capacity')
174
+ else:
175
+ self.since.pop(action, None)
176
+ self.say('rung_released', rung=action)
177
+ self.held = want
178
+ return changed
179
+
180
+ # ---- what each rung means to a caller -------------------------------------
181
+ def refusing(self) -> bool:
182
+ return 'refuse_peerings' in self.held
183
+
184
+ def narrowing(self) -> bool:
185
+ return 'narrow_scope' in self.held
186
+
187
+ def amending(self) -> bool:
188
+ return 'amend_down' in self.held
189
+
190
+ def leafing(self) -> bool:
191
+ return 'leaf_behaviour' in self.held
192
+
193
+ def why(self) -> str:
194
+ """The message a decline carries. Names the binding counter and nothing
195
+ else: a stranger learns this node is full, never who is filling it."""
196
+ p, binding = self.pressure()
197
+ return 'at %d%% of this node\'s declared %s' % (round(p * 100), binding or 'capacity')
198
+
199
+ # ---- the limits a shed took, so they can be given back ---------------------
200
+ def remember(self, peer: str, limits: Mapping[str, Any]) -> bool:
201
+ """Keep what a shed is about to take. False when the table is full:
202
+ the caller must not take what it could not promise to give back."""
203
+ if peer in self.before:
204
+ return True
205
+ return self.before.put(peer, dict(limits))
206
+
207
+ def restore_for(self, peer: str, tier: str) -> Optional[Mapping[str, Any]]:
208
+ """What this peer should get back, or None if nothing was taken.
209
+
210
+ Held inside the ceilings of the tier it holds NOW, because the ladder
211
+ may have lowered it while the shed was on and a restore must never undo
212
+ a remedy.
213
+ """
214
+ was = self.before.pop(peer, None)
215
+ if was is None:
216
+ return None
217
+ return window.within(was, tier)
218
+
219
+ def forget(self, peer: str) -> None:
220
+ self.before.pop(peer, None)
221
+
222
+ # ---- the compartment contract -------------------------------------------------
223
+ def tick(self, now: int) -> list[tuple[Any, ...]]:
224
+ """Recompute pressure and act on the rungs that changed.
225
+
226
+ Transitions and not state: a shed and a restore each cost the peers an
227
+ amend, and neither should happen twice for one crossing of a threshold.
228
+
229
+ Then one invariant, checked every tick rather than on a transition:
230
+ nothing is held back while amend_down is off. The transition alone
231
+ misses the restart - the ladder comes back empty, so a shed that was
232
+ on is never seen to release, and `before` (carried) would sit there
233
+ with the peers still cut. Normally `before` is empty here and this is
234
+ one length check.
235
+ """
236
+ self.set_total(self.settings().total) # live
237
+ did: list[tuple[Any, ...]] = []
238
+ for action, what in self.transitions(now):
239
+ p, binding = self.pressure(now)
240
+ did.append(('capacity', action, what, round(p, 3), binding))
241
+ if action == 'amend_down' and what == 'engaged':
242
+ did += self.shed_grants(now)
243
+ elif action == 'amend_down' and what == 'released':
244
+ did += self.restore_grants(now)
245
+ if self.before and not self.amending():
246
+ did += self.restore_grants(now)
247
+ return did
248
+
249
+ def trouble(self) -> list[str]:
250
+ if not self.held:
251
+ return []
252
+ p, binding = self.pressure()
253
+ return ['shedding at %d%% of the declared %s: %s'
254
+ % (round(p * 100), binding or 'total', ', '.join(self.held))]
255
+
256
+
257
+ def refuse_peering(self, peer: str) -> str:
258
+ """A stranger turned away because this node is full.
259
+
260
+ Recorded rather than only answered: a decline is not a finding, but a
261
+ node that is refusing peerings and cannot say how many is a node whose
262
+ operator finds out from somebody else.
263
+ """
264
+ self.refusals.refuse('peering', 'this node is at its declared ceiling',
265
+ subject=peer, detail={'pressure': self.why()}, at=self.clock())
266
+ return self.why()
267
+
268
+ # ---- the grants a shed moves ------------------------------------------------
269
+ def shed_grants(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
270
+ """Reduce every live grant because this node is full, not because anyone
271
+ misbehaved. What each peer held is remembered so it can be given back."""
272
+ now_val = now if now is not None else self.clock()
273
+ floor = ref_capacity.shed_floor(window.ceilings('probation'))
274
+ did: list[tuple[Any, ...]] = []
275
+ if self.grants is None:
276
+ return did # no grant store: nothing to shed
277
+ for peer in self.grants.peers():
278
+ row = self.grants.get(peer, GRANTS.ISSUED)
279
+ if row is None or row['status'] != 'live':
280
+ continue
281
+ if peer in self.before:
282
+ # Shed and not yet given back. Only a restart reaches this -
283
+ # the ladder came back empty, `before` came back carried, and
284
+ # amend_down engaged again - and cutting the already-cut
285
+ # limits would squeeze the peer twice for one shed.
286
+ continue
287
+ limits = row['payload'].get('limits') or {}
288
+ cut = ref_capacity.reduce(limits, floor)
289
+ if cut == limits:
290
+ continue # already on the floor; nothing to take
291
+ if not self.remember(peer, limits):
292
+ self.refusals.refuse('grant', 'not reduced: no room to remember what it held',
293
+ subject=peer, at=now_val)
294
+ continue
295
+ env, _ = self.amend(peer, dict(row['payload'], limits=cut),
296
+ 'this node is shedding load', 'capacity', now_val)
297
+ self.refusals.refuse('grant', 'reduced because this node is full',
298
+ subject=peer, detail={'was': limits.get('events_in'),
299
+ 'now': cut.get('events_in')}, at=now_val)
300
+ did.append(('shed', peer, env['id']))
301
+ return did
302
+
303
+ def restore_grants(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
304
+ """Give back what a shed took, held inside the tier each peer holds now."""
305
+ now_val = now if now is not None else self.clock()
306
+ did: list[tuple[Any, ...]] = []
307
+ if self.grants is None:
308
+ return did
309
+ for peer in list(self.before):
310
+ row = self.grants.get(peer, GRANTS.ISSUED)
311
+ if row is None or row['status'] != 'live':
312
+ self.forget(peer)
313
+ continue
314
+ back = self.restore_for(peer, row['payload'].get('tier', 'probation'))
315
+ if back is None or back == (row['payload'].get('limits') or {}):
316
+ continue
317
+ env, _ = self.amend(peer, dict(row['payload'], limits=back),
318
+ 'this node has room again', 'capacity', now_val)
319
+ did.append(('restored', peer, env['id']))
320
+ return did
321
+
322
+ # ---- the operator's view ---------------------------------------------------
323
+ def show(self) -> Mapping[str, Any]:
324
+ st = self.state()
325
+ st['suggested'] = self.suggested()
326
+ return st
327
+
328
+ def suggested(self) -> Mapping[str, Any]:
329
+ return ref_capacity.suggest([dict(self.peak)]) if self.peak else {}
330
+
331
+ def state(self, now: Optional[int] = None) -> CapacityStateReport:
332
+ # The ref summary is a closed TypedDict; the operator's view adds the
333
+ # node-side memory on top, so widen it to a plain dict before extending.
334
+ s = dict(ref_capacity.summary(self.measure(now), self.total))
335
+ s['total'] = dict(self.total)
336
+ s['held'] = list(self.held)
337
+ s['since'] = {a: self.since[a] for a in self.held if a in self.since}
338
+ s['peak'] = dict(self.peak)
339
+ if not s['declared']:
340
+ s['note'] = ('no aggregate ceiling is declared, so this node will accept load '
341
+ 'until it fails; meshnode capacity prints what it has carried')
342
+ return cast(CapacityStateReport, s)
@@ -0,0 +1,24 @@
1
+ """Types and records for the Capacity compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional, TypedDict
5
+
6
+
7
+ class RungChange(TypedDict):
8
+ """One rung of the shed ladder engaging or releasing."""
9
+ at: int
10
+ action: str
11
+ what: str # 'engaged' | 'released'
12
+ pressure: float
13
+
14
+
15
+ class CapacityStateReport(TypedDict, total=False):
16
+ declared: bool
17
+ pressure: float
18
+ binding: Optional[str]
19
+ total: dict[str, int]
20
+ held: list[str]
21
+ since: dict[str, int]
22
+ peak: dict[str, int]
23
+ suggested: Mapping[str, Any]
24
+ note: str
@@ -0,0 +1,25 @@
1
+ """peering/1.0 over the wire.
2
+
3
+ fleetmesh_core/peering.py decides everything: what to propose, whether to admit, what
4
+ tier to open at, what the grant says. This module gives it what the
5
+ reference deliberately lacks. The kind 30801 gets a real nip44/2 ciphertext
6
+ of the grant to the subject and a signature, and is published. The messages
7
+ travel through the transport. The grants land in SQLite. And the subject,
8
+ having received an accept or an offer, fetches the envelope by id, opens it
9
+ with the conversation key only the pair shares, and checks that the
10
+ commitment in the public tags opens to what it decrypted, so the issuer
11
+ cannot later claim to have granted something else.
12
+
13
+ A package by seam: envelope.py (the kind 30801, written and opened),
14
+ core.py (the compartment: grant writers, reconciler, contract),
15
+ handshake.py (propose / handle / serve). Every name the module exported is
16
+ re-exported here.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from .core import Peering
21
+ from .shape import PeeringHost
22
+ from .envelope import CAUSES, GRANT_KIND, RECONCILE_EVERY, GrantUnpublished, PeeringError, WireNode, open_envelope
23
+
24
+ __all__ = ['CAUSES', 'GRANT_KIND', 'RECONCILE_EVERY', 'GrantUnpublished', 'PeeringError', 'WireNode',
25
+ 'open_envelope', 'Peering', 'PeeringHost']