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,19 @@
1
+ """p2panda identity adapter for Fleetmesh.
2
+
3
+ Maps the node's Ed25519 transport key to a p2panda author identity.
4
+ Conforms to ProtocolIdentityAdapter (fleetmesh_core.wire.interfaces).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from node.protocols.base import Ed25519IdentityAdapter
9
+
10
+ __all__ = [
11
+ 'P2PandaIdentityAdapter',
12
+ ]
13
+
14
+
15
+ class P2PandaIdentityAdapter(Ed25519IdentityAdapter):
16
+ """Translates between Fleetmesh keys and p2panda author identities."""
17
+
18
+ uri_prefixes: tuple[str, ...] = ("p2panda:", "panda:")
19
+
@@ -0,0 +1,236 @@
1
+ """fleetmesh-protocol-p2panda: p2panda protocol package for Fleetmesh.
2
+
3
+ Conforms to ProtocolPackage (fleetmesh_core.wire.interfaces.ProtocolPackage).
4
+ Mounts p2panda operation log ingress onto ProtocolHostRouter, provides
5
+ P2PandaRung for TransportRouter ladder, and manages append-only SQLite persistence.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import urllib.parse
12
+ from typing import Any, Mapping, Optional
13
+
14
+ from fleetmesh_core.wire.interfaces import (
15
+ ProtocolHealth,
16
+ ProtocolHostRouter,
17
+ ProtocolIdentityAdapter,
18
+ )
19
+ from fleetmesh_core.wire.p2panda import P2PandaOperation, P2PandaOperationDict
20
+ from node.protocols.base import BaseProtocolPackage
21
+ from .identity import P2PandaIdentityAdapter
22
+ from .rung import P2PandaRung
23
+ from .store import P2PandaLogStore, P2PandaStoreError
24
+
25
+ __all__ = [
26
+ 'P2PandaProtocolPackage',
27
+ ]
28
+
29
+ log = logging.getLogger(__name__)
30
+
31
+
32
+ class P2PandaProtocolPackage(BaseProtocolPackage):
33
+ """The p2panda protocol suite for Fleetmesh."""
34
+
35
+ name: str = 'p2panda'
36
+ version: str = '0.4.0'
37
+ standards: tuple[str, ...] = (
38
+ 'p2panda-v0.4.0',
39
+ 'cbor-rfc8949',
40
+ 'ed25519-chain',
41
+ )
42
+
43
+ def __init__(
44
+ self,
45
+ node_dir: Optional[str] = None,
46
+ keystore: Optional[Any] = None,
47
+ blob_store: Optional[Any] = None,
48
+ store: Optional[P2PandaLogStore] = None,
49
+ **kwargs: Any,
50
+ ) -> None:
51
+ self.node_dir = node_dir
52
+ self.keystore = keystore
53
+ self.blob_store = blob_store
54
+ self.config: Mapping[str, Any] = {}
55
+ self._enabled: bool = True
56
+ self._running: bool = False
57
+ self._messages_in: int = 0
58
+ self._messages_out: int = 0
59
+
60
+ # Identity Adapter
61
+ if keystore is not None:
62
+ self.identity: Optional[P2PandaIdentityAdapter] = P2PandaIdentityAdapter.from_keystore(keystore)
63
+ else:
64
+ self.identity = None
65
+
66
+ # Persistent Log Store
67
+ if store is not None:
68
+ self.store: P2PandaLogStore = store
69
+ elif node_dir:
70
+ import os
71
+ db_path = os.path.join(node_dir, "p2panda.db")
72
+ self.store = P2PandaLogStore(db_path)
73
+ else:
74
+ self.store = P2PandaLogStore(":memory:")
75
+
76
+ # Outbound Transport Rung
77
+ self.rung: P2PandaRung = P2PandaRung(
78
+ store=self.store,
79
+ identity=self.identity,
80
+ enabled=self._enabled,
81
+ )
82
+
83
+ self._host: Optional[ProtocolHostRouter] = None
84
+
85
+ def configure(self, config: Mapping[str, Any]) -> None:
86
+ self.config = dict(config)
87
+ self._enabled = bool(self.config.get('enabled', True))
88
+ self.rung.enabled = self._enabled
89
+
90
+ def identity_adapter(self, keystore: Any) -> Optional[ProtocolIdentityAdapter]:
91
+ if keystore is not None:
92
+ return P2PandaIdentityAdapter.from_keystore(keystore)
93
+ return self.identity
94
+
95
+ def mount_ingress(self, host: ProtocolHostRouter) -> None:
96
+ if not self._enabled:
97
+ return
98
+ self._host = host
99
+
100
+ from node.transport import ws
101
+
102
+ # 1. GET /p2panda/status
103
+ def _handle_status(conn: Any, req: Any) -> bool:
104
+ body_dict = {
105
+ 'status': 'online' if self._running else 'configured',
106
+ 'name': self.name,
107
+ 'version': self.version,
108
+ 'author': self.identity.author_id if self.identity else '',
109
+ 'operations_count': self.store.count_operations(),
110
+ 'authors_count': len(self.store.list_authors()),
111
+ }
112
+ body = json.dumps(body_dict, indent=2).encode('utf-8')
113
+ ws.http_response(conn, "200 OK", body, "application/json")
114
+ return True
115
+
116
+ # 2. GET & POST /p2panda/operations
117
+ def _op_to_json_dict(op: P2PandaOperation) -> P2PandaOperationDict:
118
+ return op.to_dict(json_safe=True)
119
+
120
+ def _handle_operations(conn: Any, req: Any) -> bool:
121
+ method = getattr(req, 'method', 'GET').upper()
122
+ uri = getattr(req, 'path', '')
123
+ parsed = urllib.parse.urlparse(uri)
124
+
125
+ if method == 'GET':
126
+ qparams = urllib.parse.parse_qs(parsed.query)
127
+ author = qparams.get('author', [None])[0]
128
+ target_route = parsed.path
129
+ if not author and target_route.startswith('/p2panda/operations/'):
130
+ tail = target_route[len('/p2panda/operations/'):].strip('/')
131
+ if tail:
132
+ single_op = self.store.get_operation(tail)
133
+ if single_op is not None:
134
+ ws.http_response(
135
+ conn,
136
+ "200 OK",
137
+ json.dumps(_op_to_json_dict(single_op)).encode('utf-8'),
138
+ "application/json",
139
+ )
140
+ return True
141
+ author = tail
142
+
143
+ if author:
144
+ from_seq = int(qparams.get('from_seq', [0])[0])
145
+ limit = int(qparams.get('limit', [100])[0])
146
+ ops = self.store.get_log(author, from_seq=from_seq, limit=limit)
147
+ ops_data = [_op_to_json_dict(op) for op in ops]
148
+ body = json.dumps({'author': author, 'operations': ops_data}).encode('utf-8')
149
+ ws.http_response(conn, "200 OK", body, "application/json")
150
+ return True
151
+ else:
152
+ authors = self.store.list_authors()
153
+ body = json.dumps({'authors': authors, 'total_operations': self.store.count_operations()}).encode('utf-8')
154
+ ws.http_response(conn, "200 OK", body, "application/json")
155
+ return True
156
+
157
+ elif method == 'POST':
158
+ try:
159
+ payload = ws.read_body(conn, req)
160
+ except Exception as e:
161
+ ws.http_response(conn, "400 Bad Request", json.dumps({'error': str(e)}).encode('utf-8'))
162
+ return True
163
+
164
+ headers = getattr(req, 'headers', {})
165
+ content_type = headers.get(b'content-type', b'').decode('utf-8', errors='ignore').lower()
166
+
167
+ try:
168
+ if 'cbor' in content_type:
169
+ op = P2PandaOperation.from_cbor(payload)
170
+ else:
171
+ op_dict = json.loads(payload.decode('utf-8'))
172
+ op = P2PandaOperation.from_dict(op_dict)
173
+
174
+ inserted = self.store.append_operation(op, verify_chain=True)
175
+ self._messages_in += 1
176
+ status = "201 Created" if inserted else "200 OK"
177
+ ws.http_response(
178
+ conn,
179
+ status,
180
+ json.dumps({'op_id': op.id, 'inserted': inserted, 'seq_num': op.seq_num}).encode('utf-8'),
181
+ "application/json",
182
+ )
183
+ return True
184
+ except P2PandaStoreError as e:
185
+ ws.http_response(conn, "422 Unprocessable Entity", json.dumps({'error': str(e)}).encode('utf-8'))
186
+ return True
187
+ except Exception as e:
188
+ ws.http_response(conn, "400 Bad Request", json.dumps({'error': str(e)}).encode('utf-8'))
189
+ return True
190
+
191
+ return False
192
+
193
+ host.mount_route('/p2panda/status', _handle_status, methods=('GET',), owner=self.name)
194
+ host.mount_route('/p2panda/operations', _handle_operations, methods=('GET', 'POST'), owner=self.name)
195
+
196
+ def transport_rung(self, context: Any) -> Optional[Any]:
197
+ if not self._enabled:
198
+ return None
199
+ return self.rung
200
+
201
+ def compartments(self) -> tuple[type[Any], ...]:
202
+ return ()
203
+
204
+ def start(self) -> None:
205
+ if not self._enabled:
206
+ return
207
+ self._running = True
208
+
209
+ def stop(self) -> None:
210
+ self._running = False
211
+ if self.store is not None:
212
+ self.store.close()
213
+
214
+ def health(self) -> ProtocolHealth:
215
+ if not self._enabled:
216
+ return ProtocolHealth(
217
+ healthy=True,
218
+ status="disabled",
219
+ connections_active=0,
220
+ messages_in=0,
221
+ messages_out=0,
222
+ errors=(),
223
+ )
224
+ return ProtocolHealth(
225
+ healthy=True,
226
+ status="serving" if self._running else "configured",
227
+ connections_active=0,
228
+ messages_in=self._messages_in,
229
+ messages_out=self._messages_out,
230
+ errors=(),
231
+ )
232
+
233
+ def descriptor_capabilities(self) -> tuple[tuple[str, str], ...]:
234
+ if not self._enabled:
235
+ return ()
236
+ return (('c', 'p2panda'), ('c', 'cbor'))
@@ -0,0 +1,152 @@
1
+ """p2panda transport rung for TransportRouter ladder.
2
+
3
+ Standard library only. Delivers messages wrapped as append-only p2panda operations
4
+ to local log and sync peers.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any, Callable, Mapping, Optional
10
+
11
+ from fleetmesh_core.wire.cbor import cbor_encode
12
+ from fleetmesh_core.wire.p2panda import P2PandaOperation
13
+ from node.transport.const import TransportError
14
+ from node.transport.ladder import FaultLog
15
+ from node.transport.rung import Rung, Verdict, endpoints_of
16
+ from .identity import P2PandaIdentityAdapter
17
+ from .store import P2PandaLogStore
18
+
19
+ __all__ = [
20
+ 'P2PandaRung',
21
+ ]
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+
26
+ class P2PandaRung(Rung):
27
+ """Outbound transport rung delivering through p2panda append-only operation chains."""
28
+
29
+ name = 'p2panda'
30
+ rank = 15
31
+
32
+ def __init__(
33
+ self,
34
+ store: Optional[P2PandaLogStore] = None,
35
+ identity: Optional[P2PandaIdentityAdapter] = None,
36
+ enabled: bool = True,
37
+ clock: Optional[Callable[[], int]] = None,
38
+ ) -> None:
39
+ self.store = store
40
+ self.identity = identity
41
+ self.enabled = enabled
42
+ self.clock = clock
43
+ self.faults = FaultLog(rung=self.name, clock=clock)
44
+
45
+ def available(self) -> bool:
46
+ """The local side of this rung is up and an identity is configured."""
47
+ return (
48
+ self.enabled
49
+ and self.identity is not None
50
+ and bool(self.identity.author_id)
51
+ and self.store is not None
52
+ )
53
+
54
+ def target_of(self, desc: Optional[Mapping[str, Any]], theirs: Mapping[str, int]) -> Optional[str]:
55
+ """Where this rung would reach the peer that published desc."""
56
+ if desc is not None:
57
+ _, where = endpoints_of(desc)
58
+ if 'p2panda' in where:
59
+ return where['p2panda']
60
+ for t in desc.get('tags', []):
61
+ if not t:
62
+ continue
63
+ if t[0] == 'c' and len(t) > 1 and t[1] == 'p2panda':
64
+ return ''
65
+ if 'p2panda' in theirs:
66
+ return ''
67
+ return None
68
+
69
+ def deliver(self, message: Mapping[str, Any], to_pubkey: str, target: str, anonymous: bool) -> Verdict:
70
+ """Deliver message by signing and appending a p2panda operation to the log."""
71
+ if not self.available():
72
+ raise TransportError('p2panda rung unavailable or not configured with signing identity and store')
73
+
74
+ assert self.identity is not None
75
+ assert self.store is not None
76
+
77
+ if self.identity._private_key is None:
78
+ raise TransportError('p2panda identity does not have private signing key configured')
79
+
80
+ author_id = self.identity.author_id
81
+ try:
82
+ latest_seq = self.store.get_latest_seq(author_id)
83
+ next_seq = latest_seq + 1
84
+ backlink = None
85
+ if latest_seq >= 0:
86
+ latest_op = self.store.get_latest_operation(author_id)
87
+ if latest_op is not None:
88
+ backlink = latest_op.id
89
+
90
+ # Canonical CBOR serialization of the payload
91
+ payload_bytes = cbor_encode(dict(message), canonical=True)
92
+
93
+ extensions = {
94
+ 'target': to_pubkey,
95
+ 'anonymous': bool(anonymous),
96
+ }
97
+
98
+ op = P2PandaOperation.create(
99
+ public_key=author_id,
100
+ body=payload_bytes,
101
+ seq_num=next_seq,
102
+ backlink=backlink,
103
+ private_key=self.identity._private_key,
104
+ extensions=extensions,
105
+ )
106
+
107
+ self.store.append_operation(op, verify_chain=True)
108
+
109
+ # If target specifies an HTTP/HTTPS endpoint, push operation
110
+ if target and (target.startswith('http://') or target.startswith('https://')):
111
+ self._push_http(target, op)
112
+
113
+ return {self.name: (True, f'delivered via operation {op.id[:16]} seq={op.seq_num}')}
114
+
115
+ except Exception as e:
116
+ self.faults.record(f'deliver failed: {e}', ident=to_pubkey[:16] if to_pubkey else None)
117
+ raise TransportError(f'p2panda delivery failed: {e}') from e
118
+
119
+ def _push_http(self, target_url: str, op: P2PandaOperation) -> None:
120
+ import urllib.error
121
+ import urllib.request
122
+
123
+ url = target_url
124
+ if not url.endswith('/p2panda/operations') and not url.endswith('/operations'):
125
+ url = f"{url.rstrip('/')}/p2panda/operations"
126
+
127
+ data = op.to_cbor()
128
+ req = urllib.request.Request(
129
+ url,
130
+ data=data,
131
+ headers={
132
+ 'Content-Type': 'application/cbor',
133
+ 'User-Agent': 'Fleetmesh-p2panda/0.4.0',
134
+ },
135
+ method='POST',
136
+ )
137
+ try:
138
+ with urllib.request.urlopen(req, timeout=5.0) as resp:
139
+ if resp.status not in (200, 201, 202):
140
+ raise TransportError(f"HTTP upstream returned status {resp.status}")
141
+ except urllib.error.URLError as e:
142
+ raise TransportError(f"Failed to push operation to {url}: {e}") from e
143
+
144
+ def health(self) -> Optional[Mapping[str, Any]]:
145
+ return {
146
+ 'available': self.available(),
147
+ 'author_id': self.identity.author_id if self.identity else '',
148
+ 'ops_stored': self.store.count_operations() if self.store else 0,
149
+ }
150
+
151
+ def fault_log(self) -> Optional[FaultLog]:
152
+ return self.faults
@@ -0,0 +1,273 @@
1
+ """p2panda SQLite persistent operation log store.
2
+
3
+ Standard library only. Manages persistence and chain verification for:
4
+ - Append-only cryptographic operation logs.
5
+ - Monotonic sequence numbers and backlink verification.
6
+ - Author log indexing and query pagination.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from typing import Any, Optional, Sequence
13
+
14
+ from fleetmesh_core.conformance.errors import FleetmeshError
15
+ from fleetmesh_core.wire.p2panda import P2PandaHeader, P2PandaOperation
16
+ from node.compartment.schema import Column, Index, Table
17
+ from node.storage import SqliteStore, store_errors
18
+
19
+ __all__ = [
20
+ 'P2PandaStoreError',
21
+ 'P2PandaLogStore',
22
+ ]
23
+
24
+
25
+ class P2PandaStoreError(FleetmeshError):
26
+ """Base error for p2panda storage operations."""
27
+ pass
28
+
29
+
30
+ from node.protocols.isotime import now_iso as _now_iso # noqa: E402
31
+
32
+
33
+ _COLS = ('op_id, author, seq_num, backlink, previous_seq_num, payload_size, payload_hash, '
34
+ 'timestamp, signature, version, body, extensions_json, created_at')
35
+ """The column order `_row_to_op` reads positionally.
36
+
37
+ The selects said `SELECT *` and the rows were indexed by name through a
38
+ `sqlite3.Row` factory. The base hands back plain tuples, so the order is
39
+ written down once here and every select names it - which is sturdier anyway:
40
+ `SELECT *` plus positional indexing would break on the next column added.
41
+ """
42
+
43
+
44
+ class P2PandaLogStore(SqliteStore):
45
+ """Append-only p2panda operation logs, one row per operation.
46
+
47
+ A `SqliteStore` rather than a hand-rolled connection: the base owns the
48
+ connection, the lock, the per-store schema version, transaction control and
49
+ the dialect that rewrites placeholders and upserts for another backend.
50
+ This store opened its own connection per call and declared its schema as a
51
+ SQL string, so it was SQLite-only and invisible to the locator - see
52
+ docs/storage-backends.md.
53
+ """
54
+
55
+ SCHEMA = (
56
+ Table('p2panda_operations', (
57
+ Column('op_id', 'TEXT', primary_key=True),
58
+ Column('author', 'TEXT', not_null=True),
59
+ Column('seq_num', 'INTEGER', not_null=True),
60
+ Column('backlink', 'TEXT'),
61
+ Column('previous_seq_num', 'INTEGER'),
62
+ Column('payload_size', 'INTEGER', not_null=True),
63
+ Column('payload_hash', 'TEXT', not_null=True),
64
+ Column('timestamp', 'INTEGER', not_null=True),
65
+ Column('signature', 'TEXT', not_null=True),
66
+ Column('version', 'INTEGER', not_null=True, default='1'),
67
+ Column('body', 'BLOB'),
68
+ Column('extensions_json', 'TEXT'),
69
+ Column('created_at', 'TEXT', not_null=True),
70
+ ), indices=(
71
+ # The log's real constraint: one operation per (author, seq_num).
72
+ # It was a table-level UNIQUE in the SQL; a unique index is the
73
+ # typed layer's way of saying it, and it also serves the lookup
74
+ # the old non-unique index on the same columns existed for.
75
+ Index('idx_p2p_ops_author_seq', ('author', 'seq_num'), unique=True),
76
+ Index('idx_p2p_ops_backlink', ('backlink',)),
77
+ )),
78
+ )
79
+
80
+ def __init__(self, db_path: str = ":memory:"):
81
+ if db_path != ":memory:":
82
+ os.makedirs(os.path.dirname(os.path.abspath(db_path)), exist_ok=True)
83
+ super().__init__(db_path)
84
+
85
+ def _row_to_op(self, row: Sequence[Any]) -> P2PandaOperation:
86
+ ext = None
87
+ if row[11]:
88
+ try:
89
+ ext = json.loads(row[11])
90
+ except Exception:
91
+ ext = None
92
+
93
+ header = P2PandaHeader(
94
+ version=int(row[9]),
95
+ public_key=str(row[1]),
96
+ signature=str(row[8]) if row[8] else None,
97
+ payload_size=int(row[5]),
98
+ payload_hash=str(row[6]),
99
+ timestamp=int(row[7]),
100
+ seq_num=int(row[2]),
101
+ backlink=str(row[3]) if row[3] else None,
102
+ previous_seq_num=int(row[4]) if row[4] is not None else None,
103
+ extensions=ext,
104
+ )
105
+ body = bytes(row[10]) if row[10] is not None else None
106
+ return P2PandaOperation(header=header, body=body)
107
+
108
+ def append_operation(self, op: P2PandaOperation, verify_chain: bool = True) -> bool:
109
+ """Append an operation to the author's log.
110
+
111
+ Validates cryptographic integrity and chain monotonicity if verify_chain is True.
112
+ Returns True if newly inserted, False if idempotent duplicate.
113
+ """
114
+ if verify_chain:
115
+ if not op.verify_signature():
116
+ raise P2PandaStoreError(f"Signature verification failed for operation {op.id}")
117
+ if not op.verify_payload():
118
+ raise P2PandaStoreError(f"Payload integrity verification failed for operation {op.id}")
119
+
120
+ author = op.author.lower()
121
+ op_id = op.id.lower()
122
+
123
+ with self._lock:
124
+ try:
125
+ with self.transaction() as cur:
126
+
127
+ # Check if identical op_id already exists
128
+ cur.execute("SELECT op_id FROM p2panda_operations WHERE op_id = ?", (op_id,))
129
+ if cur.fetchone() is not None:
130
+ return False
131
+
132
+ if verify_chain:
133
+ # Query latest operation for author
134
+ cur.execute(
135
+ "SELECT op_id, seq_num, timestamp FROM p2panda_operations WHERE author = ? ORDER BY seq_num DESC LIMIT 1",
136
+ (author,),
137
+ )
138
+ latest_row = cur.fetchone()
139
+
140
+ if latest_row is None:
141
+ # Genesis operation for author
142
+ if op.seq_num != 0:
143
+ raise P2PandaStoreError(
144
+ f"Genesis operation for author {author} must have seq_num 0, got {op.seq_num}"
145
+ )
146
+ if op.backlink is not None and op.backlink != '':
147
+ raise P2PandaStoreError(
148
+ f"Genesis operation for author {author} must have null backlink, got {op.backlink}"
149
+ )
150
+ else:
151
+ latest_op_id = str(latest_row[0]).lower()
152
+ latest_seq = int(latest_row[1])
153
+ latest_ts = int(latest_row[2])
154
+
155
+ expected_seq = latest_seq + 1
156
+ if op.seq_num != expected_seq:
157
+ if op.seq_num <= latest_seq:
158
+ raise P2PandaStoreError(
159
+ f"Fork or duplicate sequence conflict at seq_num={op.seq_num} for author {author}"
160
+ )
161
+ raise P2PandaStoreError(
162
+ f"Missing predecessor operation: author {author} is at seq_num {latest_seq}, got {op.seq_num}"
163
+ )
164
+
165
+ if op.backlink is None or op.backlink.lower() != latest_op_id:
166
+ raise P2PandaStoreError(
167
+ f"Backlink mismatch: expected {latest_op_id}, got {op.backlink}"
168
+ )
169
+
170
+ if op.header.timestamp < latest_ts:
171
+ raise P2PandaStoreError(
172
+ f"Operation timestamp ({op.header.timestamp}) is earlier than predecessor ({latest_ts})"
173
+ )
174
+
175
+ ext_json = json.dumps(op.header.extensions) if op.header.extensions is not None else None
176
+
177
+ cur.execute(
178
+ """
179
+ INSERT INTO p2panda_operations (
180
+ op_id, author, seq_num, backlink, previous_seq_num,
181
+ payload_size, payload_hash, timestamp, signature,
182
+ version, body, extensions_json, created_at
183
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
184
+ """,
185
+ (
186
+ op_id,
187
+ author,
188
+ op.seq_num,
189
+ op.backlink.lower() if op.backlink else None,
190
+ op.header.previous_seq_num,
191
+ op.header.payload_size,
192
+ op.header.payload_hash.lower(),
193
+ op.header.timestamp,
194
+ op.header.signature.lower() if op.header.signature else '',
195
+ op.header.version,
196
+ op.body,
197
+ ext_json,
198
+ _now_iso(),
199
+ ),
200
+ )
201
+ return True
202
+ except store_errors() as e:
203
+ # A sequence collision or the author-seq uniqueness violation.
204
+ # `store_errors()` rather than `sqlite3.IntegrityError`: the
205
+ # narrow catch stays narrow and stays correct for a second
206
+ # driver with its own exception hierarchy.
207
+ raise P2PandaStoreError(f"Database integrity conflict: {e}") from e
208
+
209
+ def get_operation(self, op_id: str) -> Optional[P2PandaOperation]:
210
+ """Fetch an operation by its op_id."""
211
+ clean_id = op_id.lower().strip()
212
+ with self._lock:
213
+ row = self._one("SELECT " + _COLS + " FROM p2panda_operations WHERE op_id = ?", (clean_id,))
214
+ return self._row_to_op(row) if row else None
215
+
216
+ def get_operation_by_seq(self, author: str, seq_num: int) -> Optional[P2PandaOperation]:
217
+ """Fetch an operation by author public key and sequence number."""
218
+ clean_author = author.lower().strip()
219
+ with self._lock:
220
+ row = self._one(
221
+ "SELECT " + _COLS + " FROM p2panda_operations WHERE author = ? AND seq_num = ?",
222
+ (clean_author, int(seq_num)),
223
+ )
224
+ return self._row_to_op(row) if row else None
225
+
226
+ def get_latest_operation(self, author: str) -> Optional[P2PandaOperation]:
227
+ """Fetch the latest operation in an author's log."""
228
+ clean_author = author.lower().strip()
229
+ with self._lock:
230
+ row = self._one(
231
+ "SELECT " + _COLS + " FROM p2panda_operations "
232
+ "WHERE author = ? ORDER BY seq_num DESC LIMIT 1",
233
+ (clean_author,),
234
+ )
235
+ return self._row_to_op(row) if row else None
236
+
237
+ def get_latest_seq(self, author: str) -> int:
238
+ """Return the highest seq_num for author, or -1 if no operations exist."""
239
+ clean_author = author.lower().strip()
240
+ with self._lock:
241
+ row = self._one(
242
+ "SELECT MAX(seq_num) FROM p2panda_operations WHERE author = ?",
243
+ (clean_author,),
244
+ )
245
+ if row and row[0] is not None:
246
+ return int(row[0])
247
+ return -1
248
+
249
+ def get_log(self, author: str, from_seq: int = 0, limit: int = 100) -> list[P2PandaOperation]:
250
+ """Return ordered operations for an author from from_seq onwards."""
251
+ clean_author = author.lower().strip()
252
+ with self._lock:
253
+ rows = self._all(
254
+ "SELECT " + _COLS + " FROM p2panda_operations "
255
+ "WHERE author = ? AND seq_num >= ? ORDER BY seq_num ASC LIMIT ?",
256
+ (clean_author, int(from_seq), max(1, min(int(limit), 100_000))),
257
+ )
258
+ return [self._row_to_op(r) for r in rows]
259
+
260
+ def count_operations(self) -> int:
261
+ """Return total count of operations across all authors."""
262
+ with self._lock:
263
+ row = self._one("SELECT COUNT(*) AS total FROM p2panda_operations")
264
+ return int(row[0]) if row else 0
265
+
266
+ def list_authors(self) -> list[str]:
267
+ """Return a sorted list of unique authors with operations in store."""
268
+ with self._lock:
269
+ return [str(r[0]) for r in
270
+ self._all("SELECT DISTINCT author FROM p2panda_operations ORDER BY author ASC")]
271
+
272
+ def close(self) -> None:
273
+ super().close()