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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- 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']
|