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,46 @@
1
+ #!/usr/bin/env python3
2
+ """The reference out-of-process plugin: follow a node, print, ask.
3
+
4
+ python3 -m node.examples.follower ~/.meshnode [topic ...]
5
+
6
+ Everything it does goes through the control socket (docs/extending.md,
7
+ "Out-of-process plugins"). It holds no keys, runs as whoever started it,
8
+ and when a grant is issued it asks the node who the peer is - the one
9
+ "act" in the example, chosen because it reads rather than changes.
10
+ """
11
+ import json
12
+ import sys
13
+
14
+ from node.app import socket as control
15
+
16
+
17
+ def follow(node_dir, topics=(), out=sys.stdout, limit=None):
18
+ """Print envelopes until the node goes away, `limit` envelopes or ^C.
19
+
20
+ Returns how many envelopes it printed; the test counts them.
21
+ """
22
+ args = {'topics': list(topics)} if topics else {}
23
+ it = control.stream(node_dir, 'subscribe', timeout=None, **args)
24
+ ack = next(it)
25
+ print('following from seq %s' % ack['seq'], file=sys.stderr)
26
+ seen = 0
27
+ for env in it:
28
+ line = {'seq': env['seq'], 'topic': env['topic'], 'data': env['data']}
29
+ if env['topic'] == 'grant.issued':
30
+ # A plugin acts with the operator's verbs and no others.
31
+ peers = control.call(node_dir, 'peers')
32
+ line['peer_known'] = any(p.get('peer') == env['data']['peer'] for p in peers)
33
+ print(json.dumps(line), file=out, flush=True)
34
+ seen += 1
35
+ if limit and seen >= limit:
36
+ break
37
+ return seen
38
+
39
+
40
+ if __name__ == '__main__':
41
+ if len(sys.argv) < 2:
42
+ sys.exit(__doc__)
43
+ try:
44
+ follow(sys.argv[1], sys.argv[2:])
45
+ except KeyboardInterrupt:
46
+ pass
node/interfaces.py ADDED
@@ -0,0 +1,235 @@
1
+ """Formal interfaces, protocols, and data contracts for meshnode.
2
+
3
+ Standard library only. All wire types reflect the normative JSON Schemas in
4
+ schemas/ and the protocol invariants verified in fleetmesh_core/.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import (
9
+ Any,
10
+ Counter,
11
+ Mapping,
12
+ Optional,
13
+ Protocol,
14
+ Sequence,
15
+ TypedDict,
16
+ runtime_checkable,
17
+ )
18
+
19
+ # Core wire, event, filter, and DIDComm shapes are defined once in
20
+ # fleetmesh_core.wire.interfaces and re-exported here to ensure exact parity.
21
+ from fleetmesh_core.wire.interfaces import (
22
+ DIDCommAttachment,
23
+ DIDCommAttachmentData,
24
+ DIDCommMessage,
25
+ DeliveryEnvelope,
26
+ DeliveryHop,
27
+ EventLike,
28
+ GrantLimits,
29
+ GrantPayload,
30
+ GrantScope,
31
+ NostrEvent,
32
+ NostrFilter,
33
+ Tag,
34
+ TagList,
35
+ UnsignedNostrEvent,
36
+ ProtocolHealth,
37
+ RouteMatch,
38
+ ProtocolHostRouter,
39
+ ProtocolIdentityAdapter,
40
+ ProtocolPackage,
41
+ )
42
+
43
+ __all__ = [
44
+ 'DeliveryEnvelope', 'DeliveryHop', 'GrantLimits', 'GrantPayload', 'GrantScope',
45
+ 'Tag', 'TagList', 'NostrEvent', 'UnsignedNostrEvent', 'EventLike', 'NostrFilter',
46
+ 'DIDCommAttachmentData', 'DIDCommAttachment', 'DIDCommMessage',
47
+ 'RelayHealthRow', 'AwaitingAckRow', 'ThrottledRow', 'ReceiptRefusedRow',
48
+ 'GrantStatusRow', 'PendingGrantRow', 'EvaluationRow', 'RepricedRow',
49
+ 'HealthStatusDict', 'NodeStatusDict', 'CommandSpec',
50
+ 'KeyStoreProtocol', 'TransportProtocol', 'EventStoreProtocol', 'CompartmentProtocol',
51
+ 'ProtocolHealth', 'RouteMatch', 'ProtocolHostRouter', 'ProtocolIdentityAdapter', 'ProtocolPackage',
52
+ ]
53
+
54
+
55
+ @runtime_checkable
56
+ class KeyStoreProtocol(Protocol):
57
+ """The node keys and identity."""
58
+ node_sk: str
59
+ node_pk: str
60
+ did: str
61
+
62
+ def multikeys(self) -> dict[str, str]: ...
63
+ def kid(self, which: str = 'agreement') -> str: ...
64
+ def save(self, directory: Any) -> str: ...
65
+
66
+
67
+ @runtime_checkable
68
+ class TransportProtocol(Protocol):
69
+ """The transport adapter interface: sends and receives DIDComm envelopes."""
70
+ scheme: str
71
+ health: Mapping[str, Mapping[str, Any]]
72
+ drop_counts: Counter[str]
73
+
74
+ def send(self, peer_pk: str, message: DIDCommMessage | Mapping[str, Any], authenticated: bool = True) -> Any: ...
75
+ def request(
76
+ self,
77
+ peer_pk: str,
78
+ message: DIDCommMessage | Mapping[str, Any],
79
+ timeout: Optional[float] = None,
80
+ authenticated: bool = True,
81
+ ) -> Optional[DIDCommMessage | Mapping[str, Any]]: ...
82
+ def close(self) -> None: ...
83
+
84
+
85
+ @runtime_checkable
86
+ class EventStoreProtocol(Protocol):
87
+ """Storage for Nostr events and provenance."""
88
+ def insert_event(self, event: NostrEvent) -> str: ...
89
+ def get_event(self, event_id: str) -> Optional[NostrEvent]: ...
90
+ def has_event(self, event_id: str) -> bool: ...
91
+ def note_origin(self, event_id: str, origin: str) -> None: ...
92
+ def origin_of(self, event_id: str) -> Optional[str]: ...
93
+ def any_replicated(self, event_ids: Sequence[str]) -> bool: ...
94
+ def fingerprint_range(self, filt: NostrFilter | Mapping[str, Any], since: int, until: int, now: int) -> str: ...
95
+
96
+
97
+ # ---- what `meshnode status` and `meshnode health` format -------------------
98
+ #
99
+ # A compartment's state() is free-form: it is JSON-serialised the moment it is
100
+ # read, and the CLI prints most of it with .get(). These are the rows whose
101
+ # fields the CLI formats by name, so a compartment that renames one is caught
102
+ # where it is written rather than where an operator reads it. total=False
103
+ # because status.json on disk predates every one of them.
104
+
105
+ class RelayHealthRow(TypedDict, total=False):
106
+ connected: bool
107
+ confirmed: bool # the relay answered the REQ, not merely received it
108
+ since: Optional[int]
109
+ reconnects: int
110
+ last_event_at: Optional[int]
111
+ last_error: Optional[str]
112
+
113
+
114
+ class AwaitingAckRow(TypedDict, total=False):
115
+ peer: str
116
+ type: str
117
+ at: int
118
+ relays: list[str]
119
+ heard_at: Optional[int] # unacknowledged rows only: when the peer last spoke
120
+
121
+
122
+ class ThrottledRow(TypedDict, total=False):
123
+ peer: str
124
+ shard: str
125
+ why: str
126
+ until: int
127
+
128
+
129
+ class ReceiptRefusedRow(TypedDict, total=False):
130
+ peer: Optional[str]
131
+ window: Optional[int]
132
+ named: Optional[str]
133
+ acceptable: list[str]
134
+ at: int
135
+
136
+
137
+ class GrantStatusRow(TypedDict, total=False):
138
+ peer: str
139
+ direction: str
140
+ tier: Optional[str]
141
+ status: str
142
+
143
+
144
+ class PendingGrantRow(TypedDict, total=False):
145
+ peer: str
146
+ tier: Optional[str]
147
+ from_epoch: int
148
+
149
+
150
+ class EvaluationRow(TypedDict, total=False):
151
+ peer: str
152
+ epoch: int
153
+ severity: Optional[str] # None for a silent window: nothing was priced
154
+ state: str
155
+
156
+
157
+ class RepricedRow(TypedDict, total=False):
158
+ peer: str
159
+ epoch: int
160
+ was: str
161
+ severity: str
162
+ remedy_cancelled: bool
163
+
164
+
165
+ class HealthStatusDict(TypedDict, total=False):
166
+ subsystem_trouble: list[str]
167
+ config_error: Optional[str]
168
+ handler_failures: int
169
+ tick_failures: int
170
+ last_tick_error: Optional[str]
171
+ rejected: Mapping[str, int]
172
+ relays: Mapping[str, RelayHealthRow]
173
+ sessions_refused: int
174
+ receipts_refused: list[ReceiptRefusedRow]
175
+ awaiting_ack: list[AwaitingAckRow]
176
+ throttled: list[ThrottledRow]
177
+ grants: list[GrantStatusRow]
178
+ evaluations: list[EvaluationRow]
179
+ repriced: list[RepricedRow]
180
+ subsystems: Mapping[str, Any]
181
+
182
+
183
+ NodeStatusDict = TypedDict('NodeStatusDict', {
184
+ 'at': int,
185
+ 'did': str,
186
+ 'node_did': str,
187
+ 'pubkey': str,
188
+ 'class': str,
189
+ 'release': Mapping[str, Any],
190
+ 'face': str,
191
+ 'relays': list[str],
192
+ 'descriptor': str,
193
+ 'events_stored': int,
194
+ 'grants': list[GrantStatusRow],
195
+ 'pending_grants': list[PendingGrantRow],
196
+ 'evaluations': list[EvaluationRow],
197
+ 'repriced': list[RepricedRow],
198
+ 'reports_published': int,
199
+ 'pending_findings': int,
200
+ 'refused_sessions': int,
201
+ 'last_tick': list[list[str]],
202
+ 'uptime': int | float,
203
+ 'health': HealthStatusDict,
204
+ 'compartments': Mapping[str, Any],
205
+ 'subsystems': Mapping[str, Any],
206
+ }, total=False)
207
+
208
+
209
+ class CommandSpec(TypedDict, total=False):
210
+ """One control-socket command as the socket documents it under `commands`:
211
+ the dispatch table is the documentation, so it cannot drift from it."""
212
+ args: Mapping[str, str] # name -> required | optional | a short note
213
+ does: str
214
+ reply: str
215
+ subsystem: str # set by the registry when it prefixes the name
216
+
217
+
218
+ @runtime_checkable
219
+ class CompartmentProtocol(Protocol):
220
+ """A compartment of the node, assembled and ticked uniformly."""
221
+ name: Optional[str]
222
+ reads: tuple[str, ...]
223
+ live: tuple[str, ...]
224
+ commands: Mapping[str, CommandSpec]
225
+
226
+ def start(self) -> None: ...
227
+ def tick(self, now: int) -> list[tuple[Any, ...]]: ...
228
+ def stop(self) -> Any: ...
229
+ def state(self) -> Mapping[str, Any]: ...
230
+ def trouble(self) -> list[str]: ...
231
+ def notes(self) -> list[str]: ...
232
+ def report(self) -> Mapping[str, Any]: ...
233
+ def command(self, name: str, args: Mapping[str, Any]) -> Any: ...
234
+ def capability_protocols(self) -> Sequence[str]: ...
235
+ def descriptor_tags(self) -> Sequence[Sequence[str]]: ...
@@ -0,0 +1,27 @@
1
+ """The node's cryptographic keys and hardware enclave on disk."""
2
+ from __future__ import annotations
3
+
4
+ from .core import FILE, Keystore
5
+ from .enclave import (
6
+ DEFAULT_ORIGIN,
7
+ DEFAULT_RP_ID,
8
+ EnclaveKey,
9
+ EnclaveKeystore,
10
+ detect_hardware_enclave,
11
+ verify_detached_attestation,
12
+ verify_enclave_attestation,
13
+ verify_signature,
14
+ )
15
+
16
+ __all__ = [
17
+ 'FILE',
18
+ 'Keystore',
19
+ 'EnclaveKey',
20
+ 'EnclaveKeystore',
21
+ 'detect_hardware_enclave',
22
+ 'verify_detached_attestation',
23
+ 'verify_enclave_attestation',
24
+ 'verify_signature',
25
+ 'DEFAULT_RP_ID',
26
+ 'DEFAULT_ORIGIN',
27
+ ]
node/keystore/core.py ADDED
@@ -0,0 +1,103 @@
1
+ """The node's keys on disk.
2
+
3
+ One JSON file, mode 0600, holding the three secrets constants.json names: the
4
+ secp256k1 node key (the nostr identity and did:nostr subject), the X25519
5
+ agreement key, and the Ed25519 transport key. Generated once, read every
6
+ start. Nothing else is stored here: grants, receipts and the event store have
7
+ their own files, and an operator who rotates the node key has a new node.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import stat
14
+ from typing import Literal
15
+
16
+ from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
17
+
18
+ from fleetmesh_core.crypto import crypto, keys # noqa: E402
19
+
20
+ FILE = 'keys.json'
21
+
22
+
23
+ class Keystore:
24
+ node_sk: str
25
+ node_pk: str
26
+ did: str
27
+ agreement_priv: x25519.X25519PrivateKey
28
+ agreement_pub: x25519.X25519PublicKey
29
+ transport_priv: ed25519.Ed25519PrivateKey
30
+ transport_pub: ed25519.Ed25519PublicKey
31
+ _seeds: tuple[bytes, bytes]
32
+
33
+ def __init__(self, node_sk: str, agreement_seed: bytes, transport_seed: bytes) -> None:
34
+ # A secret that is not a scalar in [1, N) is not a key. Below 1 the
35
+ # curve arithmetic returned None and the node died on a TypeError;
36
+ # at or above N it was silently reduced, so the node signed with a
37
+ # key that was not the one in its file. Refuse both by name.
38
+ try:
39
+ value = int(node_sk, 16)
40
+ except (TypeError, ValueError):
41
+ raise ValueError('node_sk is 64 hex characters')
42
+ if not isinstance(node_sk, str) or len(node_sk) != 64 or not (1 <= value < crypto.N):
43
+ raise ValueError('node_sk must be a 32-byte scalar in [1, N)')
44
+ if len(agreement_seed) != 32 or len(transport_seed) != 32:
45
+ raise ValueError('agreement and transport seeds are 32 bytes')
46
+ self.node_sk = node_sk # hex
47
+ self.node_pk = crypto.pubkey(node_sk) # hex, x-only
48
+ self.did = 'did:nostr:' + self.node_pk
49
+ self.agreement_priv, self.agreement_pub = keys.agreement_keypair(agreement_seed)
50
+ self.transport_priv, self.transport_pub = keys.transport_keypair(transport_seed)
51
+ self._seeds = (agreement_seed, transport_seed)
52
+
53
+ # ---- encodings the descriptor and DID document carry ----------------
54
+ def multikeys(self) -> dict[str, str]:
55
+ return {'node': keys.multikey('node', bytes.fromhex(self.node_pk)),
56
+ 'agreement': keys.multikey('agreement', keys.raw_public(self.agreement_pub)),
57
+ 'transport': keys.multikey('transport', keys.raw_public(self.transport_pub))}
58
+
59
+ def kid(self, which: Literal['agreement', 'transport', 'node'] = 'agreement') -> str:
60
+ return '%s#%s' % (self.did, {'agreement': 'agree', 'transport': 'wire', 'node': 'nostr'}[which])
61
+
62
+ # ---- persistence -----------------------------------------------------
63
+ @classmethod
64
+ def generate(cls) -> Keystore:
65
+ sk = os.urandom(32).hex()
66
+ while not (1 <= int(sk, 16) < crypto.N):
67
+ sk = os.urandom(32).hex()
68
+ return cls(sk, keys.fresh_seed(), keys.fresh_seed())
69
+
70
+ @classmethod
71
+ def load(cls, directory: str | os.PathLike[str]) -> Keystore:
72
+ path = os.path.join(directory, FILE)
73
+ mode = stat.S_IMODE(os.stat(path).st_mode)
74
+ if mode & 0o077:
75
+ raise PermissionError('%s is readable by others (mode %o); refusing to load' % (path, mode))
76
+ with open(path, encoding='utf-8') as _fh:
77
+ d = json.load(_fh)
78
+ if d.get('format') != 'meshnode-keys/1':
79
+ raise ValueError('not a meshnode key file')
80
+ return cls(d['node_sk'], bytes.fromhex(d['agreement_seed']), bytes.fromhex(d['transport_seed']))
81
+
82
+ def save(self, directory: str | os.PathLike[str]) -> str:
83
+ os.makedirs(directory, mode=0o700, exist_ok=True)
84
+ path = os.path.join(directory, FILE)
85
+ if os.path.exists(path):
86
+ raise FileExistsError('%s exists; a node does not overwrite its own keys' % path)
87
+ body = json.dumps({'format': 'meshnode-keys/1', 'node_sk': self.node_sk,
88
+ 'agreement_seed': self._seeds[0].hex(),
89
+ 'transport_seed': self._seeds[1].hex(),
90
+ 'node_pk': self.node_pk, 'did': self.did}, indent=2)
91
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
92
+ with os.fdopen(fd, 'w', encoding='utf-8') as f:
93
+ f.write(body + '\n')
94
+ return path
95
+
96
+ @classmethod
97
+ def load_or_generate(cls, directory: str | os.PathLike[str]) -> Keystore:
98
+ if os.path.exists(os.path.join(directory, FILE)):
99
+ return cls.load(directory)
100
+ ks = cls.generate()
101
+ ks.save(directory)
102
+ return ks
103
+