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,258 @@
1
+ """ProjectedDB compartment: operator visibility, audit verification, and
2
+ on-demand rebuilds for co-transactional SQL projections over `ProjectedNodeStore`.
3
+
4
+ The write path itself is synchronous and co-transactional inside
5
+ `ProjectedNodeStore._put`, `_drop`, and `forget`. This compartment exposes:
6
+ - `projected/show`: registered `TableProjector`s, row counts, causal logs, and
7
+ recent payload refusals
8
+ - `projected/verify`: audits every projected SQL table against the live `events`
9
+ table (`verify_projection()`) and raises `trouble()` if any drift is found
10
+ - `projected/rebuild`: rebuilds one or all projected SQL tables from `events`
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import dataclasses
15
+ from typing import Any, Mapping, Optional
16
+
17
+ from node.channels import NOTE, Channel
18
+ from node.compartment import Command, Compartment, Every, Need
19
+ from node.compartment import stores as st
20
+ from node.config.parameter import ParamBlock, Parameter
21
+ from node.metrics import COUNTER, Metric
22
+ from node.storage.projected import (
23
+ CausalProjector,
24
+ ProjectedNodeStore,
25
+ ProjectionVerifyReport,
26
+ ProjectorSpec,
27
+ TableProjector,
28
+ )
29
+
30
+ from .types import ProjectedRebuildReply, ProjectedStateReport, ProjectedTableSummary
31
+
32
+ VERIFY_SECONDS: int = 3600
33
+
34
+
35
+ @dataclasses.dataclass(frozen=True)
36
+ class ProjectedDBConfig(ParamBlock):
37
+ key = 'projected'
38
+ name = 'projected'
39
+ DERIVED: bool = False
40
+ PARAMS = (
41
+ Parameter(
42
+ key='verify_seconds',
43
+ name='Verify Cadence',
44
+ description='Seconds between background projection consistency audits',
45
+ type=int,
46
+ default=VERIFY_SECONDS,
47
+ tags=('projected', 'store'),
48
+ ),
49
+ Parameter(
50
+ key='auto_heal',
51
+ name='Auto-Heal Drift',
52
+ description='Automatically rebuild projected SQL tables if verification detects drift',
53
+ type=bool,
54
+ default=True,
55
+ tags=('projected', 'store'),
56
+ ),
57
+ )
58
+ verify_seconds: int = VERIFY_SECONDS
59
+ auto_heal: bool = True
60
+
61
+
62
+ class ProjectedDB(Compartment):
63
+ """Operator surface and consistency auditor for `ProjectedNodeStore`."""
64
+
65
+ name = 'projected'
66
+ reads = ('projected', 'coverage')
67
+ live = ('projected',)
68
+ config_cls = ProjectedDBConfig
69
+ CHANNELS = (
70
+ Channel(
71
+ 'table_rebuilt',
72
+ 'Projected SQL table rebuilt from events',
73
+ level=NOTE,
74
+ say='projected table {table} rebuilt ({rows} rows)',
75
+ tags=('projected', 'store'),
76
+ ),
77
+ Channel(
78
+ 'projection_mismatch',
79
+ 'Projected SQL table drifted from events store',
80
+ level=NOTE,
81
+ say='projection mismatch detected ({missing} missing, {orphan} orphan)',
82
+ tags=('projected', 'store'),
83
+ ),
84
+ )
85
+ METRICS = (
86
+ Metric(
87
+ name='projected_tables_rebuilt',
88
+ description='Times a projected SQL table was rebuilt from events.',
89
+ kind=COUNTER,
90
+ counts='table_rebuilt',
91
+ tags=('projected', 'store'),
92
+ ),
93
+ Metric(
94
+ name='projected_mismatches',
95
+ description='Times projection verification found drift.',
96
+ kind=COUNTER,
97
+ counts='projection_mismatch',
98
+ tags=('projected', 'store'),
99
+ ),
100
+ )
101
+ NEEDS = (
102
+ Need('store', 'stores.events'),
103
+ Need('cfg', 'cfg', getter=True),
104
+ )
105
+ EVERY = (Every(seconds=VERIFY_SECONDS, run='verify'),)
106
+ COMMANDS = (
107
+ Command(
108
+ key='show',
109
+ name='Projected tables',
110
+ description='registered SQL table projectors, row counts, and payload refusals',
111
+ tags=('projected', 'status'),
112
+ permission='projected/show',
113
+ run='state',
114
+ reply='{enabled, tables, causal_logs, refusals, verified_at, last_verify_ok}',
115
+ ),
116
+ Command(
117
+ key='verify',
118
+ name='Verify projections',
119
+ description='audit projected SQL tables against the live events table',
120
+ tags=('projected',),
121
+ permission='projected/verify',
122
+ run='verify',
123
+ reply='{ok, tables, missing_events, orphan_rows, mismatched_rows}',
124
+ ),
125
+ Command(
126
+ key='rebuild',
127
+ name='Rebuild projections',
128
+ description='rebuild one or all projected SQL tables from the live events table',
129
+ tags=('projected',),
130
+ parameters=(
131
+ Parameter(
132
+ key='table_name',
133
+ name='table_name',
134
+ description='optional projected SQL table name to rebuild (omit to rebuild all)',
135
+ type=str,
136
+ default='',
137
+ required=False,
138
+ ),
139
+ ),
140
+ permission='projected/rebuild',
141
+ run='rebuild',
142
+ reply='{rebuilt: {table: rows}}',
143
+ ),
144
+ )
145
+ STORES = (
146
+ st.Store(
147
+ key='rebuilds',
148
+ description='total rebuilds performed per projected table',
149
+ kind=st.TABLE,
150
+ shape=int,
151
+ bound=256,
152
+ keyed_by='table',
153
+ evict=st.LRU,
154
+ ),
155
+ )
156
+
157
+ def __init__(self, store: Any, cfg: Any, clock: Any, node: Any = None) -> None:
158
+ Compartment.__init__(self, node, clock)
159
+ self.store = store
160
+ self._cfg = cfg
161
+ self.rebuilds: st.Table[str, int]
162
+ self.verified_at: Optional[int] = None
163
+ self.last_verify_ok: Optional[bool] = None
164
+
165
+ def is_projected_store(self) -> bool:
166
+ return isinstance(self.store, ProjectedNodeStore)
167
+
168
+ def register_projector(self, projector: TableProjector | ProjectorSpec) -> Optional[TableProjector]:
169
+ if not isinstance(self.store, ProjectedNodeStore):
170
+ return None
171
+ return self.store.register_projector(projector)
172
+
173
+ def register_causal_projector(self, causal: CausalProjector) -> Optional[CausalProjector]:
174
+ if not isinstance(self.store, ProjectedNodeStore):
175
+ return None
176
+ return self.store.register_causal_projector(causal)
177
+
178
+ def verify(self, now: Optional[int] = None) -> ProjectionVerifyReport:
179
+ now_ts = now if now is not None else self.clock()
180
+ self.verified_at = now_ts
181
+ if not isinstance(self.store, ProjectedNodeStore):
182
+ self.last_verify_ok = True
183
+ return {
184
+ 'ok': True,
185
+ 'tables': {},
186
+ 'missing_events': [],
187
+ 'orphan_rows': [],
188
+ 'mismatched_rows': [],
189
+ }
190
+ report = self.store.verify_projection()
191
+ self.last_verify_ok = report['ok']
192
+ if not report['ok']:
193
+ self.say(
194
+ 'projection_mismatch',
195
+ missing=len(report['missing_events']),
196
+ orphan=len(report['orphan_rows']),
197
+ )
198
+ raw_cfg = (self._cfg() or {}).get('projected') or {}
199
+ auto_heal = raw_cfg.get('auto_heal', True) if isinstance(raw_cfg, Mapping) else True
200
+ if auto_heal:
201
+ self.rebuild(now=now_ts)
202
+ report = self.store.verify_projection()
203
+ self.last_verify_ok = report['ok']
204
+ return report
205
+
206
+ def rebuild(
207
+ self,
208
+ table_name: Optional[str | Mapping[str, Any]] = None,
209
+ now: Optional[int] = None,
210
+ ) -> ProjectedRebuildReply:
211
+ if not isinstance(self.store, ProjectedNodeStore):
212
+ return {'rebuilt': {}}
213
+ target_table: Optional[str]
214
+ if isinstance(table_name, Mapping):
215
+ raw_tbl = str(table_name.get('table_name') or '').strip()
216
+ target_table = raw_tbl or None
217
+ else:
218
+ target_table = (table_name or '').strip() or None if isinstance(table_name, str) else None
219
+ rebuilt = self.store.backfill(table_name=target_table)
220
+ for tbl, rows in rebuilt.items():
221
+ self.rebuilds[tbl] = self.rebuilds.get(tbl, 0) + 1
222
+ self.say('table_rebuilt', table=tbl, rows=rows)
223
+ return {'rebuilt': rebuilt}
224
+
225
+ def state(self) -> ProjectedStateReport:
226
+ if not isinstance(self.store, ProjectedNodeStore):
227
+ return {
228
+ 'enabled': False,
229
+ 'tables': {},
230
+ 'causal_logs': [],
231
+ 'refusals': [],
232
+ 'verified_at': self.verified_at,
233
+ 'last_verify_ok': self.last_verify_ok,
234
+ }
235
+ summaries: dict[str, ProjectedTableSummary] = {}
236
+ for tp in self.store.projectors():
237
+ summaries[tp.table_name] = {
238
+ 'table': tp.table_name,
239
+ 'kind': tp.kind,
240
+ 'mode': tp.spec.mode(),
241
+ 'primary_key': list(tp.spec.primary_key),
242
+ 'rows': self.store.table_count(tp.table_name),
243
+ 'projected_total': self.store.projected_counts.get(tp.table_name, 0),
244
+ }
245
+ return {
246
+ 'enabled': True,
247
+ 'tables': summaries,
248
+ 'causal_logs': [cp.log_id for cp in self.store.causal_projectors()],
249
+ 'refusals': list(self.store.projection_refusals[-20:]),
250
+ 'verified_at': self.verified_at,
251
+ 'last_verify_ok': self.last_verify_ok,
252
+ }
253
+
254
+ def trouble(self) -> list[str]:
255
+ out: list[str] = []
256
+ if self.last_verify_ok is False:
257
+ out.append('one or more projected SQL tables failed verification against the events store')
258
+ return out
@@ -0,0 +1,29 @@
1
+ """Types and status records for the ProjectedDB compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, TypedDict
5
+
6
+ from node.storage.projected import ProjectionRefusal
7
+
8
+
9
+ class ProjectedTableSummary(TypedDict):
10
+ table: str
11
+ kind: int
12
+ mode: str
13
+ primary_key: list[str]
14
+ rows: int
15
+ projected_total: int
16
+
17
+
18
+ class ProjectedStateReport(TypedDict):
19
+ enabled: bool
20
+ tables: dict[str, ProjectedTableSummary]
21
+ causal_logs: list[str]
22
+ refusals: list[ProjectionRefusal]
23
+ verified_at: Optional[int]
24
+ last_verify_ok: Optional[bool]
25
+
26
+
27
+ class ProjectedRebuildReply(TypedDict):
28
+ rebuilt: dict[str, int]
29
+
@@ -0,0 +1,7 @@
1
+ """What this node stops keeping, and the one thing it must never stop keeping.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+ """
5
+ from .compartment import Retention
6
+
7
+ __all__ = ['Retention']
@@ -0,0 +1,214 @@
1
+ """What this node stops keeping, and the one thing it must never stop keeping.
2
+
3
+ An open relay face collects what an open face invites. On the alpha mesh one
4
+ node was holding 6,025 events to every other node's 38, of which 5,625 were
5
+ NIP-59 gift wraps between strangers using it as an ordinary DM relay - about
6
+ 5,200 arriving a day, 62 MB and climbing, on a box with 113 MB of memory free.
7
+ Nothing bounded it: NIP-40 expiry is enforced when an event is read and never on
8
+ disk, and only 68 of those 6,025 carried an expiration tag at all, so leaning on
9
+ other people's compliance would have reclaimed nothing.
10
+
11
+ So retention is the operator's, stated per kind, and **the default keeps
12
+ everything**. A relay that quietly began deleting what it was handed would be a
13
+ worse thing than one that fills up, and the specification prices retrieval
14
+ rather than retention precisely because a standing promise to hold something is
15
+ the one product this protocol declines to sell. Nothing here is protocol. It is
16
+ a disk, and whose disk it is decides.
17
+
18
+ The guard is the part that is not a preference. A kind 30803 coverage claim
19
+ names a shard as a filter and carries a negentropy fingerprint over the set;
20
+ evicting an event inside a claimed shard would leave this node advertising a
21
+ fingerprint it can no longer reproduce, and a peer reconciling against it would
22
+ find the difference and be right. So an event matching any shard this node
23
+ claims is never evicted, whatever the operator asked for, and the refusal is
24
+ recorded rather than performed silently.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import dataclasses
29
+ from typing import Any, Iterator, Mapping, Optional
30
+ from fleetmesh_core.storage import store as store_mod # noqa: E402
31
+ from node.compartment import stores as st
32
+ from node.channels import Channel, NOTE
33
+ from node.metrics import COUNTER, Metric
34
+ from node.compartment import Command, Compartment, Every, Need # noqa: E402
35
+ from node.config.parameter import Parameter, ParamBlock
36
+ from .types import (
37
+ RetentionStateReport,
38
+ )
39
+
40
+ # A sweep walks the store, so it does not run on every tick. An hour is far
41
+ # finer than any age worth setting and coarse enough to cost nothing.
42
+ SWEEP_SECONDS: int = 3600
43
+ # How many events one page of a sweep holds. The sweep reads events to decide
44
+ # whether a shard claim protects them, and the backlog it reads on the first
45
+ # sweep after a policy is set is unbounded by nature - days of an open face.
46
+ SWEEP_PAGE: int = 500
47
+
48
+
49
+ @dataclasses.dataclass(frozen=True)
50
+ class RetentionConfig(ParamBlock):
51
+ key = 'retention'
52
+ name = 'retention'
53
+ DERIVED: bool = False
54
+ PARAMS = (
55
+ Parameter(key='sweep_seconds', name='Sweep Cadence', description='Seconds between retention sweeps', type=int, default=SWEEP_SECONDS, tags=('retention', 'sweep')),
56
+ Parameter(key='sweep_page', name='Sweep Page Size', description='Number of events examined per sweep page', type=int, default=SWEEP_PAGE, tags=('retention', 'sweep')),
57
+ )
58
+ sweep_seconds: int = SWEEP_SECONDS
59
+ sweep_page: int = SWEEP_PAGE
60
+
61
+
62
+ class Retention(Compartment):
63
+ """Per-kind ages, swept on a slow cadence, with the shards held back."""
64
+
65
+ name = 'retention'
66
+ reads = ('retention', 'coverage')
67
+ live = ('retention',)
68
+ config_cls = RetentionConfig
69
+ CHANNELS = (
70
+ Channel('events_evicted', 'Expired events evicted during sweep', level=NOTE, say='retention sweep evicted {count} expired events', tags=('retention', 'sweep')),
71
+ Channel('eviction_refused', 'Event eviction refused because it matches a claimed shard', level=NOTE, say='retention eviction refused for event {id}', tags=('retention', 'sweep')),
72
+ )
73
+ METRICS = (
74
+ Metric(name='retention_events_evicted', description='Times expired events were evicted.', kind=COUNTER, counts='events_evicted', tags=('retention', 'sweep')),
75
+ Metric(name='retention_evictions_refused', description='Times evictions were refused.', kind=COUNTER, counts='eviction_refused', tags=('retention', 'sweep')),
76
+ )
77
+ NEEDS = (
78
+ Need('store', 'stores.events'),
79
+ Need('cfg', 'cfg', getter=True),
80
+ )
81
+ EVERY = (Every(seconds=SWEEP_SECONDS, run='sweep'),)
82
+ COMMANDS = (
83
+ Command(
84
+ key='show',
85
+ name='Retention',
86
+ description='the ages this operator set, what they protect, and what has been evicted',
87
+ tags=('retention', 'status'),
88
+ permission='retention/show',
89
+ run='state',
90
+ reply='{ages, protected, swept_at, evicted, held_back}',
91
+ ),
92
+ Command(
93
+ key='sweep',
94
+ name='Retention sweep',
95
+ description='run the sweep now rather than waiting for the cadence',
96
+ tags=('retention',),
97
+ permission='retention/sweep',
98
+ run='sweep',
99
+ reply='{evicted: {kind: n}, held_back: n, scanned: n}',
100
+ ),
101
+ )
102
+ STORES = (
103
+ st.Store(key='evicted', description='total ever evicted, per event kind',
104
+ kind=st.TABLE, shape=int, bound=1024, keyed_by='kind', evict=st.LRU),
105
+ )
106
+
107
+ def __init__(self, store: Any, cfg: Any, clock: Any, node: Any = None) -> None:
108
+ Compartment.__init__(self, node, clock)
109
+ self.store = store
110
+ self._cfg = cfg # a callable returning the live config
111
+ self.swept_at: Optional[int] = None
112
+ self.evicted: st.Table[int, int] # kind -> total ever evicted here
113
+ self.held_back: int = 0 # events an age would have taken and a shard kept
114
+
115
+ # ---- what the operator asked for ----------------------------------------
116
+ def ages(self) -> dict[int, int]:
117
+ """{kind: seconds}. Absent, empty or unreadable all mean keep everything."""
118
+ raw = (self._cfg() or {}).get('retention') or {}
119
+ if not isinstance(raw, dict):
120
+ return {}
121
+ out: dict[int, int] = {}
122
+ for k, v in raw.items():
123
+ try:
124
+ kind, age = int(k), int(v)
125
+ except (TypeError, ValueError):
126
+ self.refusals.refuse('retention', 'not a kind and a number of seconds',
127
+ at=self.clock(), detail={'kind': str(k)[:16], 'age': str(v)[:16]})
128
+ continue
129
+ if age > 0:
130
+ out[kind] = age
131
+ return out
132
+
133
+ def protected(self) -> list[Mapping[str, Any]]:
134
+ """The shard filters a coverage claim or ProjectedNodeStore covers. Never evicted."""
135
+ cov = (self._cfg() or {}).get('coverage') or []
136
+ out: list[Mapping[str, Any]] = [
137
+ c['filter'] for c in cov
138
+ if isinstance(c, dict) and isinstance(c.get('filter'), dict)
139
+ ]
140
+ if hasattr(self.store, 'protected_filters'):
141
+ for f in self.store.protected_filters():
142
+ if f not in out:
143
+ out.append(f)
144
+ return out
145
+
146
+ # ---- the sweep ----------------------------------------------------------
147
+ def sweep(self, now: Optional[int] = None) -> Mapping[str, Any]:
148
+ now = now if now is not None else self.clock()
149
+ ages, guard = self.ages(), self.protected()
150
+ evicted: dict[int, int] = {}
151
+ held_back = 0
152
+ scanned = 0
153
+ if not ages:
154
+ self.swept_at = now
155
+ return {'evicted': evicted, 'held_back': held_back, 'scanned': scanned}
156
+ for kind, age in sorted(ages.items()):
157
+ cutoff = now - age
158
+ for ev in self._older(kind, cutoff):
159
+ scanned += 1
160
+ if any(store_mod.matches(ev, f) for f in guard):
161
+ # Not a preference and not overridable: the fingerprint this
162
+ # node published over that shard has to stay reproducible.
163
+ held_back += 1
164
+ self.held_back += 1
165
+ self.refusals.refuse('retention', 'inside a shard this node claims coverage for',
166
+ at=now, detail={'id': ev['id'][:16], 'kind': kind})
167
+ continue
168
+ self.store.forget(ev['id'])
169
+ evicted[kind] = evicted.get(kind, 0) + 1
170
+ self.evicted[kind] = self.evicted.get(kind, 0) + 1
171
+ if sum(evicted.values()) > 0 and hasattr(self.store, 'vacuum'):
172
+ try:
173
+ self.store.vacuum(pages=50)
174
+ except Exception:
175
+ pass
176
+ self.swept_at = now
177
+ return {'evicted': evicted, 'held_back': held_back, 'scanned': scanned}
178
+
179
+ def _older(self, kind: int, cutoff: int) -> Iterator[Mapping[str, Any]]:
180
+ """One page at a time, oldest first, so the sweep costs a page and not
181
+ the backlog. The first sweep after a policy is set is the big one."""
182
+ after: Optional[tuple[int, str]] = None
183
+ while True:
184
+ page = self.store.by_kind_before(kind, cutoff, SWEEP_PAGE, after)
185
+ if not page:
186
+ return
187
+ for ev in page:
188
+ yield ev
189
+ if len(page) < SWEEP_PAGE:
190
+ return
191
+ after = (page[-1]['created_at'], page[-1]['id'])
192
+
193
+ # ---- the contract -------------------------------------------------------
194
+ def _report_sweep(self, r: Mapping[str, Any]) -> list[tuple[Any, ...]]:
195
+ did: list[tuple[Any, ...]] = []
196
+ if r.get('evicted'):
197
+ did.append(('evicted', sum(r['evicted'].values()), r['evicted']))
198
+ if r.get('held_back'):
199
+ did.append(('held back', r['held_back'], 'inside a claimed shard'))
200
+ return did
201
+
202
+ def state(self) -> RetentionStateReport:
203
+ return {'ages': {str(k): v for k, v in self.ages().items()},
204
+ 'protected': len(self.protected()),
205
+ 'swept_at': self.swept_at,
206
+ 'evicted': {str(k): v for k, v in self.evicted.items()},
207
+ 'held_back': self.held_back}
208
+
209
+ def trouble(self) -> list[str]:
210
+ out: list[str] = []
211
+ if self.held_back:
212
+ out.append('%d event(s) an age would have taken are inside a shard this node claims, and '
213
+ 'were kept: a coverage fingerprint has to stay reproducible' % self.held_back)
214
+ return out
@@ -0,0 +1,12 @@
1
+ """Types and records for the Retention compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, TypedDict
5
+
6
+
7
+ class RetentionStateReport(TypedDict):
8
+ ages: dict[str, int]
9
+ protected: int
10
+ swept_at: Optional[int]
11
+ evicted: dict[str, int]
12
+ held_back: int
@@ -0,0 +1,35 @@
1
+ """sync/1.0 over the wire.
2
+
3
+ fleetmesh_core/sync.py runs a session as two objects handing each other messages in one
4
+ process. Here the two objects are on two nodes and the messages travel as
5
+ DIDComm envelopes through the transport, on one thread per session. The
6
+ reference still decides everything: what to ask for, what to serve, what to
7
+ count, what to find.
8
+
9
+ Two things are added at the wire. Every delivered event travels inside a
10
+ delivery envelope with a hop signed by the node that sent it, carried as a
11
+ DIDComm attachment named delivery-envelope, and the receiver verifies every
12
+ hop before the reference sees the event; an event whose envelope does not
13
+ verify is not delivered and is a finding. And each side's session is bound
14
+ to the grant that governs it: the initiator pulls under the grant it holds
15
+ from the peer, the responder serves under the grant it issued.
16
+
17
+ One thing follows a session. A responder that asserts the shard first-hand,
18
+ over a range that has closed, and has just served a peer to equality over
19
+ that whole range, may hand the shard over: a signed statement the peer
20
+ carries in its own coverage claim so it can assert the same set. The offer
21
+ is the responder's to make (config `handover`), the statement copies its
22
+ terms from the claim this node has already published and nothing else, and
23
+ the receiver keeps it only if it fingerprints to what the receiver now holds.
24
+
25
+ A package by seam: envelope.py (the wire vocabulary), core.py (the
26
+ compartment), initiator.py (`pull`), responder.py (`handle`), handover.py.
27
+ Every name the module exported is re-exported here.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ from .core import SyncPeer
32
+ from .envelope import ATTACHMENT, MAX_SESSIONS_PER_PEER, PIURI, SESSION_IDLE, SyncError, envelope_attachment, verified_events
33
+
34
+ __all__ = ['ATTACHMENT', 'MAX_SESSIONS_PER_PEER', 'PIURI', 'SESSION_IDLE', 'SyncError', 'envelope_attachment',
35
+ 'verified_events', 'SyncPeer']