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
node/storage/grants.py ADDED
@@ -0,0 +1,317 @@
1
+ """Grants on disk: the ones this node issued and the ones it holds.
2
+
3
+ One SQLite table, one row per (peer, direction). The payload is the grant as
4
+ decrypted or as written, the envelope id is the kind 30801 that committed to
5
+ it, and status is live or revoked. A node that restarts must know exactly
6
+ what it granted and what it was granted, or its receipts are fiction.
7
+
8
+ Two more tables came from the two-machine run. `history` keeps every grant a
9
+ pair has been under and the epoch from which it governed, because a window is
10
+ priced under the grant that governed it and not under whatever grant happens
11
+ to be current when the grace period ends. `pending` holds the grant a window
12
+ evaluation decided on until the next window boundary, because a grant that
13
+ changes mid-window changes what a receipt in flight means: the issuer was
14
+ rejecting every receipt naming the grant it had just replaced, and the
15
+ subject was restarting its chain at seq 0 on the grant it had just taken up,
16
+ so every amend filed an S2 misreport against an honest node. An amend now
17
+ takes effect at a boundary, as the specification's grant loop says: the
18
+ verdict on one window shapes "the next window's grant".
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import builtins
23
+ import json
24
+ import time
25
+ from typing import Any, Literal, Mapping, Optional
26
+
27
+ from ..compartment.schema import Column, Migration, Table
28
+ from . import SqliteStore
29
+ from fleetmesh_core.wire.interfaces import EventLike, GrantPayload
30
+ from fleetmesh_core.wire.wire_types import GoverningRow, GrantListRow, GrantRow, HistoryRow, PendingRow
31
+
32
+ GrantDirection = Literal['issued', 'received']
33
+ ISSUED: GrantDirection = 'issued'
34
+ RECEIVED: GrantDirection = 'received'
35
+
36
+
37
+ class GrantStore(SqliteStore):
38
+ SCHEMA = (
39
+ Table('grants', (
40
+ Column('peer', 'TEXT', not_null=True),
41
+ Column('direction', 'TEXT', not_null=True),
42
+ Column('envelope_id', 'TEXT', not_null=True),
43
+ Column('payload', 'TEXT', not_null=True),
44
+ Column('tier', 'TEXT', not_null=True),
45
+ Column('status', 'TEXT', not_null=True, default="'live'"),
46
+ Column('updated_at', 'INTEGER', not_null=True),
47
+ ), primary_key=('peer', 'direction')),
48
+ Table('history', (
49
+ Column('peer', 'TEXT', not_null=True),
50
+ Column('direction', 'TEXT', not_null=True),
51
+ Column('envelope_id', 'TEXT', not_null=True),
52
+ Column('payload', 'TEXT', not_null=True),
53
+ Column('effective_epoch', 'INTEGER', not_null=True),
54
+ Column('put_at', 'INTEGER', not_null=True),
55
+ ), primary_key=('peer', 'direction', 'envelope_id')),
56
+ Table('pending', (
57
+ Column('peer', 'TEXT', primary_key=True),
58
+ Column('payload', 'TEXT', not_null=True),
59
+ Column('note', 'TEXT'),
60
+ Column('severity', 'TEXT'),
61
+ Column('decided_epoch', 'INTEGER', not_null=True),
62
+ )),
63
+ Table('seeds', (
64
+ Column('peer', 'TEXT', primary_key=True),
65
+ Column('tier', 'TEXT', not_null=True),
66
+ Column('at', 'INTEGER', not_null=True),
67
+ )),
68
+ )
69
+ MIGRATIONS = (
70
+ # The envelope's own created_at, which is what NIP-01 supersedes on. A
71
+ # grant is only ever superseded forwards, so this is how a node refuses
72
+ # an amend that arrived out of order.
73
+ Migration(1, up='ALTER TABLE grants ADD COLUMN env_created_at INTEGER', name='grants_env_created_at'),
74
+ # The signed kind 30801 itself, not only its id. A grant is addressable
75
+ # under a blinded tag, so each new one replaces the last at that
76
+ # coordinate and no relay serves the superseded one - and a report
77
+ # about a past window cites exactly that. A report carries its own
78
+ # copy now, which it can only do if this node kept it.
79
+ Migration(2, up='ALTER TABLE history ADD COLUMN event TEXT', name='history_event'),
80
+ # Insertion order, carried explicitly. `history` ordered by
81
+ # (effective_epoch, put_at, rowid) and rowid is SQLite's implicit
82
+ # insertion counter, which PostgreSQL has no equivalent for - ctid is a
83
+ # physical location, not insertion order, and moves when a row is
84
+ # updated. Two grants for one peer and direction written in the same
85
+ # second at the same epoch tie on everything else, and which one won
86
+ # decided which grant governed the window - the record a peer's
87
+ # standing is read from. So the tiebreaker becomes a column.
88
+ Migration(3, up='ALTER TABLE history ADD COLUMN seq INTEGER NOT NULL DEFAULT 0', name='history_seq'),
89
+ )
90
+
91
+ def _next_seq(self, peer: str, direction: str) -> int:
92
+ """The next insertion number for this peer and direction.
93
+
94
+ Explicit rather than an auto-increment, because the two backends spell
95
+ that differently and SQLite cannot add one with ALTER TABLE. Every
96
+ caller holds the store lock, so the read-then-write cannot race.
97
+ """
98
+ row = self._one('SELECT COALESCE(MAX(seq), 0) + 1 FROM history '
99
+ 'WHERE peer=? AND direction=?', (peer, direction))
100
+ return int(row[0]) if row and row[0] is not None else 1
101
+
102
+ def _post_schema(self) -> None:
103
+ # Rows written before `seq` existed get their insertion order from the
104
+ # backend's own, where it has one. SQLite's is `rowid`; PostgreSQL has
105
+ # none - `ctid` is a physical location, not insertion order, and moves
106
+ # when a row is updated - but a PostgreSQL store is new, so its history
107
+ # is empty and there is nothing to carry over. `WHERE seq = 0` makes
108
+ # this run once: `_next_seq` starts at 1, so a row it numbered is never
109
+ # mistaken for one that needs numbering.
110
+ expr = getattr(self.dialect, 'rowid_expr', None)
111
+ if expr and self.has_column('history', 'seq'):
112
+ self.execute('UPDATE history SET seq = %s WHERE seq = 0' % expr)
113
+
114
+ # A store from before history existed: whatever it holds now has
115
+ # governed since the beginning of time, as far as it can tell.
116
+ for peer, direction, env, payload, updated_at in self._all(
117
+ 'SELECT peer, direction, envelope_id, payload, updated_at FROM grants'):
118
+ if self._one('SELECT 1 FROM history WHERE peer=? AND direction=?',
119
+ (peer, direction)) is None:
120
+ self.execute(
121
+ 'INSERT OR IGNORE INTO history '
122
+ '(peer, direction, envelope_id, payload, effective_epoch, put_at, event, seq) '
123
+ 'VALUES (?,?,?,?,?,?,NULL,?)',
124
+ (peer, direction, env, payload, 0, updated_at,
125
+ self._next_seq(peer, direction)))
126
+
127
+ def put(
128
+ self,
129
+ peer: str,
130
+ direction: GrantDirection,
131
+ payload: GrantPayload | Mapping[str, Any],
132
+ envelope_id: str,
133
+ now: Optional[int] = None,
134
+ env_created_at: Optional[int] = None,
135
+ effective_epoch: Optional[int] = None,
136
+ event: Optional[EventLike] = None,
137
+ ) -> None:
138
+ """Record a grant. effective_epoch is the first window it governs; a
139
+ caller that does not say gets the window `now` falls in."""
140
+ if direction not in (ISSUED, RECEIVED):
141
+ raise ValueError('direction is issued or received')
142
+ now = int(now or time.time())
143
+ if effective_epoch is None:
144
+ effective_epoch = now // (payload.get('window') or 3600)
145
+ with self._lock:
146
+ if env_created_at is not None:
147
+ held = self._one('SELECT env_created_at, envelope_id FROM grants WHERE peer=? AND direction=?',
148
+ (peer, direction))
149
+ if held and held[0] is not None and (env_created_at, envelope_id) < (held[0], held[1]):
150
+ # A grant is only superseded forwards, in the order a relay
151
+ # resolves the coordinate: latest created_at, lower id on a
152
+ # tie. Two written for one subject by two threads were put
153
+ # in the other order, and the issuer then held the one the
154
+ # relay had let go. The history still records it.
155
+ self.execute(
156
+ 'INSERT OR REPLACE INTO history '
157
+ '(peer, direction, envelope_id, payload, effective_epoch, put_at, event, seq) '
158
+ 'VALUES (?,?,?,?,?,?,?,?)',
159
+ (peer, direction, envelope_id, json.dumps(payload, sort_keys=True),
160
+ int(effective_epoch), now,
161
+ json.dumps(event, sort_keys=True) if event is not None else None,
162
+ self._next_seq(peer, direction)))
163
+ self.db.commit()
164
+ return
165
+ self.execute('INSERT OR REPLACE INTO grants VALUES (?,?,?,?,?,?,?,?)',
166
+ (peer, direction, envelope_id, json.dumps(payload, sort_keys=True),
167
+ payload.get('tier', ''), 'revoked' if payload.get('tier') == 'revoked' else 'live',
168
+ now, env_created_at))
169
+ self.execute(
170
+ 'INSERT OR REPLACE INTO history '
171
+ '(peer, direction, envelope_id, payload, effective_epoch, put_at, event, seq) '
172
+ 'VALUES (?,?,?,?,?,?,?,?)',
173
+ (peer, direction, envelope_id, json.dumps(payload, sort_keys=True),
174
+ int(effective_epoch), now,
175
+ json.dumps(event, sort_keys=True) if event is not None else
176
+ self.envelope_event(peer, direction, envelope_id, as_json=True),
177
+ self._next_seq(peer, direction)))
178
+ self.db.commit()
179
+
180
+ def governing(self, peer: str, direction: GrantDirection, epoch: int) -> Optional[GoverningRow]:
181
+ """The grant that governed one window: the latest one effective at or
182
+ before it. Two written in the same second tie on put_at, so insertion
183
+ order breaks the tie: a grant is only superseded forwards. Falls back
184
+ to the current row, so a store that predates the history still answers."""
185
+ with self._lock:
186
+ row = self._one(
187
+ 'SELECT payload, envelope_id, effective_epoch FROM history WHERE peer=? AND direction=? '
188
+ 'AND effective_epoch<=? ORDER BY effective_epoch DESC, put_at DESC, seq DESC LIMIT 1',
189
+ (peer, direction, int(epoch)))
190
+ if row is None: # a window before the first grant: the first grant is the nearest answer
191
+ row = self._one(
192
+ 'SELECT payload, envelope_id, effective_epoch FROM history WHERE peer=? AND direction=? '
193
+ 'ORDER BY effective_epoch, put_at, seq LIMIT 1', (peer, direction))
194
+ if row:
195
+ return {'payload': json.loads(row[0]), 'envelope_id': row[1], 'effective_epoch': row[2]}
196
+ cur = self.get(peer, direction)
197
+ if cur is None:
198
+ return None
199
+ return {'payload': cur['payload'], 'envelope_id': cur['envelope_id'], 'effective_epoch': None}
200
+
201
+ def envelope_event(
202
+ self,
203
+ peer: str,
204
+ direction: GrantDirection,
205
+ envelope_id: str,
206
+ as_json: bool = False,
207
+ ) -> Any:
208
+ """The signed kind 30801 this node held under that id, if it kept one."""
209
+ with self._lock:
210
+ row = self._one('SELECT event FROM history WHERE peer=? AND direction=? AND envelope_id=?',
211
+ (peer, direction, envelope_id))
212
+ if not row or not row[0]:
213
+ return None
214
+ return row[0] if as_json else json.loads(row[0])
215
+
216
+ def known_envelopes(self, peer: str, direction: GrantDirection) -> list[str]:
217
+ with self._lock:
218
+ return [r[0] for r in self._all(
219
+ 'SELECT envelope_id FROM history WHERE peer=? AND direction=? ORDER BY effective_epoch, put_at',
220
+ (peer, direction))]
221
+
222
+ # ---- the grant a window decided on, waiting for the boundary -------------
223
+ def put_pending(self, peer: str, payload: GrantPayload | Mapping[str, Any], note: str, severity: Optional[str],
224
+ decided_epoch: int) -> None:
225
+ with self._lock:
226
+ self.execute('INSERT OR REPLACE INTO pending VALUES (?,?,?,?,?)',
227
+ (peer, json.dumps(payload, sort_keys=True), note, severity, int(decided_epoch)))
228
+ self.db.commit()
229
+
230
+ def pending(self, peer: str) -> Optional[PendingRow]:
231
+ with self._lock:
232
+ row = self._one('SELECT payload, note, severity, decided_epoch FROM pending WHERE peer=?',
233
+ (peer,))
234
+ if not row:
235
+ return None
236
+ return {'payload': json.loads(row[0]), 'note': row[1], 'severity': row[2], 'decided_epoch': row[3]}
237
+
238
+ def clear_pending(self, peer: str) -> None:
239
+ with self._lock:
240
+ self.execute('DELETE FROM pending WHERE peer=?', (peer,))
241
+ self.db.commit()
242
+
243
+ # ---- the operator's decisions, as applied ----------------------------------
244
+ def seed_applied(self, peer: str) -> Optional[tuple[str, int]]:
245
+ with self._lock:
246
+ row = self._one('SELECT tier, at FROM seeds WHERE peer=?', (peer,))
247
+ return (row[0], row[1]) if row else None
248
+
249
+ def put_seed(self, peer: str, tier: str, at: int) -> None:
250
+ with self._lock:
251
+ self.execute('INSERT OR REPLACE INTO seeds VALUES (?,?,?)', (peer, tier, int(at)))
252
+ self.db.commit()
253
+
254
+ def get(self, peer: str, direction: GrantDirection) -> Optional[GrantRow]:
255
+ # Every write here took the lock and every read did not, on a
256
+ # connection opened with check_same_thread=False and shared by the
257
+ # tick, sync, peering and transport threads. sqlite answers that with
258
+ # an intermittent "bad parameter or other API misuse", which is what a
259
+ # sweep caught in grants.get after the capacity ladder added a reader
260
+ # on the peering path. A read is as much a use of the connection as a
261
+ # write.
262
+ with self._lock:
263
+ row = self._one('SELECT payload, envelope_id, status, env_created_at, updated_at FROM grants '
264
+ 'WHERE peer=? AND direction=?', (peer, direction))
265
+ if not row:
266
+ return None
267
+ return {'payload': json.loads(row[0]), 'envelope_id': row[1], 'status': row[2],
268
+ 'env_created_at': row[3], 'updated_at': row[4]}
269
+
270
+ def history(self, peer: str, direction: GrantDirection) -> list[HistoryRow]:
271
+ """Every grant that has governed a window with this peer, oldest first.
272
+
273
+ Here because `grant_view` in app.py used to read it straight off the
274
+ connection, without the lock, from the control socket's thread while
275
+ the tick and the sync threads were writing. `windows_view` beside it
276
+ took the account store's lock, so the rule was known and this one call
277
+ was missed. A store that hands out its connection cannot enforce
278
+ anything about how it is used.
279
+ """
280
+ return [{'envelope_id': r[0], 'effective_epoch': r[1], 'tier': json.loads(r[2]).get('tier')}
281
+ for r in self._all(
282
+ 'SELECT envelope_id, effective_epoch, payload FROM history '
283
+ 'WHERE peer=? AND direction=? ORDER BY effective_epoch, put_at, seq',
284
+ (peer, direction))]
285
+
286
+ # `builtins.list` because the method itself is called `list`, and inside the
287
+ # class body the bare name would resolve to the method, not the type.
288
+ def list(self, direction: Optional[GrantDirection] = None) -> builtins.list[GrantListRow]:
289
+ with self._lock:
290
+ q = 'SELECT peer, direction, payload, envelope_id, status FROM grants'
291
+ args: tuple[Any, ...] = ()
292
+ if direction:
293
+ q += ' WHERE direction=?'
294
+ args = (direction,)
295
+ return [{'peer': r[0], 'direction': r[1], 'payload': json.loads(r[2]),
296
+ 'envelope_id': r[3], 'status': r[4]} for r in self._all(q, args)]
297
+
298
+ def zero(self, peer: str, now: Optional[int] = None) -> None:
299
+ """Both directions to zero and revoked: a terminate or a revocation."""
300
+ with self._lock:
301
+ for direction in (ISSUED, RECEIVED):
302
+ row = self.get(peer, direction)
303
+ if row:
304
+ p = row['payload']
305
+ p['limits'] = {k: 0 for k in p.get('limits', {})}
306
+ p['tier'] = 'revoked'
307
+ self.execute('UPDATE grants SET payload=?, tier=?, status=?, updated_at=? '
308
+ 'WHERE peer=? AND direction=?',
309
+ (json.dumps(p, sort_keys=True), 'revoked', 'revoked',
310
+ int(now or time.time()), peer, direction))
311
+ self.execute('DELETE FROM pending WHERE peer=?', (peer,))
312
+ self.db.commit()
313
+
314
+ def peers(self) -> builtins.list[str]:
315
+ with self._lock:
316
+ return sorted({r[0] for r in self._all('SELECT peer FROM grants WHERE status=\'live\'')})
317
+
@@ -0,0 +1,150 @@
1
+ """Where each of the node's stores lives, decided once from config.
2
+
3
+ Every store site in the tree reads the same two things: a directory and a
4
+ logical name, joined into `<directory>/<name>.sqlite`. That join is the only
5
+ reason the tree is tied to SQLite now that the dialect layer exists, so it is
6
+ the thing to replace - not with a flag threaded through fourteen files, but
7
+ with one object that answers "where does the store called `grants` live" and
8
+ is built once from the node's configuration.
9
+
10
+ loc = Locator.from_config(directory) # the default
11
+ loc = Locator.from_config(directory, url='postgresql://...')
12
+ store = loc.open(grants.GrantStore, 'grants')
13
+
14
+ On SQLite that is the same path the site computed before, so the default node
15
+ is byte-for-byte unchanged. On PostgreSQL every store gets the same URL: one
16
+ node per database is settled (docs/storage-backends.md section 10), the forty-
17
+ four table names do not collide, and the schema version is already keyed by
18
+ store name rather than by database - the defect that taught us so is recorded
19
+ at postgres.py:218.
20
+
21
+ One dialect instance is shared by every store it opens. `PostgresDialect`
22
+ learns each table's columns and primary key from the DDL passing through
23
+ `sql()`, so sharing means the reflection is learned once rather than fourteen
24
+ times, and the methods that use a connection take it as an argument.
25
+
26
+ A backend is named by URL scheme, and an unknown one is refused by name
27
+ rather than quietly becoming a filename.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import os
32
+ from typing import Any, Callable, Optional, TypedDict
33
+
34
+ from .dialect import Dialect, SqliteDialect
35
+
36
+ SQLITE_SUFFIX = '.sqlite'
37
+ POSTGRES_SCHEMES = ('postgresql://', 'postgres://')
38
+
39
+
40
+ class _Backend(TypedDict):
41
+ backend: str
42
+
43
+
44
+ class StorageBackend(_Backend, total=False):
45
+ """The backend as `meshnode health` reports it.
46
+
47
+ A named record rather than a loose mapping, for the same reason
48
+ `StoreLease` is one: check-dicts.py holds this tree to typing what it
49
+ returns, and a row an operator reads by field name is the case that rule
50
+ exists for. Exactly one of `directory` and `server` is present - which one
51
+ is the answer being reported.
52
+ """
53
+ directory: str
54
+ server: str
55
+ owner: str
56
+
57
+
58
+ class Locator:
59
+ """The node's storage target. Build it with `from_config`."""
60
+
61
+ def __init__(self, directory: str, dialect: Dialect,
62
+ url: Optional[str] = None, owner: Optional[str] = None) -> None:
63
+ self.directory: str = directory
64
+ self.dialect: Dialect = dialect
65
+ self.url: Optional[str] = url
66
+ self.owner: Optional[str] = owner
67
+
68
+ # ---- construction -----------------------------------------------------
69
+
70
+ @classmethod
71
+ def from_config(cls, directory: str, url: Optional[str] = None,
72
+ owner: Optional[str] = None) -> 'Locator':
73
+ """The locator a node's config asks for.
74
+
75
+ No URL is the default and needs nothing of the operator. A URL names
76
+ its backend by scheme; anything else is a configuration error and is
77
+ reported as one, because treating it as a path would produce a node
78
+ that silently ran on SQLite while an operator believed otherwise.
79
+
80
+ `owner` is the node's identity for the ownership lease. It is what
81
+ makes one node per database enforced rather than assumed
82
+ (docs/storage-backends.md section 5a): `BaseStore` claims a lease only
83
+ when it is given an owner, so a locator without one opens stores that
84
+ claim nothing. On a server every store shares the database and
85
+ therefore shares the one `fleetmesh_owner` row, so the first store to
86
+ open claims the database for this node and a second node is refused.
87
+ """
88
+ if not url:
89
+ return cls(directory, SqliteDialect(), owner=owner)
90
+ if url.startswith(POSTGRES_SCHEMES):
91
+ from .postgres import PostgresDialect # noqa: PLC0415 (optional)
92
+ return cls(directory, PostgresDialect(), url=url, owner=owner)
93
+ raise ValueError(
94
+ 'unknown storage URL scheme: %r - expected one of %s, or no URL for the '
95
+ 'SQLite default' % (url.split('://')[0], ', '.join(POSTGRES_SCHEMES)))
96
+
97
+ # ---- use --------------------------------------------------------------
98
+
99
+ def target(self, name: str) -> str:
100
+ """The connection target for the store called `name`.
101
+
102
+ A file per store on SQLite. One database for all of them on a server,
103
+ which is what "one node per database" means.
104
+ """
105
+ if self.url is not None:
106
+ return self.url
107
+ return os.path.join(self.directory, name + SQLITE_SUFFIX)
108
+
109
+ def open(self, store_cls: Callable[..., Any], name: str, **kw: Any) -> Any:
110
+ """Construct a store of `store_cls` where `name` belongs.
111
+
112
+ `kw` is passed through untouched, so a store with its own arguments -
113
+ `ttl_seconds`, `readonly` - keeps them, and a caller naming `owner`
114
+ itself wins over the locator's.
115
+ """
116
+ if self.owner is not None:
117
+ kw.setdefault('owner', self.owner)
118
+ return store_cls(self.target(name), dialect=self.dialect, **kw)
119
+
120
+ # ---- what an operator is shown ----------------------------------------
121
+
122
+ def describe(self) -> StorageBackend:
123
+ """The backend, for `meshnode health`.
124
+
125
+ Which backend is live is otherwise invisible until something breaks,
126
+ and a node believed to be on PostgreSQL while running on SQLite is
127
+ exactly the failure this reports. The URL is reduced to host and
128
+ database: a connection string carries a password.
129
+ """
130
+ out: StorageBackend = {'backend': self.dialect.name}
131
+ if self.url is None:
132
+ out['directory'] = self.directory
133
+ else:
134
+ out['server'] = redact(self.url)
135
+ if self.owner is not None:
136
+ out['owner'] = self.owner
137
+ return out
138
+
139
+
140
+ def redact(url: str) -> str:
141
+ """`postgresql://user:pw@host:5432/db` -> `host:5432/db`.
142
+
143
+ A password in a connection string must not reach a log, a health block or
144
+ a receipt, and the part an operator needs to recognise the server is the
145
+ part after the credentials.
146
+ """
147
+ rest = url.split('://', 1)[1] if '://' in url else url
148
+ if '@' in rest:
149
+ rest = rest.rsplit('@', 1)[1]
150
+ return rest.split('?', 1)[0]