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,227 @@
1
+ """Execution dispatcher (run_job) and kind 30811 job receipts.
2
+
3
+ Separate because this is the seam between executors and the wire/subsystem layers: compute/wire imports it without needing the Compute subsystem.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import time
10
+ from typing import Any, Callable, Mapping, Optional
11
+
12
+
13
+ from .types import ComputeReceiptPayload
14
+
15
+ from fleetmesh_core.crypto import crypto, nip44
16
+ from fleetmesh_core.identity import identity
17
+ from fleetmesh_core.storage import store
18
+ from fleetmesh_core.wire.interfaces import NostrEvent
19
+ from .common import ComputeError, RECEIPT_KIND, DEFAULT_EXPIRATION, resolve_blob
20
+ from .sandbox import ComputeLimits, clamp_limits, run_sandboxed
21
+ from .executors import (
22
+ DEFAULT_REGISTRY,
23
+ ExecutorRegistry,
24
+ FfmpegTranscodeExecutor,
25
+ JobRunResult,
26
+ WasmExecutor,
27
+ _wasm_engine,
28
+ wasmtime_available,
29
+ )
30
+ from .executors.media import NODE_ONLY_INPUT_KEYS, requester_input
31
+
32
+ # -------------------------------------------------------------------------
33
+ # Execution Dispatcher
34
+ # -------------------------------------------------------------------------
35
+ def run_job(
36
+ job_payload: Mapping[str, Any],
37
+ custom_runner: Optional[Callable[[Mapping[str, Any]], bytes]] = None,
38
+ registry: Optional[ExecutorRegistry] = None,
39
+ limits: Optional[ComputeLimits] = None,
40
+ *,
41
+ allow_unsandboxed: bool = False,
42
+ ) -> JobRunResult:
43
+ """Execute a compute job deterministically or non-deterministically.
44
+
45
+ ``allow_unsandboxed`` lets the job run in-process on a host without
46
+ fork/rlimits (macOS spawn-only, Windows). Off by default: an operator who
47
+ sells compute must choose that explicitly rather than discover it.
48
+ """
49
+ fuel_max = int(job_payload.get('fuel_max', 1000000))
50
+ job_limits = clamp_limits(limits or ComputeLimits(
51
+ max_fuel=fuel_max,
52
+ max_memory_bytes=int(job_payload.get('max_memory_bytes', 1073741824)),
53
+ timeout_seconds=int(job_payload.get('timeout_seconds', 60)),
54
+ ))
55
+
56
+ # Blob stores hold sqlite handles/locks; keep them out of the forked child.
57
+ child_payload = {k: v for k, v in job_payload.items() if k != 'blob_store'}
58
+ # Executors size their own memory caps (wasm linear memory) under this.
59
+ child_payload['_memory_ceiling_bytes'] = job_limits.max_memory_bytes
60
+
61
+ if custom_runner is not None:
62
+ raw_output = run_sandboxed(lambda: custom_runner(child_payload), job_limits,
63
+ allow_unsandboxed=allow_unsandboxed)
64
+ # Compare the unclamped usage; clamping first made the check dead.
65
+ fuel_used = len(raw_output) * 10
66
+ if fuel_used > fuel_max:
67
+ raise ComputeError('job execution exceeded fuel ceiling (%d > %d)' % (fuel_used, fuel_max))
68
+ result_hash = crypto.sha256(raw_output)
69
+ return {
70
+ 'result_hash': result_hash,
71
+ 'raw_output': raw_output,
72
+ 'fuel_used': fuel_used,
73
+ 'engine': 'custom',
74
+ 'wasi_profile_digest': '',
75
+ 'counters': {},
76
+ 'verification_mode': 'delivery',
77
+ }
78
+
79
+ reg = registry or DEFAULT_REGISTRY
80
+ executor = reg.select_for_job(child_payload)
81
+ bstore = job_payload.get('blob_store') or getattr(reg, 'blob_store', None)
82
+ if isinstance(executor, WasmExecutor):
83
+ # Load wasmtime's native library and build the engine in the parent:
84
+ # after fork the child runs under RLIMIT_AS, where mapping a ~20 MB
85
+ # shared object (and ctypes) can fail. The forked child inherits both.
86
+ if wasmtime_available():
87
+ _wasm_engine()
88
+ # Resolve module/input here: the blob store (sqlite handles) is not
89
+ # passed into the forked child, so the child could never read a
90
+ # module stored on this node. Pass the verified bytes instead.
91
+ for key in ('module', 'input'):
92
+ spec = job_payload.get(key)
93
+ if isinstance(spec, Mapping) and 'raw' not in spec:
94
+ data = resolve_blob(spec, blob_store=bstore,
95
+ blossom_servers=job_payload.get('blossom_servers'))
96
+ if data is not None:
97
+ child_payload[key] = dict(spec, raw=data)
98
+ elif isinstance(executor, FfmpegTranscodeExecutor):
99
+ # The requester is remote: drop the fields only the node may set,
100
+ # then add this node's own source_path from its blob store.
101
+ for k in NODE_ONLY_INPUT_KEYS:
102
+ child_payload.pop(k, None)
103
+ inp = job_payload.get('input')
104
+ if isinstance(inp, Mapping):
105
+ inp = requester_input(inp)
106
+ child_payload['input'] = inp
107
+ if isinstance(inp, Mapping) and 'raw' not in inp:
108
+ src_sha = str(inp.get('source_sha256') or inp.get('sha256') or '').strip().lower()
109
+ if bstore is not None and len(src_sha) == 64:
110
+ p = None
111
+ if hasattr(bstore, 'get_blob_path'):
112
+ p = bstore.get_blob_path(src_sha)
113
+ elif hasattr(bstore, '_blob_file_path'):
114
+ p = bstore._blob_file_path(src_sha)
115
+ if p and os.path.isfile(p):
116
+ child_payload['input'] = dict(inp, source_path=p)
117
+ else:
118
+ data = (
119
+ bstore.get_blob(src_sha)
120
+ if hasattr(bstore, 'get_blob')
121
+ else resolve_blob({'sha256': src_sha}, blob_store=bstore, blossom_servers=job_payload.get('blossom_servers'))
122
+ )
123
+ if data is not None:
124
+ child_payload['input'] = dict(inp, raw=data)
125
+
126
+ if isinstance(executor, FfmpegTranscodeExecutor):
127
+ # FfmpegTranscodeExecutor spawns its own isolated `ffmpeg` subprocess
128
+ # with explicit timeout and nice priority; running directly avoids
129
+ # RLIMIT_AS breaking dynamic libavcodec mappings on Linux hosts.
130
+ if bstore is not None:
131
+ child_payload['_blob_store'] = bstore
132
+ res = executor.execute(child_payload, fuel_max)
133
+ else:
134
+ res = run_sandboxed(lambda: executor.execute(child_payload, fuel_max), job_limits,
135
+ allow_unsandboxed=allow_unsandboxed)
136
+ blob_store = job_payload.get('blob_store')
137
+ result_cid = None
138
+ if blob_store is not None and res.raw_output and hasattr(blob_store, 'put') and not hasattr(blob_store, 'store_blob'):
139
+ desc = blob_store.put(res.raw_output)
140
+ result_cid = desc.get('cid') if isinstance(desc, dict) else str(desc)
141
+ else:
142
+ from node.protocols.nostr import blobstore
143
+ result_cid = blobstore.sha256_to_cid(res.result_hash)
144
+
145
+ return {
146
+ 'result_hash': res.result_hash,
147
+ 'result_cid': result_cid,
148
+ 'raw_output': res.raw_output,
149
+ 'fuel_used': res.fuel_used,
150
+ 'engine': res.engine_id,
151
+ 'wasi_profile_digest': res.wasi_profile_digest,
152
+ 'counters': dict(res.counters),
153
+ 'verification_mode': res.verification_mode,
154
+ }
155
+
156
+
157
+ # -------------------------------------------------------------------------
158
+ # Kind 30811: Compute Receipt
159
+ # -------------------------------------------------------------------------
160
+ def build_receipt(
161
+ seller_sk: str,
162
+ buyer_pk: str,
163
+ receipt_payload: Mapping[str, Any],
164
+ blinding: str,
165
+ expiration: Optional[int] = None,
166
+ now: Optional[int] = None,
167
+ ) -> NostrEvent:
168
+ """Build a signed kind 30811 compute job receipt event.
169
+
170
+ Encrypted under NIP-44 conversation key to the buyer, tagged with the
171
+ blinded job identifier.
172
+ """
173
+ now_val = int(now if now is not None else time.time())
174
+ exp_val = int(expiration if expiration is not None else now_val + DEFAULT_EXPIRATION)
175
+
176
+ d = identity.job_tag(seller_sk, buyer_pk)
177
+ canon_payload = crypto.canonical(receipt_payload)
178
+ commit = crypto.commit(canon_payload, blinding)
179
+
180
+ conv = nip44.conversation_key(seller_sk, buyer_pk)
181
+ ciphertext = nip44.encrypt(canon_payload.decode('utf-8'), conv)
182
+
183
+ tags: list[list[str]] = [
184
+ ['d', d],
185
+ ['commit', commit],
186
+ ['scheme', 'sha256-blind/1', 'nip44/2'],
187
+ ['expiration', str(exp_val)],
188
+ ]
189
+
190
+ ev = store.make_signed_event(seller_sk, now_val, RECEIPT_KIND, tags, ciphertext)
191
+ return ev
192
+
193
+
194
+ def open_receipt(receipt: NostrEvent | Mapping[str, Any], buyer_sk: str) -> ComputeReceiptPayload:
195
+ """Decrypt and verify a kind 30811 compute job receipt."""
196
+ if receipt.get('kind') != RECEIPT_KIND:
197
+ raise ComputeError('not a kind 30811')
198
+
199
+ issuer = receipt.get('pubkey')
200
+ if not issuer:
201
+ raise ComputeError('receipt has no pubkey')
202
+
203
+ tags = {t[0]: t for t in receipt.get('tags', []) if t and isinstance(t, (list, tuple))}
204
+ d_tag = tags.get('d')
205
+ d_val = d_tag[1] if d_tag and len(d_tag) > 1 else None
206
+
207
+ if not d_val or d_val != identity.job_tag(buyer_sk, issuer):
208
+ raise ComputeError('d tag is not the blinded job identifier derived with this seller')
209
+
210
+ try:
211
+ conv = nip44.conversation_key(buyer_sk, issuer)
212
+ plain = nip44.decrypt(receipt.get('content', ''), conv)
213
+ payload = json.loads(plain)
214
+ except Exception as e:
215
+ raise ComputeError('failed to decrypt receipt ciphertext: %s' % (e,))
216
+
217
+ commit_tag = tags.get('commit')
218
+ commit_val = commit_tag[1] if commit_tag and len(commit_tag) > 1 else None
219
+ blinding = payload.get('blinding')
220
+ if not blinding:
221
+ # Fallback to empty if absent from payload
222
+ blinding = ''
223
+
224
+ if not commit_val or not crypto.open_commit(commit_val, crypto.canonical(payload), blinding):
225
+ raise ComputeError('commitment does not open to decrypted receipt payload')
226
+
227
+ return payload
@@ -0,0 +1,180 @@
1
+ """ComputeLoadMonitor: host CPU utilization sensing, worker slot accounting, and job reflection decisions."""
2
+ from __future__ import annotations
3
+
4
+ import contextlib
5
+ import os
6
+ import threading
7
+ import time
8
+ from typing import Callable, Iterator, Optional, TypedDict
9
+
10
+ DEFAULT_CPU_REFLECT_CEILING: float = 0.75
11
+
12
+
13
+ class ComputeLoadSnapshot(TypedDict):
14
+ cpu_utilization_ratio: float
15
+ cpu_load_pct: int
16
+ cpu_ceiling: float
17
+ active_jobs: int
18
+ max_concurrent_jobs: int
19
+ free_slots: int
20
+ should_reflect: bool
21
+ reflect_reason: str
22
+ jobs_reflected: int
23
+ total_fuel_used: int
24
+
25
+
26
+ def _read_proc_stat_jiffies() -> Optional[tuple[int, int]]:
27
+ """Read `(idle_jiffies, total_jiffies)` from `/proc/stat` on Linux hosts."""
28
+ try:
29
+ with open('/proc/stat', 'r', encoding='utf-8') as f:
30
+ first_line = f.readline()
31
+ if not first_line.startswith('cpu '):
32
+ return None
33
+ parts = [int(x) for x in first_line.split()[1:]]
34
+ if len(parts) < 4:
35
+ return None
36
+ # user, nice, system, idle, iowait, irq, softirq, steal
37
+ idle = parts[3] + (parts[4] if len(parts) > 4 else 0)
38
+ total = sum(parts[:8])
39
+ return idle, total
40
+ except (OSError, ValueError, IndexError):
41
+ return None
42
+
43
+
44
+ class ComputeLoadMonitor:
45
+ """Thread-safe CPU load sensor and active compute job slot tracker for a meshnode."""
46
+
47
+ def __init__(
48
+ self,
49
+ *,
50
+ max_concurrent_jobs: Optional[int] = None,
51
+ cpu_ceiling: float = DEFAULT_CPU_REFLECT_CEILING,
52
+ cpu_sampler: Optional[Callable[[], float]] = None,
53
+ ) -> None:
54
+ cores = max(1, os.cpu_count() or 2)
55
+ self.max_concurrent_jobs: int = max(1, int(max_concurrent_jobs if max_concurrent_jobs is not None else max(1, cores // 2)))
56
+ self.cpu_ceiling: float = max(0.05, min(0.99, float(cpu_ceiling)))
57
+ self._cpu_sampler: Optional[Callable[[], float]] = cpu_sampler
58
+ self._lock: threading.Lock = threading.Lock()
59
+ self.active_jobs: int = 0
60
+ self.jobs_reflected: int = 0
61
+ self.total_fuel_used: int = 0
62
+ self._last_sample_at: float = 0.0
63
+ self._last_cpu_ratio: float = 0.0
64
+ self._prev_jiffies: Optional[tuple[int, int]] = _read_proc_stat_jiffies()
65
+
66
+ def set_cpu_sampler(self, sampler: Optional[Callable[[], float]]) -> None:
67
+ with self._lock:
68
+ self._cpu_sampler = sampler
69
+
70
+ def cpu_utilization_ratio(self) -> float:
71
+ """Return current host CPU utilization ratio in `[0.0, 1.0]`."""
72
+ with self._lock:
73
+ if self._cpu_sampler is not None:
74
+ try:
75
+ return max(0.0, min(1.0, float(self._cpu_sampler())))
76
+ except Exception:
77
+ return 0.0
78
+
79
+ now = time.monotonic()
80
+ if now - self._last_sample_at < 0.75 and self._last_sample_at > 0.0:
81
+ return self._last_cpu_ratio
82
+
83
+ cur_jiffies = _read_proc_stat_jiffies()
84
+ if cur_jiffies is not None:
85
+ prev = self._prev_jiffies
86
+ self._prev_jiffies = cur_jiffies
87
+ if prev is not None:
88
+ d_idle = cur_jiffies[0] - prev[0]
89
+ d_total = cur_jiffies[1] - prev[1]
90
+ if d_total > 0:
91
+ ratio = max(0.0, min(1.0, 1.0 - (float(d_idle) / float(d_total))))
92
+ self._last_cpu_ratio = round(ratio, 4)
93
+ self._last_sample_at = now
94
+ return self._last_cpu_ratio
95
+
96
+ if hasattr(os, 'getloadavg'):
97
+ try:
98
+ load_1m = float(os.getloadavg()[0])
99
+ except OSError:
100
+ self._last_sample_at = now
101
+ return self._last_cpu_ratio
102
+ cores = max(1, os.cpu_count() or 1)
103
+ ratio = max(0.0, min(1.0, load_1m / float(cores)))
104
+ self._last_cpu_ratio = round(ratio, 4)
105
+ self._last_sample_at = now
106
+ return self._last_cpu_ratio
107
+
108
+ self._last_sample_at = now
109
+ return self._last_cpu_ratio
110
+
111
+ @property
112
+ def free_slots(self) -> int:
113
+ with self._lock:
114
+ return max(0, self.max_concurrent_jobs - self.active_jobs)
115
+
116
+ def should_reflect_job(self, cpu_ceiling: Optional[float] = None) -> tuple[bool, str]:
117
+ """Return `(should_reflect, reason)` based on active job slots and live CPU load."""
118
+ ceiling = float(cpu_ceiling if cpu_ceiling is not None else self.cpu_ceiling)
119
+ with self._lock:
120
+ active = self.active_jobs
121
+ max_jobs = self.max_concurrent_jobs
122
+ if active >= max_jobs:
123
+ return True, f'slots_full ({active}/{max_jobs})'
124
+ cpu = self.cpu_utilization_ratio()
125
+ if cpu >= ceiling:
126
+ return True, f'high_cpu ({int(round(cpu * 100))}% >= {int(round(ceiling * 100))}%)'
127
+ return False, 'local_capacity_available'
128
+
129
+ @contextlib.contextmanager
130
+ def job_slot(self) -> Iterator[None]:
131
+ """Acquire a local compute worker slot for the duration of a job."""
132
+ with self._lock:
133
+ self.active_jobs += 1
134
+ try:
135
+ yield
136
+ finally:
137
+ with self._lock:
138
+ self.active_jobs = max(0, self.active_jobs - 1)
139
+
140
+ def record_fuel(self, fuel_used: int) -> None:
141
+ with self._lock:
142
+ self.total_fuel_used += max(0, int(fuel_used))
143
+
144
+ def record_reflection(self) -> None:
145
+ with self._lock:
146
+ self.jobs_reflected += 1
147
+
148
+ def offer_limits(self, max_fuel: int = 50_000_000_000) -> list[list[str]]:
149
+ """Build Kind 11803 `['limit', ...]` tags advertising live CPU load and free worker slots."""
150
+ cpu_pct = int(round(self.cpu_utilization_ratio() * 100))
151
+ return [
152
+ ['fuel_max', str(int(max_fuel))],
153
+ ['cpu_load_pct', str(cpu_pct)],
154
+ ['free_slots', str(self.free_slots)],
155
+ ['concurrent', str(self.max_concurrent_jobs)],
156
+ ]
157
+
158
+ def snapshot(self) -> ComputeLoadSnapshot:
159
+ cpu = self.cpu_utilization_ratio()
160
+ with self._lock:
161
+ active = self.active_jobs
162
+ max_j = self.max_concurrent_jobs
163
+ reflected = self.jobs_reflected
164
+ fuel = self.total_fuel_used
165
+ reflect, reason = self.should_reflect_job()
166
+ return {
167
+ 'cpu_utilization_ratio': round(cpu, 4),
168
+ 'cpu_load_pct': int(round(cpu * 100)),
169
+ 'cpu_ceiling': self.cpu_ceiling,
170
+ 'active_jobs': active,
171
+ 'max_concurrent_jobs': max_j,
172
+ 'free_slots': max(0, max_j - active),
173
+ 'should_reflect': reflect,
174
+ 'reflect_reason': reason,
175
+ 'jobs_reflected': reflected,
176
+ 'total_fuel_used': fuel,
177
+ }
178
+
179
+
180
+ __all__ = ['DEFAULT_CPU_REFLECT_CEILING', 'ComputeLoadMonitor', 'ComputeLoadSnapshot']