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,605 @@
1
+ """Bacalhau REST client connector and Blossom CAS compute-over-data mounting bridge.
2
+
3
+ Implements Phase 2 of the compute roadmap:
4
+ - BacalhauClientProtocol: abstract interface for Bacalhau daemon orchestrators.
5
+ - BacalhauRestClient: native HTTP/REST connector for Bacalhau v1 orchestrator API.
6
+ - MockBacalhauClient: test double for deterministic testing and dry-run fixtures.
7
+ - BlossomMountBridge: Blossom CAS input resolution and output packaging/uploading.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ from typing import Any, Callable, Mapping, NamedTuple, Optional, Protocol, Sequence, TypedDict
15
+
16
+ from node.transport import netfetch
17
+ from ..common import ComputeError, resolve_blob
18
+ from ..sandbox import ComputeLimits
19
+ from ..types import BacalhauJobSummary
20
+ from .base import ComputeExecutionResult, ComputeExecutor, ComputeQuote
21
+
22
+
23
+ ProgressCallback = Callable[[str, float, Mapping[str, Any]], None]
24
+
25
+
26
+ class BlossomStagingResult(NamedTuple):
27
+ """Result of staging Blossom CAS input blobs into container mount points."""
28
+ staged_paths: list[str]
29
+ total_bytes: int
30
+ files: dict[str, bytes]
31
+ source_cids: list[str]
32
+
33
+
34
+ class BlossomUploadResult(NamedTuple):
35
+ """Result of packaging and committing container outputs to Blossom CAS."""
36
+ result_cid: str
37
+ result_hash: str
38
+ output_bytes: bytes
39
+ mounted_outputs: list[str]
40
+
41
+
42
+ class BacalhauTaskSpec(TypedDict, total=False):
43
+ """Specification of a single Bacalhau container task."""
44
+ name: str
45
+ engine: str
46
+ image: str
47
+ entrypoint: list[str]
48
+ parameters: list[str]
49
+ env: dict[str, str]
50
+ input_sources: list[Mapping[str, Any]]
51
+ result_paths: list[Mapping[str, Any]]
52
+ resources: Mapping[str, Any]
53
+ timeouts: dict[str, int]
54
+
55
+
56
+ class BacalhauClientProtocol(Protocol):
57
+ """Interface required for Bacalhau orchestrator connectors."""
58
+
59
+ def submit_job(self, job_payload: Mapping[str, Any], timeout_seconds: int = 300) -> str:
60
+ """Submit a batch job specification and return assigned JobID."""
61
+ ...
62
+
63
+ def get_job_status(self, job_id: str) -> Mapping[str, Any]:
64
+ """Query execution lifecycle state for the given job_id."""
65
+ ...
66
+
67
+ def get_job_results(self, job_id: str) -> Mapping[str, Any]:
68
+ """Fetch outputs, logs, and artifacts for a completed job."""
69
+ ...
70
+
71
+ def cancel_job(self, job_id: str, reason: str = '') -> bool:
72
+ """Abort or terminate a running Bacalhau job."""
73
+ ...
74
+
75
+ def is_alive(self) -> bool:
76
+ """Check if the Bacalhau daemon/orchestrator endpoint is reachable."""
77
+ ...
78
+
79
+
80
+ class BacalhauRestClient:
81
+ """Live Bacalhau REST API v1 connector using Fleetmesh netfetch."""
82
+
83
+ def __init__(
84
+ self,
85
+ endpoint: str = 'http://127.0.0.1:1234',
86
+ timeout_seconds: float = 30.0,
87
+ ) -> None:
88
+ self.endpoint: str = endpoint.rstrip('/')
89
+ self.timeout_seconds: float = timeout_seconds
90
+
91
+ def is_alive(self) -> bool:
92
+ """Check if the Bacalhau daemon endpoint responds to health probes."""
93
+ for api_route in ('/api/v1/orchestrator/nodes', '/api/v1/version', '/api/v1/agent/alive'):
94
+ url = f"{self.endpoint}{api_route}"
95
+ try:
96
+ body, _ = netfetch.fetch(
97
+ url,
98
+ max_bytes=65536,
99
+ timeout=2.0,
100
+ method='GET',
101
+ allow_private=True,
102
+ )
103
+ if body:
104
+ return True
105
+ except Exception:
106
+ continue
107
+ return False
108
+
109
+ def build_bacalhau_job_spec(self, job_payload: Mapping[str, Any]) -> Mapping[str, Any]:
110
+ """Convert Fleetmesh job descriptor into standard Bacalhau v1 format."""
111
+ engine = str(job_payload.get('engine', 'docker')).lower()
112
+ image = str(job_payload.get('image', ''))
113
+ wasm_mod = job_payload.get('wasm_module', {})
114
+ entrypoint = job_payload.get('entrypoint') or ['/bin/sh', '-c']
115
+ parameters = job_payload.get('parameters') or []
116
+ env_vars = job_payload.get('env') or {}
117
+ resources = job_payload.get('resources') or {'cpu': 1, 'gpu': 0, 'memory_gib': 2.0, 'timeout_seconds': 300}
118
+ storage_mounts = job_payload.get('storage_mounts') or []
119
+ output_mounts = job_payload.get('output_mounts') or []
120
+
121
+ input_sources: list[Mapping[str, Any]] = []
122
+ for mount in storage_mounts:
123
+ cid = mount.get('source_cid', '')
124
+ mount_path = mount.get('mount_path', '/inputs')
125
+ input_sources.append({
126
+ 'Source': {
127
+ 'Type': 'urlDownload' if cid.startswith('http') else 's3' if cid.startswith('s3://') else 'localDirectory',
128
+ 'Params': {
129
+ 'URL': cid if cid.startswith('http') else '',
130
+ 'Target': mount_path,
131
+ }
132
+ }
133
+ })
134
+
135
+ result_paths = [
136
+ {'Name': m.get('name', 'output'), 'Path': m.get('container_path', '/outputs')}
137
+ for m in output_mounts
138
+ ] or [{'Name': 'outputs', 'Path': '/outputs'}]
139
+
140
+ task = {
141
+ 'Name': 'main',
142
+ 'Engine': {
143
+ 'Type': engine,
144
+ 'Params': {
145
+ 'Image': image or wasm_mod.get('sha256', ''),
146
+ 'Entrypoint': entrypoint,
147
+ 'Parameters': parameters,
148
+ 'EnvironmentVariables': [f"{k}={v}" for k, v in env_vars.items()],
149
+ }
150
+ },
151
+ 'InputSources': input_sources,
152
+ 'ResultPaths': result_paths,
153
+ 'Resources': {
154
+ 'CPU': str(resources.get('cpu', 1)),
155
+ 'Memory': f"{int(resources.get('memory_gib', 2))}Gb",
156
+ 'GPU': str(resources.get('gpu', 0)),
157
+ },
158
+ 'Timeouts': {
159
+ 'ExecutionTimeout': int(resources.get('timeout_seconds', 300)),
160
+ },
161
+ 'Network': {
162
+ 'Type': 'None',
163
+ },
164
+ }
165
+
166
+ job_id = str(job_payload.get('id') or os.urandom(8).hex())
167
+ return {
168
+ 'Job': {
169
+ 'Name': f"fleetmesh-{job_id[:16]}",
170
+ 'Type': 'batch',
171
+ 'Count': 1,
172
+ 'Tasks': [task],
173
+ }
174
+ }
175
+
176
+ def submit_job(self, job_payload: Mapping[str, Any], timeout_seconds: int = 300) -> str:
177
+ """Submit a job specification to the Bacalhau orchestrator."""
178
+ spec = self.build_bacalhau_job_spec(job_payload)
179
+ data_bytes = json.dumps(spec).encode('utf-8')
180
+
181
+ # Try orchestrator API first, fall back to /api/v1/run
182
+ last_err: Optional[Exception] = None
183
+ for api_route in ('/api/v1/orchestrator/jobs', '/api/v1/run'):
184
+ url = f"{self.endpoint}{api_route}"
185
+ try:
186
+ body, _ = netfetch.fetch(
187
+ url,
188
+ max_bytes=1048576,
189
+ timeout=self.timeout_seconds,
190
+ method='POST',
191
+ data=data_bytes,
192
+ headers={'Content-Type': 'application/json', 'User-Agent': 'fleetmesh-bacalhau/1.0'},
193
+ allow_private=True,
194
+ )
195
+ resp = json.loads(body.decode('utf-8'))
196
+ job_id = resp.get('JobID') or resp.get('ID') or (resp.get('Job') or {}).get('ID')
197
+ if job_id:
198
+ return str(job_id)
199
+ except netfetch.FetchError as e:
200
+ last_err = e
201
+ continue
202
+ except Exception as e:
203
+ last_err = e
204
+ continue
205
+
206
+ raise ComputeError(f"Bacalhau job submission failed: {last_err}")
207
+
208
+ def get_job_status(self, job_id: str) -> Mapping[str, Any]:
209
+ """Fetch current state of a Bacalhau job."""
210
+ url = f"{self.endpoint}/api/v1/orchestrator/jobs/{job_id}"
211
+ try:
212
+ body, _ = netfetch.fetch(
213
+ url,
214
+ max_bytes=524288,
215
+ timeout=self.timeout_seconds,
216
+ method='GET',
217
+ allow_private=True,
218
+ )
219
+ data = json.loads(body.decode('utf-8'))
220
+ state = data.get('State') or (data.get('Job') or {}).get('State') or {}
221
+ status_str = state.get('StateType') or 'Running'
222
+ return {
223
+ 'job_id': job_id,
224
+ 'state': status_str,
225
+ 'raw': data,
226
+ }
227
+ except Exception as e:
228
+ raise ComputeError(f"Failed to fetch Bacalhau job status for {job_id}: {e}")
229
+
230
+ def get_job_results(self, job_id: str) -> Mapping[str, Any]:
231
+ """Fetch results and output references for a completed job."""
232
+ url = f"{self.endpoint}/api/v1/orchestrator/jobs/{job_id}/results"
233
+ try:
234
+ body, _ = netfetch.fetch(
235
+ url,
236
+ max_bytes=1048576,
237
+ timeout=self.timeout_seconds,
238
+ method='GET',
239
+ allow_private=True,
240
+ )
241
+ return json.loads(body.decode('utf-8'))
242
+ except Exception as e:
243
+ raise ComputeError(f"Failed to fetch Bacalhau job results for {job_id}: {e}")
244
+
245
+ def cancel_job(self, job_id: str, reason: str = '') -> bool:
246
+ """Cancel an in-flight Bacalhau job."""
247
+ url = f"{self.endpoint}/api/v1/orchestrator/jobs/{job_id}/stop"
248
+ try:
249
+ data_bytes = json.dumps({'Reason': reason or 'cancelled by fleetmesh'}).encode('utf-8')
250
+ netfetch.fetch(
251
+ url,
252
+ max_bytes=65536,
253
+ timeout=self.timeout_seconds,
254
+ method='POST',
255
+ data=data_bytes,
256
+ headers={'Content-Type': 'application/json'},
257
+ allow_private=True,
258
+ )
259
+ return True
260
+ except Exception:
261
+ return False
262
+
263
+
264
+ class MockBacalhauClient:
265
+ """Test double implementing BacalhauClientProtocol for isolated and mock tests."""
266
+
267
+ def __init__(self, simulate_failure: bool = False) -> None:
268
+ self.submitted_jobs: list[Mapping[str, Any]] = []
269
+ self.cancelled_jobs: list[str] = []
270
+ self.simulate_failure: bool = simulate_failure
271
+ self.call_history: list[str] = []
272
+
273
+ def is_alive(self) -> bool:
274
+ return True
275
+
276
+ def submit_job(self, job_payload: Mapping[str, Any], timeout_seconds: int = 300) -> str:
277
+ self.call_history.append('submit_job')
278
+ if self.simulate_failure:
279
+ raise ComputeError("Simulated Bacalhau daemon failure")
280
+ job_id = f"mock-bacalhau-{os.urandom(8).hex()}"
281
+ self.submitted_jobs.append({'job_id': job_id, 'payload': job_payload})
282
+ return job_id
283
+
284
+ def get_job_status(self, job_id: str) -> Mapping[str, Any]:
285
+ self.call_history.append(f'get_job_status:{job_id}')
286
+ if job_id in self.cancelled_jobs:
287
+ return {'job_id': job_id, 'state': 'Cancelled'}
288
+ return {'job_id': job_id, 'state': 'Completed'}
289
+
290
+ def get_job_results(self, job_id: str) -> Mapping[str, Any]:
291
+ self.call_history.append(f'get_job_results:{job_id}')
292
+ return {
293
+ 'job_id': job_id,
294
+ 'status': 'success',
295
+ 'output_cid': f"bafkrei{hashlib.sha256(job_id.encode()).hexdigest()[:48]}",
296
+ 'exit_code': 0,
297
+ }
298
+
299
+ def cancel_job(self, job_id: str, reason: str = '') -> bool:
300
+ self.call_history.append(f'cancel_job:{job_id}')
301
+ self.cancelled_jobs.append(job_id)
302
+ return True
303
+
304
+
305
+ class BlossomMountBridge:
306
+ """Manages input staging from Blossom CAS and output artifact publishing."""
307
+
308
+ def resolve_and_stage_inputs(
309
+ self,
310
+ storage_mounts: Sequence[Mapping[str, Any]],
311
+ blob_store: Optional[Any] = None,
312
+ blossom_servers: Optional[Sequence[str]] = None,
313
+ ) -> BlossomStagingResult:
314
+ """Resolve inputs from Blossom CAS or local storage, staging them for execution."""
315
+ staged_paths: list[str] = []
316
+ source_cids: list[str] = []
317
+ files: dict[str, bytes] = {}
318
+ total_bytes = 0
319
+
320
+ for mount in storage_mounts:
321
+ cid = str(mount.get('source_cid', ''))
322
+ mount_path = str(mount.get('mount_path', '/inputs'))
323
+ if not cid:
324
+ continue
325
+
326
+ source_cids.append(cid)
327
+ spec = {'cid': cid} if cid.startswith('baf') else {'sha256': cid}
328
+ data = resolve_blob(spec, blob_store=blob_store, blossom_servers=blossom_servers)
329
+ if data is not None:
330
+ files[mount_path] = data
331
+ staged_paths.append(mount_path)
332
+ total_bytes += len(data)
333
+
334
+ return BlossomStagingResult(
335
+ staged_paths=staged_paths,
336
+ total_bytes=total_bytes,
337
+ files=files,
338
+ source_cids=source_cids,
339
+ )
340
+
341
+ def package_and_upload_outputs(
342
+ self,
343
+ raw_output_or_files: bytes | Mapping[str, bytes],
344
+ output_mounts: Sequence[Mapping[str, Any]],
345
+ blob_store: Optional[Any] = None,
346
+ blossom_servers: Optional[Sequence[str]] = None,
347
+ ) -> BlossomUploadResult:
348
+ """Hash, persist, and register job output artifacts into Blossom CAS."""
349
+ if isinstance(raw_output_or_files, bytes):
350
+ out_bytes = raw_output_or_files
351
+ else:
352
+ out_bytes = json.dumps(
353
+ {k: v.decode('utf-8', errors='replace') for k, v in raw_output_or_files.items()},
354
+ sort_keys=True
355
+ ).encode('utf-8')
356
+
357
+ result_hash = hashlib.sha256(out_bytes).hexdigest()
358
+
359
+ # Persist to local blob_store if present
360
+ result_cid = None
361
+ if blob_store is not None and hasattr(blob_store, 'put'):
362
+ try:
363
+ desc = blob_store.put(out_bytes)
364
+ result_cid = desc.get('cid') if isinstance(desc, dict) else str(desc)
365
+ except Exception:
366
+ pass
367
+
368
+ if not result_cid:
369
+ # Format canonical Blossom SHA-256 CID
370
+ result_cid = result_hash
371
+
372
+ # Push to remote Blossom servers if configured
373
+ if blossom_servers:
374
+ for srv in blossom_servers:
375
+ try:
376
+ url = f"{srv.rstrip('/')}/upload"
377
+ netfetch.fetch(
378
+ url,
379
+ max_bytes=1048576,
380
+ timeout=15.0,
381
+ method='PUT',
382
+ data=out_bytes,
383
+ headers={'Content-Type': 'application/octet-stream', 'X-SHA-256': result_hash},
384
+ allow_private=True,
385
+ )
386
+ except Exception:
387
+ pass
388
+
389
+ mounted_outputs = [str(m.get('name', 'outputs')) for m in output_mounts] or ['default']
390
+ return BlossomUploadResult(
391
+ result_cid=result_cid,
392
+ result_hash=result_hash,
393
+ output_bytes=out_bytes,
394
+ mounted_outputs=mounted_outputs,
395
+ )
396
+
397
+
398
+ class BacalhauExecutor(ComputeExecutor):
399
+ """Compute-over-data batch driver backed by Bacalhau / Docker containers.
400
+
401
+ Integrates Phase 2 capabilities:
402
+ - Mounts Blossom CAS CIDs directly into container volume mounts.
403
+ - Dispatches jobs to Bacalhau orchestrator or local sandbox.
404
+ - Emits 5-state lifecycle progress heartbeats (compute/1.1/progress).
405
+ - Honors cooperative cancellation tokens (compute/1.1/cancel).
406
+ """
407
+
408
+ engine_id: str = 'bacalhau'
409
+ wasi_profile_digest: str = ''
410
+
411
+ def __init__(
412
+ self,
413
+ endpoint: str = 'http://127.0.0.1:1234',
414
+ mock_mode: bool = False,
415
+ client: Optional[BacalhauClientProtocol] = None,
416
+ mount_bridge: Optional[BlossomMountBridge] = None,
417
+ ) -> None:
418
+ self.endpoint = endpoint.rstrip('/')
419
+ self.mock_mode = mock_mode
420
+ self.client = client
421
+ self.mount_bridge = mount_bridge or BlossomMountBridge()
422
+
423
+ def is_live_runnable(self) -> bool:
424
+ """Check whether Bacalhau execution can succeed on this node."""
425
+ if self.mock_mode:
426
+ return True
427
+ if self.client is not None:
428
+ try:
429
+ return self.client.is_alive()
430
+ except Exception:
431
+ return False
432
+ return False
433
+
434
+ def quote(
435
+ self,
436
+ module_spec: Mapping[str, Any],
437
+ input_spec: Mapping[str, Any],
438
+ limits: ComputeLimits,
439
+ ) -> ComputeQuote:
440
+ cpu_minutes = int(input_spec.get('cpu_minutes', 1))
441
+ gpu_minutes = int(input_spec.get('gpu_minutes', 0))
442
+ ram_gib = float(input_spec.get('ram_gib', 1.0))
443
+ fuel_est = int(cpu_minutes * 150 + gpu_minutes * 1200 + ram_gib * 50)
444
+ return ComputeQuote(
445
+ fuel_estimate=min(fuel_est, limits.max_fuel),
446
+ rate_unit='msat_per_job',
447
+ rate_amount=fuel_est,
448
+ engine_id=self.engine_id,
449
+ )
450
+
451
+ def execute(
452
+ self,
453
+ job_payload: Mapping[str, Any],
454
+ fuel_cap: int,
455
+ progress_callback: Optional[ProgressCallback] = None,
456
+ cancellation_token: Optional[Any] = None,
457
+ ) -> ComputeExecutionResult:
458
+ engine = job_payload.get('engine', 'docker')
459
+ image = job_payload.get('image', '')
460
+ wasm_module = job_payload.get('wasm_module', {})
461
+ entrypoint = job_payload.get('entrypoint') or ['/bin/sh', '-c']
462
+ parameters = job_payload.get('parameters') or []
463
+ storage_mounts = job_payload.get('storage_mounts') or []
464
+ output_mounts = job_payload.get('output_mounts') or []
465
+ resources = job_payload.get('resources') or {'cpu': 1, 'gpu': 0, 'memory_gib': 2.0, 'timeout_seconds': 60}
466
+
467
+ verif_mode = self.verification_mode(job_payload)
468
+
469
+ # Honesty gate. If not in mock_mode, an active and verified live backend must be present.
470
+ if not self.mock_mode and self.client is None and not self.is_live_runnable():
471
+ raise ComputeError('not implemented: bacalhau executor is a stub (mock_mode only)')
472
+
473
+ def _check_cancelled() -> None:
474
+ if cancellation_token is not None:
475
+ is_set = False
476
+ if hasattr(cancellation_token, 'is_set') and cancellation_token.is_set():
477
+ is_set = True
478
+ elif isinstance(cancellation_token, bool) and cancellation_token:
479
+ is_set = True
480
+ if is_set:
481
+ raise ComputeError('job cancelled by requester')
482
+
483
+ # 1. State: queued
484
+ if progress_callback:
485
+ progress_callback('queued', 0.0, {'status': 'job queued for batch execution'})
486
+
487
+ # 2. State: downloading_inputs (stage Blossom CAS volumes)
488
+ _check_cancelled()
489
+ if progress_callback:
490
+ progress_callback('downloading_inputs', 0.2, {'status': 'staging CAS input volumes'})
491
+
492
+ blob_store = job_payload.get('blob_store')
493
+ blossom_servers = job_payload.get('blossom_servers')
494
+
495
+ staging_res = self.mount_bridge.resolve_and_stage_inputs(
496
+ storage_mounts=storage_mounts,
497
+ blob_store=blob_store,
498
+ blossom_servers=blossom_servers,
499
+ )
500
+ mounted_inputs = staging_res.files
501
+ total_in_bytes = staging_res.total_bytes
502
+
503
+ # 3. State: running (execute via Bacalhau orchestrator or deterministic pricing model)
504
+ _check_cancelled()
505
+ if progress_callback:
506
+ progress_callback('running', 0.5, {'status': 'executing container workload'})
507
+
508
+ client_results: Optional[Mapping[str, Any]] = None
509
+ if self.client is not None:
510
+ timeout_sec = int(resources.get('timeout_seconds') or 60)
511
+ job_id = self.client.submit_job(job_payload, timeout_seconds=timeout_sec)
512
+ try:
513
+ _check_cancelled()
514
+ status_data = self.client.get_job_status(job_id)
515
+ state_str = str(status_data.get('state', '')).lower()
516
+ if 'cancel' in state_str:
517
+ raise ComputeError('job cancelled by requester')
518
+ if 'fail' in state_str or 'error' in state_str:
519
+ raise ComputeError(f"Bacalhau job failed: {status_data}")
520
+
521
+ _check_cancelled()
522
+ client_results = self.client.get_job_results(job_id)
523
+ except ComputeError:
524
+ raise
525
+ except Exception as e:
526
+ if cancellation_token is not None and getattr(cancellation_token, 'is_set', lambda: False)():
527
+ self.client.cancel_job(job_id, 'cancelled by fleetmesh session')
528
+ raise ComputeError('job cancelled by requester')
529
+ raise ComputeError(f"Bacalhau job execution failed: {e}")
530
+
531
+ cpu = int(resources.get('cpu') or 1)
532
+ gpu = int(resources.get('gpu') or 0)
533
+ timeout_sec = int(resources.get('timeout_seconds') or 60)
534
+ cpu_sec = min(timeout_sec, 10)
535
+ gpu_sec = min(timeout_sec, 10) if gpu > 0 else 0
536
+ mem_gib = float(resources.get('memory_gib') or 1.0)
537
+ fuel_used = cpu * 150 + gpu * 1200 + int(mem_gib * 50)
538
+
539
+ if fuel_used > fuel_cap:
540
+ raise ComputeError(f"Bacalhau job exceeded fuel cap ({fuel_used} > {fuel_cap})")
541
+
542
+ # 4. State: uploading_outputs (package artifacts and commit to Blossom CAS)
543
+ _check_cancelled()
544
+ if progress_callback:
545
+ progress_callback('uploading_outputs', 0.8, {'status': 'packaging and uploading output artifacts'})
546
+
547
+ out_summary: BacalhauJobSummary = {
548
+ 'status': 'completed',
549
+ 'engine': engine,
550
+ 'image': image or wasm_module.get('sha256', ''),
551
+ 'entrypoint': entrypoint,
552
+ 'parameters': parameters,
553
+ 'mounted_paths': list(mounted_inputs.keys()),
554
+ 'outputs': [m.get('name', 'output') for m in output_mounts] or ['default'],
555
+ 'bytes_processed': total_in_bytes,
556
+ }
557
+ if client_results:
558
+ out_summary['results'] = client_results
559
+
560
+ upload_res = self.mount_bridge.package_and_upload_outputs(
561
+ raw_output_or_files=json.dumps(out_summary, sort_keys=True).encode('utf-8'),
562
+ output_mounts=output_mounts,
563
+ blob_store=blob_store,
564
+ blossom_servers=blossom_servers,
565
+ )
566
+ raw_output = upload_res.output_bytes
567
+ result_hash = upload_res.result_hash
568
+
569
+ # 5. State: completed
570
+ if progress_callback:
571
+ progress_callback('completed', 1.0, {
572
+ 'status': 'job completed successfully',
573
+ 'result_hash': result_hash,
574
+ 'result_cid': upload_res.result_cid,
575
+ })
576
+
577
+ counters = {
578
+ 'cpu_seconds': cpu_sec,
579
+ 'gpu_seconds': gpu_sec,
580
+ 'bytes_in': total_in_bytes,
581
+ 'bytes_out': len(raw_output),
582
+ }
583
+
584
+ return ComputeExecutionResult(
585
+ result_hash=result_hash,
586
+ raw_output=raw_output,
587
+ fuel_used=fuel_used,
588
+ engine_id=self.engine_id,
589
+ wasi_profile_digest=f"bacalhau:{engine}:{image or 'wasm'}",
590
+ counters=counters,
591
+ verification_mode=verif_mode,
592
+ )
593
+
594
+
595
+ __all__ = [
596
+ 'ProgressCallback',
597
+ 'BlossomStagingResult',
598
+ 'BlossomUploadResult',
599
+ 'BacalhauTaskSpec',
600
+ 'BacalhauClientProtocol',
601
+ 'BacalhauRestClient',
602
+ 'MockBacalhauClient',
603
+ 'BlossomMountBridge',
604
+ 'BacalhauExecutor',
605
+ ]