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,563 @@
1
+ """One declaration per setting, and one rule for which layer wins.
2
+
3
+ A `Block` states a compartment's config as a frozen dataclass. That works, but
4
+ each setting's knowledge ends up in four places: the docstring says what it
5
+ means, the field says its type and default, `check` says what is wrong with a
6
+ bad value, and `from_dict` repeats the default while coercing. `UpdatesConfig`
7
+ wrote `DEFAULT_CHECK_EVERY_SECONDS` twice for exactly that reason. Four places
8
+ agreeing by hand is how a control comes to report something the node does not
9
+ do, which is a defect this tree has now met several times.
10
+
11
+ A `Parameter` is that knowledge said once. `ParamBlock` derives the other
12
+ three from it, so a compartment declares and does not re-implement.
13
+
14
+ It also buys what no amount of dataclass fields could: the node can be *asked*
15
+ what it is configurable by. `every_parameter()` enumerates the lot with types,
16
+ defaults, tags and whether a reload applies them, which is what makes uniform
17
+ defaults something you can check rather than hope for.
18
+
19
+ ## Escalation
20
+
21
+ Highest wins, and the order is stated here rather than per call site - before
22
+ this, exactly one setting in the node had a documented precedence
23
+ (`app/config.py:_blob_storage_cap`, env then file then manifest), so an
24
+ operator had nothing general to rely on. That chain is preserved: FILE beats
25
+ MANIFEST because a manifest is compiled *into* the file by
26
+ `config_from_manifests`, so the file is the later word.
27
+
28
+ OVERRIDE set live over the control socket; only for `live` parameters
29
+ ARG a command line argument
30
+ ENV an environment variable, when the parameter names one
31
+ FILE config.json, which is the operator's own file
32
+ MANIFEST compiled from a meshctl manifest
33
+ DEFAULT the parameter's own default
34
+
35
+ ## Field names
36
+
37
+ The design was given in a schema; these are its Python spellings.
38
+
39
+ key key
40
+ name name
41
+ description description
42
+ tags tags
43
+ type type (plus `item` for what a list or mapping holds)
44
+ valueRequired required
45
+ defaultValue default
46
+ optionalValues choices - the permitted set, named for what it is
47
+ verifyRegex verify_regex
48
+ liveToggle live
49
+
50
+ Two things were added. `minimum` and `maximum`, because a bound is part of
51
+ what most of this tree's integers are - `non_negative_int` and `positive_int`
52
+ are among base.py's most used predicates, and a `must` clause promising
53
+ "non-negative" with nothing enforcing it is a sentence an operator never gets
54
+ to read. And `must` itself, which is not a duplicate of `description`:
55
+ the description is what the setting means, `must` is what a wrong value is
56
+ told. "seconds between checks; 0 = only on command" against "be a
57
+ non-negative integer of seconds". Deriving the second from the type would
58
+ produce worse English than the sentences operators already read.
59
+ """
60
+ from __future__ import annotations
61
+
62
+ import dataclasses
63
+ import re
64
+ from typing import Any, Callable, Generic, Mapping, Optional, Sequence, Type, TypedDict, TypeVar, Union
65
+
66
+ from .base import Block, Problems
67
+
68
+ T = TypeVar('T')
69
+
70
+ # Precedence, highest first. The index is the precedence, so a layer added
71
+ # later must be inserted where it belongs rather than appended.
72
+ OVERRIDE, ARG, ENV, FILE, MANIFEST, DEFAULT = 'override', 'arg', 'env', 'file', 'manifest', 'default'
73
+ SOURCES: tuple[str, ...] = (OVERRIDE, ARG, ENV, FILE, MANIFEST, DEFAULT)
74
+
75
+ # Which half of a parameter's access statement the node acts on today.
76
+ #
77
+ # `sources` is enforced: `resolve` skips a layer the parameter does not
78
+ # permit and `check` refuses such a value by name.
79
+ #
80
+ # `requires` is DECLARED ONLY. Nothing authenticates a principal yet - the
81
+ # control socket is chmod 0o600 and all or nothing, with no peer credential
82
+ # read and no token - so a parameter naming an ability is stating an intent,
83
+ # not defending itself. Shipping it as though it were a defence would be a
84
+ # control reporting a protection that does not exist, which is worse than no
85
+ # field at all because it reads as security. Same footing, and for the same
86
+ # reason, as `BaseStore.DURABILITY`. `every_parameter` reports which is which
87
+ # so the two cannot be confused, and enforcement arrives with an
88
+ # authenticated principal on the socket.
89
+ ACCESS_ENFORCED: tuple[str, ...] = ('sources',)
90
+ ACCESS_DECLARED_ONLY: tuple[str, ...] = ('requires',)
91
+
92
+
93
+ class _Stated(TypedDict):
94
+ """The part of a parameter that is always reported."""
95
+ key: str
96
+ name: str
97
+ description: str
98
+ type: str
99
+ default: Any
100
+ required: bool
101
+ live: bool
102
+ tags: list[str]
103
+
104
+
105
+ class ParameterRecord(_Stated, total=False):
106
+ """A parameter as `meshnode config` and the docs read it.
107
+
108
+ Named records rather than loose mappings, for the reason check-dicts.py
109
+ enforces it: a row an operator reads by field name is the case that rule
110
+ exists for. The optional half is present only when the parameter states
111
+ it, so a report does not show an empty pattern or a bound that is not
112
+ there.
113
+ """
114
+ choices: list[Any]
115
+ verify_regex: str
116
+ minimum: Union[int, float]
117
+ maximum: Union[int, float]
118
+ env: str
119
+ secret: bool
120
+ requires: str
121
+ requires_enforced: bool
122
+ sources: list[str]
123
+ resource: str
124
+ short: str
125
+ cli_name: str
126
+ positional: bool
127
+ nargs: str
128
+
129
+
130
+ class ParameterRow(ParameterRecord, total=False):
131
+ """A `ParameterRecord` with the block it belongs to, for the flat list."""
132
+ block: str
133
+
134
+
135
+ class BlockRecord(TypedDict):
136
+ """One block, and every parameter it declares."""
137
+ key: str
138
+ label: str
139
+ parameters: list[ParameterRecord]
140
+
141
+
142
+ @dataclasses.dataclass(frozen=True)
143
+ class Parameter(Generic[T]):
144
+ """Everything true about one setting, in one place.
145
+
146
+ `validate` is the escape hatch, and it is deliberate. A regex and a set of
147
+ choices describe a scalar or a list of scalars completely; they cannot
148
+ describe `updates.policy`, which is a mapping whose *values* are an enum
149
+ and which has a special sentence for a policy that does not exist yet. A
150
+ parameter that cannot state its own rule says so by carrying the callable
151
+ that can, and stays the single declaration either way - which is the point,
152
+ because a setting this file does not know about is a setting
153
+ `every_parameter()` cannot show an operator.
154
+ """
155
+ key: str
156
+ name: str
157
+ description: str
158
+ # `typing.Type` rather than the builtin: the field below is named `type`,
159
+ # which shadows the builtin everywhere else in this class body, so an
160
+ # annotation written `type[...]` would resolve to the field. This keeps
161
+ # the schema's own name for it.
162
+ type: Type[Any]
163
+ # A value, or a factory for one. A mutable default needs the factory, and
164
+ # `release_key` derives its default from the running release. `value()`
165
+ # resolves either, so nothing else has to know which it is.
166
+ default: Union[T, Callable[[], T]]
167
+ must: str = ''
168
+ tags: tuple[str, ...] = ()
169
+ required: bool = False
170
+ choices: tuple[Any, ...] = ()
171
+ verify_regex: str = ''
172
+ live: bool = False
173
+ item: Optional[Type[Any]] = None
174
+ # Access. `requires` is the UCAN ability a delegation must carry to set
175
+ # this - UCAN's `can`, a verb and not a role, so a role registry no mesh
176
+ # can agree on is never needed. Empty means operator only and is the
177
+ # default on purpose: remote settability is opted into per parameter and
178
+ # is never inherited, so a parameter added without thinking about access
179
+ # is closed rather than open.
180
+ # The dataclass field this lands on, when it is not the file's own key.
181
+ # The MeshNode manifest writes `maxBytes` and `allowPrivateMirror`; the
182
+ # block holds `max_bytes` and `allow_private_mirror`. The file's spelling
183
+ # is the key, because that is what an operator types.
184
+ field: str = ''
185
+ # A credential, so a report must not print it. `settlement.nwc_uri` is a
186
+ # wallet connection string. The lease refusal in node/storage/base.py put
187
+ # a database password in an operator's log earlier today for want of
188
+ # exactly this distinction, so it is stated rather than remembered.
189
+ secret: bool = False
190
+ requires: str = ''
191
+ # Which escalation layers may set it. Empty means all of them. This is
192
+ # the half an ability string cannot state: the six layers are six
193
+ # different principals, so "host only, never over the wire" is said here
194
+ # and nowhere else.
195
+ sources: tuple[str, ...] = ()
196
+ minimum: Optional[Union[int, float]] = None
197
+ maximum: Optional[Union[int, float]] = None
198
+ env: str = ''
199
+ validate: Optional[Callable[[str, Any, Problems], None]] = None
200
+ coerce: Optional[Callable[[Any], Any]] = None
201
+ short: str = ''
202
+ cli_name: str = ''
203
+ positional: bool = False
204
+ nargs: str = ''
205
+ cli: bool = True
206
+
207
+ @property
208
+ def attr(self) -> str:
209
+ """The dataclass field name: the key unless an alias was given."""
210
+ return self.field or self.key
211
+
212
+ def value(self) -> Any:
213
+ """The default, with a factory called. One place knows which it is."""
214
+ d: Any = self.default
215
+ return d() if callable(d) else d
216
+
217
+ def resource(self, label: str, node_did: str = '') -> str:
218
+ """The UCAN resource this parameter is, as `with` would name it.
219
+
220
+ Derived rather than declared: 151 hand-written resource strings would
221
+ be 151 chances to name the wrong one, and the name is already implied
222
+ by the block and the key.
223
+ """
224
+ path = 'config/%s.%s' % (label, self.key)
225
+ return '%s#%s' % (node_did, path) if node_did else path
226
+
227
+ def settable_by(self, source: str) -> bool:
228
+ """Whether `source` is a layer allowed to set this parameter.
229
+
230
+ A data rule, decided by what the parameter declares about layers -
231
+ not by who is asking, which is a question this class must never
232
+ answer. An empty `sources` permits every layer, and DEFAULT is always
233
+ permitted because it is not a layer anyone sets.
234
+ """
235
+ if source == DEFAULT or not self.sources:
236
+ return True
237
+ return source in self.sources
238
+
239
+ def sentence(self, label: str) -> str:
240
+ """What an operator is told when the value is wrong."""
241
+ return '%s.%s must %s' % (label, self.key, self.must)
242
+
243
+ def ok(self, value: Any) -> bool:
244
+ """Whether `value` satisfies what this parameter states about itself.
245
+
246
+ Only the stated things: type, the element type, the permitted set and
247
+ the regex. A parameter carrying `validate` is not judged here.
248
+ """
249
+ if self.type is bool:
250
+ # bool before int, because bool is a subclass of int and `true`
251
+ # is not an acceptable integer anywhere in this file.
252
+ if not isinstance(value, bool):
253
+ return False
254
+ elif self.type is int:
255
+ if not isinstance(value, int) or isinstance(value, bool):
256
+ return False
257
+ # The bound is part of the type for most of this tree's integers:
258
+ # `non_negative_int` and `positive_int` are two of base.py's most
259
+ # used predicates, and a `must` promising "non-negative" that
260
+ # nothing enforces is a sentence an operator never sees.
261
+ if self.minimum is not None and value < self.minimum:
262
+ return False
263
+ if self.maximum is not None and value > self.maximum:
264
+ return False
265
+ elif self.type in (tuple, list):
266
+ if not isinstance(value, (list, tuple)):
267
+ return False
268
+ if self.item is not None and not all(_is(v, self.item) for v in value):
269
+ return False
270
+ if self.choices and not all(v in self.choices for v in value):
271
+ return False
272
+ if self.verify_regex and not all(
273
+ isinstance(v, str) and re.fullmatch(self.verify_regex, v) for v in value):
274
+ return False
275
+ return True
276
+ elif self.type is dict:
277
+ if not isinstance(value, Mapping):
278
+ return False
279
+ elif not isinstance(value, self.type):
280
+ return False
281
+ if self.choices and value not in self.choices:
282
+ return False
283
+ if self.verify_regex:
284
+ if not isinstance(value, str) or not re.fullmatch(self.verify_regex, value):
285
+ return False
286
+ return True
287
+
288
+ def read(self, raw: Mapping[str, Any]) -> Any:
289
+ """This parameter's value out of a raw config object.
290
+
291
+ An absent key and an explicit `null` both mean "not said", so both
292
+ take the default. `coerce` is what turns what the file is allowed to
293
+ say into what the compartment reads - a pubkey lowercased, a list
294
+ frozen to a tuple.
295
+ """
296
+ v = raw.get(self.key) if isinstance(raw, Mapping) else None
297
+ if v is None:
298
+ v = self.value()
299
+ elif self.coerce is not None:
300
+ v = self.coerce(v)
301
+ elif self.type is tuple and isinstance(v, (list, tuple)):
302
+ v = tuple(v)
303
+ return v
304
+
305
+ def describe(self) -> ParameterRecord:
306
+ """The parameter as data, for `meshnode config` and for the docs.
307
+
308
+ `default` goes through the factory when it is one, so what is shown is
309
+ a value an operator could paste rather than the word 'function'.
310
+ """
311
+ # A secret's default is not shown. The point of a report is that an
312
+ # operator can read it, and a credential printed into a terminal, a
313
+ # log or a paste is a credential disclosed.
314
+ shown: Any = '(secret)' if self.secret and self.value() else _jsonable(self.value())
315
+ out: ParameterRecord = {
316
+ 'key': self.key, 'name': self.name, 'description': self.description,
317
+ 'type': _type_name(self.type, self.item), 'default': shown,
318
+ 'required': self.required, 'live': self.live, 'tags': list(self.tags),
319
+ }
320
+ if self.secret:
321
+ out['secret'] = True
322
+ if self.choices:
323
+ out['choices'] = [_jsonable(c) for c in self.choices]
324
+ if self.verify_regex:
325
+ out['verify_regex'] = self.verify_regex
326
+ if self.minimum is not None:
327
+ out['minimum'] = self.minimum
328
+ if self.maximum is not None:
329
+ out['maximum'] = self.maximum
330
+ if self.env:
331
+ out['env'] = self.env
332
+ if self.requires:
333
+ out['requires'] = self.requires
334
+ # Stated on the row, not only in a footer, because a row is what
335
+ # an operator reads and a declared ability must not look enforced.
336
+ out['requires_enforced'] = False
337
+ if self.sources:
338
+ out['sources'] = list(self.sources)
339
+ if self.short:
340
+ out['short'] = self.short
341
+ if self.cli_name:
342
+ out['cli_name'] = self.cli_name
343
+ if self.positional:
344
+ out['positional'] = True
345
+ if self.nargs:
346
+ out['nargs'] = self.nargs
347
+ return out
348
+
349
+
350
+ def _is(v: Any, t: Type[Any]) -> bool:
351
+ if t is int:
352
+ return isinstance(v, int) and not isinstance(v, bool)
353
+ return isinstance(v, t)
354
+
355
+
356
+ def _type_name(t: Type[Any], item: Optional[Type[Any]]) -> str:
357
+ base = {int: 'int', str: 'str', bool: 'bool', dict: 'object',
358
+ tuple: 'list', list: 'list'}.get(t, getattr(t, '__name__', str(t)))
359
+ if item is not None and base == 'list':
360
+ return 'list[%s]' % _type_name(item, None)
361
+ return base
362
+
363
+
364
+ def _jsonable(v: Any) -> Any:
365
+ if isinstance(v, tuple):
366
+ return [_jsonable(x) for x in v]
367
+ if isinstance(v, Mapping):
368
+ return {k: _jsonable(x) for k, x in v.items()}
369
+ return v
370
+
371
+
372
+ @dataclasses.dataclass(frozen=True)
373
+ class Arg(Generic[T]):
374
+ """A resolved value, and where it came from.
375
+
376
+ `source` is not in the original schema and is the one thing added to it.
377
+ The purpose of this system is that an operator can see how the node is
378
+ configured, and "900" does not answer "why 900" - "900, from config.json"
379
+ does, and distinguishes a deliberate setting from a default nobody chose.
380
+ """
381
+ key: str
382
+ type: Type[Any]
383
+ value: T
384
+ source: str = DEFAULT
385
+
386
+ @property
387
+ def is_default(self) -> bool:
388
+ return self.source == DEFAULT
389
+
390
+
391
+ def resolve(param: Parameter[T], layers: Mapping[str, Any]) -> Arg[T]:
392
+ """The winning value for `param`, by the order at the top of this file.
393
+
394
+ A layer that does not mention the parameter is skipped, and so is an
395
+ explicit `null` - a file saying `null` is declining to set it, not setting
396
+ it to nothing. OVERRIDE is ignored for a parameter that is not `live`,
397
+ because applying one would be a change the node cannot honour until it
398
+ restarts, reported as though it had taken effect.
399
+
400
+ A layer the parameter does not permit is skipped too. That is enforcement,
401
+ and it is enforceable here because it reads only what the parameter
402
+ declares: no identity is involved, so no security decision is being taken
403
+ inside a class whose job is to describe data. `ParamBlock.check` refuses
404
+ such a value by name as well, because a setting silently doing nothing is
405
+ the failure this whole mechanism exists to remove.
406
+ """
407
+ for source in SOURCES:
408
+ if source == DEFAULT:
409
+ break
410
+ if source == OVERRIDE and not param.live:
411
+ continue
412
+ if not param.settable_by(source):
413
+ continue
414
+ layer = layers.get(source)
415
+ if not isinstance(layer, Mapping) or layer.get(param.key) is None:
416
+ continue
417
+ v = layer[param.key]
418
+ return Arg(param.key, param.type, param.coerce(v) if param.coerce else v, source)
419
+ return Arg(param.key, param.type, param.value(), DEFAULT)
420
+
421
+
422
+ class ParamBlock(Block):
423
+ """A `Block` whose `check` and `from_dict` come from its `PARAMS`.
424
+
425
+ A subclass declares `PARAMS` and the dataclass fields that hold them, and
426
+ writes neither method. Sentences come out in `PARAMS` order, which is why
427
+ the order is the order an operator reads in the file.
428
+
429
+ `SWITCHABLE` keeps the shorthand an optional compartment already has: the
430
+ block may be plain `true`, meaning on with every default.
431
+ """
432
+ PARAMS: tuple[Parameter[Any], ...] = ()
433
+ SWITCHABLE: bool = False
434
+ # The parameter that is true because the block is present at all.
435
+ #
436
+ # `p2p` and `lan` both declare `enabled: bool = False` and mean opposite
437
+ # things by it: p2p is on as soon as its key appears, lan is not. That
438
+ # distinction lived only in the body of `from_dict` - two blocks that
439
+ # look identical behaving differently - and a conversion that trusted the
440
+ # dataclass default would have silently turned the libp2p transport off
441
+ # on every node configuring it as an object. Said here instead.
442
+ PRESENCE_ENABLES: str = ''
443
+ # False when PARAMS are a declaration for the census and the methods stay
444
+ # hand-written, because the raw form is not an object of named settings:
445
+ # `capacity` IS the mapping, and carries no field names in the file at
446
+ # all. Stated rather than inferred, so a block that merely has not been
447
+ # converted cannot pass for a deliberate exception.
448
+ DERIVED: bool = True
449
+
450
+ @classmethod
451
+ def check(cls, raw: Any, p: Problems) -> None:
452
+ label = cls.label or cls.key
453
+ if cls.SWITCHABLE:
454
+ if isinstance(raw, bool):
455
+ return
456
+ if not isinstance(raw, Mapping):
457
+ p.add('%s must be true, false or an object' % label)
458
+ return
459
+ elif not isinstance(raw, Mapping):
460
+ p.add('%s must be an object' % label)
461
+ return
462
+ for param in cls.PARAMS:
463
+ # config.json is the FILE layer. A parameter that does not permit
464
+ # it is refused by name rather than ignored: an operator who
465
+ # writes a setting that silently does nothing is exactly the
466
+ # failure this mechanism removes.
467
+ if (param.key in raw and raw[param.key] is not None
468
+ and not param.settable_by(FILE)):
469
+ p.add('%s.%s cannot be set in config.json; it is settable by %s'
470
+ % (label, param.key, ', '.join(param.sources)))
471
+ continue
472
+ if param.validate is not None:
473
+ param.validate(label, raw, p)
474
+ continue
475
+ if param.key not in raw or raw[param.key] is None:
476
+ if param.required:
477
+ p.add('%s.%s is required' % (label, param.key))
478
+ continue
479
+ if not param.ok(raw[param.key]):
480
+ p.add(param.sentence(label))
481
+
482
+ @classmethod
483
+ def from_dict(cls, raw: Any) -> Any:
484
+ if cls.SWITCHABLE and isinstance(raw, bool):
485
+ # `true` is on with every default. `false` is the block's own
486
+ # default, which for a presence-enabled block is off.
487
+ if raw and cls.PRESENCE_ENABLES:
488
+ return cls(**{cls.PRESENCE_ENABLES: True})
489
+ return cls()
490
+ src: Mapping[str, Any] = raw if isinstance(raw, Mapping) else {}
491
+ vals = {p.attr: p.read(src) for p in cls.PARAMS}
492
+ if cls.PRESENCE_ENABLES and src.get(cls.PRESENCE_ENABLES) is None:
493
+ # Reached only because the key is in the file, so the block is on
494
+ # unless the operator wrote it off explicitly.
495
+ vals[cls.PRESENCE_ENABLES] = True
496
+ return cls(**vals)
497
+
498
+ @classmethod
499
+ def parameters(cls) -> tuple[Parameter[Any], ...]:
500
+ return cls.PARAMS
501
+
502
+ @classmethod
503
+ def describe(cls) -> BlockRecord:
504
+ """This block as data: its key and every parameter it declares."""
505
+ return {'key': cls.key, 'label': cls.label or cls.key,
506
+ 'parameters': [p.describe() for p in cls.PARAMS]}
507
+
508
+
509
+ def declared(block_cls: type) -> tuple[Parameter[Any], ...]:
510
+ """The parameters a block declares, or () for one not yet converted.
511
+
512
+ Returns () rather than raising so the two kinds of block can coexist while
513
+ the tree is converted one compartment at a time, and so a report can say
514
+ honestly which blocks it cannot describe yet.
515
+ """
516
+ return tuple(getattr(block_cls, 'PARAMS', ()) or ())
517
+
518
+
519
+ def every_parameter(blocks: Sequence[type]) -> list[ParameterRow]:
520
+ """Every declared parameter across `blocks`, as flat rows.
521
+
522
+ This is the visibility the system exists for: one list, with the block it
523
+ belongs to, its type, its default and whether a reload applies it.
524
+ """
525
+ out: list[ParameterRow] = []
526
+ for b in blocks:
527
+ for param in declared(b):
528
+ row: ParameterRow = {**param.describe(),
529
+ 'block': getattr(b, 'key', '') or getattr(b, '__name__', '')}
530
+ out.append(row)
531
+ return out
532
+
533
+
534
+ def converted(block_cls: type) -> bool:
535
+ """Whether this block states its settings as parameters.
536
+
537
+ Asked of the class, not of its PARAMS: `mediation` and `transport` are
538
+ reserved and have no settings at all, so an empty PARAMS is their correct
539
+ and final answer. Counting them as outstanding would never reach zero.
540
+ """
541
+ return isinstance(block_cls, type) and issubclass(block_cls, ParamBlock)
542
+
543
+
544
+ def undeclared(blocks: Sequence[type]) -> list[str]:
545
+ """Blocks not yet stating their settings as parameters."""
546
+ return [getattr(b, 'key', '') or b.__name__ for b in blocks if not converted(b)]
547
+
548
+
549
+ def hand_written(blocks: Sequence[type]) -> list[str]:
550
+ """Blocks that declare parameters but keep their own check and from_dict.
551
+
552
+ Separate from `undeclared` because they mean different things: this list
553
+ is complete for the census and deliberately not derived, that one is work
554
+ outstanding.
555
+ """
556
+ return [getattr(b, 'key', '') or b.__name__ for b in blocks
557
+ if declared(b) and not getattr(b, 'DERIVED', True)]
558
+
559
+
560
+ __all__ = ['Parameter', 'ParameterRecord', 'ParameterRow', 'BlockRecord', 'Arg', 'ParamBlock',
561
+ 'resolve', 'declared', 'every_parameter', 'undeclared', 'SOURCES', 'OVERRIDE', 'ARG',
562
+ 'ENV', 'FILE', 'MANIFEST', 'DEFAULT', 'ACCESS_ENFORCED', 'ACCESS_DECLARED_ONLY',
563
+ 'hand_written', 'converted']