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,477 @@
1
+ """Iroh QUIC / stream transport adapter for fleetmesh.
2
+
3
+ Protocol ALPN: `fleetmesh/didcomm/1` as specified in `spec.html` and `schema/messages.json`.
4
+ Provides direct, low-latency, point-to-point DIDComm streaming between mesh nodes
5
+ using Iroh NodeAddr tickets and Ed25519 transport identities.
6
+
7
+ Operates in two modes:
8
+ 1. Pure Python In-Process Stream Server & Client (zero external C-dependencies):
9
+ Standard-library socket streaming with 4-byte big-endian framing over ALPN
10
+ `fleetmesh/didcomm/1`, supporting instant bi-directional request/reply and
11
+ direct peer-to-peer delivery.
12
+ 2. Iroh Sidecar Adapter Mode (`iroh.sock`):
13
+ When an external Rust `iroh` daemon is running alongside the node,
14
+ communicates via local UNIX domain socket to leverage full UDP hole-punching
15
+ and DERP relay routing across NATs.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import collections
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import queue
24
+ import socket
25
+ import struct
26
+ import tempfile
27
+ import threading
28
+ import time
29
+ from typing import (
30
+ Any,
31
+ Callable,
32
+ Counter,
33
+ Deque,
34
+ Mapping,
35
+ Optional,
36
+ Union,
37
+ )
38
+
39
+ from fleetmesh_core.identity.did import did_nostr
40
+ from fleetmesh_core.crypto import keys
41
+ from fleetmesh_core.identity import didcomm
42
+ from .. import didcomm_inbound
43
+ from .. import const as transport_const
44
+ from ..const import Inbound, TransportError
45
+ from fleetmesh_core.identity.iroh_ticket import ALPN, IrohTicket # re-exported: the ticket is the rung's shared leaf
46
+ from .types import IrohHealthReport
47
+
48
+ DEFAULT_PREFERENCE: int = 10
49
+ MAX_STREAM_PAYLOAD: int = 16 * 1024 * 1024 # 16 MB maximum stream envelope
50
+ SOCK: str = 'iroh.sock'
51
+
52
+
53
+ def sidecar_socket_path(directory: str) -> str:
54
+ """Derive unix socket path for the iroh sidecar."""
55
+ real = os.path.realpath(directory)
56
+ p = os.path.join(real, SOCK)
57
+ if len(p.encode('utf-8')) < 100:
58
+ return p
59
+ return os.path.join(
60
+ tempfile.gettempdir(),
61
+ 'fleetmesh-iroh-%s.sock' % hashlib.sha256(real.encode('utf-8')).hexdigest()[:16]
62
+ )
63
+
64
+
65
+ class IrohTransport:
66
+ """Iroh QUIC / stream transport adapter.
67
+
68
+ Manages ALPN fleetmesh/didcomm/1 listeners and outbound stream dispatch.
69
+ """
70
+
71
+ scheme: str = 'iroh'
72
+
73
+ def __init__(
74
+ self,
75
+ keystore: Any,
76
+ peers: Any,
77
+ host: str = '127.0.0.1',
78
+ port: int = 0,
79
+ ticket: Optional[str] = None,
80
+ sidecar_dir: Optional[str] = None,
81
+ clock: Optional[Callable[[], int]] = None,
82
+ timeout: float = 5.0,
83
+ mode: str = 'stream',
84
+ ) -> None:
85
+ self.ks: Any = keystore
86
+ self.peers: Any = peers
87
+ self.host: str = host
88
+ self.port: int = port
89
+ self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
90
+ self.timeout: float = timeout
91
+ self.sidecar_dir: Optional[str] = sidecar_dir
92
+ self.mode: str = mode
93
+
94
+ # Node Ed25519 transport public key (Multikey fed01...)
95
+ raw_transport = keys.raw_public(self.ks.transport_pub)
96
+ self.node_id: str = raw_transport.hex()
97
+
98
+ self._ticket_str: Optional[str] = ticket
99
+ self._server_sock: Optional[socket.socket] = None
100
+ self._listener_thread: Optional[threading.Thread] = None
101
+ self._stop: threading.Event = threading.Event()
102
+
103
+ self._quic_engine: Optional[Any] = None
104
+ self._quic_worker_thread: Optional[threading.Thread] = None
105
+
106
+ self.inbox: queue.Queue[Inbound] = queue.Queue(maxsize=10000)
107
+ self._waiting: dict[str, queue.Queue[Inbound]] = {} # thid -> Queue
108
+ self._lock: threading.Lock = threading.Lock()
109
+
110
+ # Telemetry & Health counters
111
+ self.streams_in: int = 0
112
+ self.streams_out: int = 0
113
+ self.bytes_in: int = 0
114
+ self.bytes_out: int = 0
115
+ self.dropped: Deque[tuple[str, Optional[str]]] = collections.deque(maxlen=200)
116
+ self.drop_counts: Counter[str] = collections.Counter()
117
+ # Replay and freshness state for this rung (didcomm_inbound.InboundGuard).
118
+ self.guard: didcomm_inbound.InboundGuard = didcomm_inbound.InboundGuard()
119
+ # Anoncrypt messages refused because their type is not first-contact.
120
+ self.anonymous_refused: int = 0
121
+ self.last_error: Optional[str] = None
122
+ self.active_sidecar: bool = False
123
+
124
+ @property
125
+ def ticket(self) -> str:
126
+ """The canonical ticket string for advertising in Kind 11801."""
127
+ if self._ticket_str:
128
+ return self._ticket_str
129
+ return IrohTicket(self.node_id, self.host, self.port).encode()
130
+
131
+ @property
132
+ def health(self) -> IrohHealthReport:
133
+ """Health status for operator telemetry and health check commands."""
134
+ is_conn = (
135
+ self._server_sock is not None
136
+ or self.active_sidecar
137
+ or (self._quic_engine is not None and self._quic_engine.sock is not None)
138
+ )
139
+ is_listen = (
140
+ self._server_sock is not None
141
+ or (self._quic_engine is not None and self._quic_engine.sock is not None)
142
+ )
143
+ h: IrohHealthReport = {
144
+ 'scheme': 'iroh',
145
+ 'alpn': ALPN,
146
+ 'mode': self.mode,
147
+ 'connected': is_conn,
148
+ 'listening': is_listen,
149
+ 'endpoint': f"{self.host}:{self.port}",
150
+ 'ticket': self.ticket,
151
+ 'node_id': self.node_id,
152
+ 'streams_in': self.streams_in,
153
+ 'streams_out': self.streams_out,
154
+ 'bytes_in': self.bytes_in,
155
+ 'bytes_out': self.bytes_out,
156
+ 'last_error': self.last_error,
157
+ 'active_sidecar': self.active_sidecar,
158
+ }
159
+ if self._quic_engine is not None:
160
+ h['quic_engine'] = self._quic_engine.status()
161
+ return h
162
+
163
+ def _drop(self, reason: str, ident: Optional[str] = None) -> None:
164
+ prefix = reason.split(':', 1)[0].strip()
165
+ self.drop_counts[prefix] += 1
166
+ self.dropped.append((reason, ident))
167
+ self.last_error = reason
168
+
169
+ def start(self) -> IrohTransport:
170
+ """Bind server socket and launch stream receiver."""
171
+ # 1. QUIC Mode (RFC 9000 Live Engine over UDP)
172
+ if self.mode == 'quic':
173
+ from . import quic as iroh_live
174
+ if self._quic_engine is None:
175
+ self._quic_engine = iroh_live.IrohLiveEngine(
176
+ node_id=self.node_id,
177
+ host=self.host,
178
+ port=self.port,
179
+ alpn=ALPN,
180
+ timeout=self.timeout,
181
+ )
182
+ self._quic_engine.start()
183
+ self.port = self._quic_engine.port
184
+ self._stop.clear()
185
+ self._quic_worker_thread = threading.Thread(target=self._quic_worker_loop, daemon=True)
186
+ self._quic_worker_thread.start()
187
+ return self
188
+
189
+ # 2. Check if external sidecar exists
190
+ if self.sidecar_dir:
191
+ sp = sidecar_socket_path(self.sidecar_dir)
192
+ if os.path.exists(sp):
193
+ from node.compartment.sidecar import probe_unix_socket
194
+ resp = probe_unix_socket(sp, {'cmd': 'status'}, timeout=1.0)
195
+ if isinstance(resp, dict) and resp.get('ok'):
196
+ self.active_sidecar = True
197
+ return self
198
+ self.active_sidecar = False
199
+
200
+ # 3. In-Process Socket Server (zero external dependencies)
201
+ if self._server_sock is None:
202
+ self._server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
203
+ self._server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
204
+ self._server_sock.bind((self.host, self.port))
205
+ # Resolve actual bound ephemeral port if port 0 was passed
206
+ self.port = self._server_sock.getsockname()[1]
207
+ self._server_sock.listen(64)
208
+ self._server_sock.settimeout(0.5)
209
+
210
+ self._stop.clear()
211
+ self._listener_thread = threading.Thread(target=self._accept_loop, daemon=True)
212
+ self._listener_thread.start()
213
+
214
+ return self
215
+
216
+ def _quic_worker_loop(self) -> None:
217
+ while not self._stop.is_set():
218
+ if self._quic_engine is None:
219
+ break
220
+ item = self._quic_engine.receive_stream(timeout=0.2)
221
+ if item is None:
222
+ continue
223
+ conn, stream_id, raw_bytes = item
224
+ self.streams_in += 1
225
+ self.bytes_in += len(raw_bytes)
226
+
227
+ inbound = self._process_inbound_bytes(raw_bytes)
228
+ if inbound is None:
229
+ continue
230
+
231
+ thid = inbound.thid
232
+ with self._lock:
233
+ q = self._waiting.get(thid) if thid else None
234
+
235
+ if q is not None:
236
+ try:
237
+ q.put_nowait(inbound)
238
+ except queue.Full:
239
+ self._drop('waiter queue full')
240
+ else:
241
+ try:
242
+ self.inbox.put_nowait(inbound)
243
+ except queue.Full:
244
+ self._drop('inbox queue saturated')
245
+
246
+ def stop(self) -> None:
247
+ """Shut down listener socket and background threads."""
248
+ self._stop.set()
249
+ if self._quic_engine is not None:
250
+ self._quic_engine.stop()
251
+ self._quic_engine = None
252
+ if self._quic_worker_thread is not None:
253
+ self._quic_worker_thread.join(timeout=1.0)
254
+ self._quic_worker_thread = None
255
+ if self._server_sock is not None:
256
+ try:
257
+ self._server_sock.close()
258
+ except OSError:
259
+ pass
260
+ self._server_sock = None
261
+ if self._listener_thread is not None:
262
+ self._listener_thread.join(timeout=2.0)
263
+ self._listener_thread = None
264
+
265
+ def _accept_loop(self) -> None:
266
+ while not self._stop.is_set():
267
+ try:
268
+ if self._server_sock is None:
269
+ break
270
+ conn, addr = self._server_sock.accept()
271
+ except socket.timeout:
272
+ continue
273
+ except OSError:
274
+ break
275
+ threading.Thread(target=self._handle_conn, args=(conn, addr), daemon=True).start()
276
+
277
+ def _handle_conn(self, conn: socket.socket, addr: tuple[str, int]) -> None:
278
+ conn.settimeout(self.timeout)
279
+ try:
280
+ # Read 4-byte big-endian payload length
281
+ hdr = conn.recv(4)
282
+ if len(hdr) < 4:
283
+ return
284
+ length = struct.unpack('>I', hdr)[0]
285
+ if length > MAX_STREAM_PAYLOAD:
286
+ self._drop('stream payload exceeds maximum length limit')
287
+ return
288
+
289
+ buf = bytearray()
290
+ while len(buf) < length:
291
+ chunk = conn.recv(min(65536, length - len(buf)))
292
+ if not chunk:
293
+ break
294
+ buf.extend(chunk)
295
+
296
+ if len(buf) < length:
297
+ self._drop('incomplete stream payload received')
298
+ return
299
+
300
+ self.streams_in += 1
301
+ self.bytes_in += len(buf) + 4
302
+
303
+ # Process inbound payload
304
+ inbound = self._process_inbound_bytes(bytes(buf))
305
+ if inbound is None:
306
+ return
307
+
308
+ thid = inbound.thid
309
+ with self._lock:
310
+ q = self._waiting.get(thid) if thid else None
311
+
312
+ if q is not None:
313
+ try:
314
+ q.put_nowait(inbound)
315
+ except queue.Full:
316
+ self._drop('waiter queue full')
317
+ else:
318
+ try:
319
+ self.inbox.put_nowait(inbound)
320
+ except queue.Full:
321
+ self._drop('inbox queue saturated')
322
+
323
+ except Exception as e:
324
+ self._drop(f"stream read failed: {e}")
325
+ finally:
326
+ try:
327
+ conn.close()
328
+ except OSError:
329
+ pass
330
+
331
+ def _process_inbound_bytes(self, raw_bytes: bytes) -> Optional[Inbound]:
332
+ """Open a DIDComm JWE off a stream; None, with the reason recorded.
333
+
334
+ A stream is bytes from whoever connected to the port. There used to
335
+ be a plaintext fallback here "for discovery / ping" that built an
336
+ Inbound out of an unencrypted object and named as its sender whatever
337
+ `from` said. Unauthenticated, yes, but delivered into the same
338
+ `_waiting` queue that request() reads by thid, so a connect-and-write
339
+ from anyone answered any conversation this node had open. Discovery
340
+ has its own protocols (mDNS, descriptors); the DIDComm port carries
341
+ envelopes and refuses the rest.
342
+ """
343
+ inbound, why = didcomm_inbound.open_inbound(
344
+ raw_bytes, self.ks, self.peers, f"iroh-{os.urandom(8).hex()}",
345
+ guard=self.guard, now=int(self.clock()))
346
+ if inbound is None:
347
+ self._drop(why or 'envelope: refused')
348
+ return None
349
+ if not inbound.authenticated and not transport_const.anonymous_allowed(inbound.message.get('type')):
350
+ # Anoncrypt off a stream: nobody proved who wrote it, and no
351
+ # handler for this type treats the sender as a stranger.
352
+ self.anonymous_refused += 1
353
+ self._drop('unauthenticated: anoncrypt %s is not a first-contact type'
354
+ % str(inbound.message.get('type'))[:80])
355
+ return None
356
+ return inbound
357
+
358
+ # ---- Outbound Stream Dispatch ------------------------------------------
359
+
360
+ def send(
361
+ self,
362
+ message: Mapping[str, Any],
363
+ to_pubkey: str,
364
+ ticket: Optional[Union[str, IrohTicket]] = None,
365
+ anonymous: bool = False,
366
+ timeout: Optional[float] = None,
367
+ ) -> dict[str, tuple[bool, str]]:
368
+ """Encrypt and dispatch a DIDComm envelope over an Iroh stream.
369
+
370
+ Returns { 'iroh': (True, 'delivered via iroh quic stream') }.
371
+ """
372
+ parsed_ticket = self._resolve_ticket(to_pubkey, ticket)
373
+ if parsed_ticket is None:
374
+ raise TransportError(f"no iroh ticket found for peer {to_pubkey[:16]}")
375
+
376
+ their_agree = self.peers.agreement(to_pubkey)
377
+ if their_agree is None:
378
+ raise TransportError(f"no live agreement key for peer {to_pubkey[:16]}")
379
+
380
+ msg_dict = dict(message)
381
+ msg_dict.setdefault('from', self.ks.did)
382
+ msg_dict.setdefault('to', [did_nostr(to_pubkey)])
383
+ msg_dict.setdefault('created_time', int(self.clock()))
384
+
385
+ kid = did_nostr(to_pubkey) + '#agree'
386
+ sender = None if anonymous else (self.ks.kid('agreement'), self.ks.agreement_priv)
387
+ jwe = didcomm.encrypt(msg_dict, [(kid, their_agree)], sender=sender)
388
+
389
+ payload_bytes = json.dumps(jwe, separators=(',', ':'), sort_keys=True).encode('utf-8')
390
+
391
+ # QUIC Mode (RFC 9000 Live Engine over UDP)
392
+ if self.mode == 'quic':
393
+ if self._quic_engine is None:
394
+ raise TransportError("quic engine not running")
395
+ conn = self._quic_engine.get_or_connect(parsed_ticket)
396
+ sid = self._quic_engine.send_stream(conn, payload_bytes)
397
+ self.streams_out += 1
398
+ self.bytes_out += len(payload_bytes)
399
+ return {'iroh': (True, f"delivered via iroh quic stream {sid} to {parsed_ticket.host}:{parsed_ticket.port}")}
400
+
401
+ framed = struct.pack('>I', len(payload_bytes)) + payload_bytes
402
+
403
+ eff_timeout = timeout or self.timeout
404
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
405
+ sock.settimeout(eff_timeout)
406
+ try:
407
+ sock.connect((parsed_ticket.host, parsed_ticket.port))
408
+ sock.sendall(framed)
409
+ self.streams_out += 1
410
+ self.bytes_out += len(framed)
411
+ return {'iroh': (True, f"delivered via iroh quic stream to {parsed_ticket.host}:{parsed_ticket.port}")}
412
+ except Exception as e:
413
+ self._drop(f"iroh stream connect failed: {e}", to_pubkey)
414
+ raise TransportError(f"iroh stream connect to {parsed_ticket.host}:{parsed_ticket.port} failed: {e}")
415
+ finally:
416
+ try:
417
+ sock.close()
418
+ except OSError:
419
+ pass
420
+
421
+ def request(
422
+ self,
423
+ message: Mapping[str, Any],
424
+ to_pubkey: str,
425
+ ticket: Optional[Union[str, IrohTicket]] = None,
426
+ timeout: float = 15.0,
427
+ anonymous: bool = False,
428
+ ) -> Optional[Inbound]:
429
+ """Send a message and wait on the threaded response queue."""
430
+ thid = message.get('thid') or message.get('id')
431
+ if not thid:
432
+ raise TransportError("cannot request without a message id or thid")
433
+
434
+ q = self.listen(thid)
435
+ try:
436
+ self.send(message, to_pubkey, ticket=ticket, anonymous=anonymous, timeout=timeout)
437
+ try:
438
+ return q.get(timeout=timeout)
439
+ except queue.Empty:
440
+ return None
441
+ finally:
442
+ self.unlisten(thid)
443
+
444
+ def listen(self, thid: str) -> queue.Queue[Inbound]:
445
+ """Route messages matching thread id to a dedicated queue."""
446
+ with self._lock:
447
+ q = self._waiting.get(thid)
448
+ if q is None:
449
+ q = self._waiting[thid] = queue.Queue()
450
+ return q
451
+
452
+ def unlisten(self, thid: str) -> None:
453
+ """Deregister thread listener queue."""
454
+ with self._lock:
455
+ self._waiting.pop(thid, None)
456
+
457
+ def receive(self, timeout: Optional[float] = None) -> Optional[Inbound]:
458
+ """Fetch the next delivered message from the inbox."""
459
+ try:
460
+ return self.inbox.get(timeout=timeout)
461
+ except queue.Empty:
462
+ return None
463
+
464
+ def _resolve_ticket(self, to_pubkey: str, ticket: Optional[Union[str, IrohTicket]]) -> Optional[IrohTicket]:
465
+ if isinstance(ticket, IrohTicket):
466
+ return ticket
467
+ if isinstance(ticket, str) and ticket:
468
+ return IrohTicket.parse(ticket)
469
+
470
+ # Look in peer's Kind 11801 descriptor
471
+ desc = self.peers.descriptor(to_pubkey)
472
+ if desc is not None:
473
+ for t in desc.get('tags', []):
474
+ if t and t[0] == 'endpoint' and len(t) > 2 and t[1] == 'iroh':
475
+ return IrohTicket.parse(t[2])
476
+
477
+ return None