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,282 @@
|
|
|
1
|
+
"""Resource limits and the forked job sandbox (rlimits, wall-clock kill).
|
|
2
|
+
|
|
3
|
+
Separate because the sandbox state (_IN_SANDBOX_CHILD, the availability probe) is module-global and mutated in the forked child, so it must live in exactly one module.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import os
|
|
9
|
+
import signal as signal_mod
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Callable, Iterator, Optional
|
|
14
|
+
|
|
15
|
+
import types
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import resource as _resource_mod
|
|
19
|
+
except ImportError: # pragma: no cover - resource is POSIX-only
|
|
20
|
+
_resource_mod = None # type: ignore[assignment] # stub types the module as always present; it is absent on Windows
|
|
21
|
+
resource: types.ModuleType | None = _resource_mod
|
|
22
|
+
|
|
23
|
+
from .common import ComputeError
|
|
24
|
+
|
|
25
|
+
class ComputeLimits:
|
|
26
|
+
"""Resource bounds for compute execution."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
max_fuel: int = 2500000000,
|
|
31
|
+
max_memory_bytes: int = 1073741824,
|
|
32
|
+
timeout_seconds: int = 60,
|
|
33
|
+
max_tokens: int = 4096,
|
|
34
|
+
max_steps: int = 25,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.max_fuel = max_fuel
|
|
37
|
+
self.max_memory_bytes = max_memory_bytes
|
|
38
|
+
self.timeout_seconds = timeout_seconds
|
|
39
|
+
self.max_tokens = max_tokens
|
|
40
|
+
self.max_steps = max_steps
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Operator ceilings: a job request can ask for less, never more. Override with
|
|
44
|
+
# FLEETMESH_COMPUTE_MAX_TIMEOUT_S / FLEETMESH_COMPUTE_MAX_MEMORY_BYTES.
|
|
45
|
+
NODE_MAX_TIMEOUT_S: int = int(os.environ.get('FLEETMESH_COMPUTE_MAX_TIMEOUT_S', '300'))
|
|
46
|
+
NODE_MAX_MEMORY_BYTES: int = int(os.environ.get('FLEETMESH_COMPUTE_MAX_MEMORY_BYTES', str(1 << 30)))
|
|
47
|
+
_SANDBOX_KILL_GRACE_S: float = 2.0
|
|
48
|
+
|
|
49
|
+
# How much of a dead child's stderr travels in the error. A panic prints a few
|
|
50
|
+
# lines; a loop printing forever must not become the error message.
|
|
51
|
+
_CHILD_STDERR_CAP: int = 2048
|
|
52
|
+
|
|
53
|
+
# Signals that really do mean a limit was hit. Everything else - SIGSEGV,
|
|
54
|
+
# SIGABRT, SIGBUS, SIGILL - is a crash in the job or the runtime, and saying
|
|
55
|
+
# "CPU or memory limit exceeded" for those names a cause that has not been
|
|
56
|
+
# established (audit 2026-10-07, I7). SIGKILL is ambiguous on purpose: it is
|
|
57
|
+
# the OOM killer, or this node's own kill on the way out, and the message says
|
|
58
|
+
# both rather than picking one.
|
|
59
|
+
_LIMIT_SIGNALS: dict[int, str] = {
|
|
60
|
+
getattr(signal_mod, 'SIGXCPU', -1): 'the CPU limit was exceeded',
|
|
61
|
+
getattr(signal_mod, 'SIGKILL', -1):
|
|
62
|
+
'killed - the kernel out-of-memory killer, or this node killing it on the way out',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _signal_name(sig: Optional[int]) -> str:
|
|
67
|
+
"""'SIGABRT (6)' where the number is a signal, else just the number."""
|
|
68
|
+
if sig is None:
|
|
69
|
+
return 'unknown'
|
|
70
|
+
try:
|
|
71
|
+
return '%s (%d)' % (signal_mod.Signals(sig).name, sig)
|
|
72
|
+
except (ValueError, AttributeError):
|
|
73
|
+
return str(sig)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_child_stderr(fh: Any) -> str:
|
|
77
|
+
"""The last _CHILD_STDERR_CAP bytes the child wrote, and close the file.
|
|
78
|
+
|
|
79
|
+
The tail rather than the head: a panic's reason is the last thing printed,
|
|
80
|
+
and a job that logged for a while before dying would otherwise push it out.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
fh.seek(0, os.SEEK_END)
|
|
84
|
+
size = fh.tell()
|
|
85
|
+
fh.seek(max(0, size - _CHILD_STDERR_CAP))
|
|
86
|
+
raw = fh.read()
|
|
87
|
+
except Exception:
|
|
88
|
+
return ''
|
|
89
|
+
finally:
|
|
90
|
+
with contextlib.suppress(Exception):
|
|
91
|
+
fh.close()
|
|
92
|
+
text = raw.decode('utf-8', 'replace') if isinstance(raw, bytes) else str(raw)
|
|
93
|
+
if len(text) >= _CHILD_STDERR_CAP:
|
|
94
|
+
text = '…' + text[-_CHILD_STDERR_CAP:]
|
|
95
|
+
return text
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _died_message(sig: Optional[int], stderr: str) -> str:
|
|
99
|
+
"""What is actually known about a child that died without answering.
|
|
100
|
+
|
|
101
|
+
Before this, every signal got "CPU or memory limit exceeded" stapled to
|
|
102
|
+
it. Observed in practice: signal 6 is SIGABRT - a wasmtime panic, with the
|
|
103
|
+
reason printed one line earlier ("cannot use Wasmtime in a forked process
|
|
104
|
+
when mach ports are configured") - and a SIGSEGV in an executor and a
|
|
105
|
+
kernel OOM kill read identically. The signal number was always there; the
|
|
106
|
+
diagnosis beside it was wrong, and it is the half an operator reads.
|
|
107
|
+
"""
|
|
108
|
+
out = 'job sandbox died (signal/exit %s)' % _signal_name(sig)
|
|
109
|
+
if sig is not None and sig in _LIMIT_SIGNALS:
|
|
110
|
+
out += ': %s' % _LIMIT_SIGNALS[sig]
|
|
111
|
+
if stderr:
|
|
112
|
+
out += '; it said: %s' % stderr.strip()
|
|
113
|
+
return out
|
|
114
|
+
# Address space the wasm runtime needs around a job (sigaltstack, JIT pages,
|
|
115
|
+
# guard regions), granted on top of the job's own ceiling; see sandbox_limits.
|
|
116
|
+
_RUNTIME_SLACK_BYTES: int = 64 << 20
|
|
117
|
+
|
|
118
|
+
# True only inside a forked job child; rlimits are never touched in the node process.
|
|
119
|
+
_IN_SANDBOX_CHILD: bool = False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def clamp_limits(limits: ComputeLimits) -> ComputeLimits:
|
|
123
|
+
"""Clamp requester-supplied limits to the node operator's ceilings."""
|
|
124
|
+
def _c(v: int, cap: int) -> int:
|
|
125
|
+
v = int(v) if v else cap
|
|
126
|
+
return max(1, min(v, cap))
|
|
127
|
+
return ComputeLimits(
|
|
128
|
+
max_fuel=limits.max_fuel,
|
|
129
|
+
max_memory_bytes=_c(limits.max_memory_bytes, NODE_MAX_MEMORY_BYTES),
|
|
130
|
+
timeout_seconds=_c(limits.timeout_seconds, NODE_MAX_TIMEOUT_S),
|
|
131
|
+
max_tokens=limits.max_tokens,
|
|
132
|
+
max_steps=limits.max_steps,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _apply_rlimits(limits: ComputeLimits) -> None:
|
|
137
|
+
"""Set process-wide rlimits. Only ever called in a disposable job child."""
|
|
138
|
+
if resource is None:
|
|
139
|
+
return
|
|
140
|
+
if limits.timeout_seconds and hasattr(resource, 'RLIMIT_CPU'):
|
|
141
|
+
try:
|
|
142
|
+
used = int(time.process_time()) # child inherits parent's CPU accounting
|
|
143
|
+
soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
|
|
144
|
+
want = used + int(limits.timeout_seconds) + 1
|
|
145
|
+
if hard != resource.RLIM_INFINITY:
|
|
146
|
+
want = min(want, hard)
|
|
147
|
+
resource.setrlimit(resource.RLIMIT_CPU, (want, hard))
|
|
148
|
+
except (ValueError, OSError):
|
|
149
|
+
pass
|
|
150
|
+
if limits.max_memory_bytes and hasattr(resource, 'RLIMIT_AS'):
|
|
151
|
+
try:
|
|
152
|
+
soft, hard = resource.getrlimit(resource.RLIMIT_AS)
|
|
153
|
+
# RLIMIT_AS counts the whole address space, and a child forked
|
|
154
|
+
# from a running node inherits the node's (thread stacks, sqlite
|
|
155
|
+
# maps, loaded libraries - often most of a gigabyte). An absolute
|
|
156
|
+
# cap left a job almost nothing; grant the job its bytes on top
|
|
157
|
+
# of what it was born with.
|
|
158
|
+
# The runtime itself also needs room the job did not ask for:
|
|
159
|
+
# wasmtime allocates a sigaltstack per thread, JIT code pages
|
|
160
|
+
# and guard regions before the module runs a single
|
|
161
|
+
# instruction. Without this slack a small ceiling aborted the
|
|
162
|
+
# child with "failed to allocate memory for sigaltstack" and
|
|
163
|
+
# the job died at the rlimit instead of trapping cleanly at the
|
|
164
|
+
# store limit - and whether it did depended on how much address
|
|
165
|
+
# space the parent happened to hold at fork, which made the
|
|
166
|
+
# outcome flake with unrelated code. The wasm store limit (set
|
|
167
|
+
# from max_memory_bytes by the executor) is what bounds the
|
|
168
|
+
# module; this rlimit only backstops the runtime around it.
|
|
169
|
+
want = _current_address_space() + int(limits.max_memory_bytes) + _RUNTIME_SLACK_BYTES
|
|
170
|
+
if hard != resource.RLIM_INFINITY:
|
|
171
|
+
want = min(want, hard)
|
|
172
|
+
resource.setrlimit(resource.RLIMIT_AS, (want, hard))
|
|
173
|
+
except (ValueError, OSError):
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _current_address_space() -> int:
|
|
178
|
+
try:
|
|
179
|
+
with open('/proc/self/statm') as f:
|
|
180
|
+
return int(f.read().split()[0]) * os.sysconf('SC_PAGE_SIZE')
|
|
181
|
+
except (OSError, ValueError, IndexError):
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@contextlib.contextmanager
|
|
186
|
+
def sandbox_limits(limits: Optional[ComputeLimits] = None) -> Iterator[None]:
|
|
187
|
+
"""Apply rlimits for the duration of the block -- inside a job child only.
|
|
188
|
+
|
|
189
|
+
rlimits are process-wide: setting them in the node process let one job's
|
|
190
|
+
RLIMIT_CPU (measured against the node's total CPU time) SIGXCPU-kill the
|
|
191
|
+
whole node, and RLIMIT_AS cap every thread. Outside a sandbox child this
|
|
192
|
+
is therefore a no-op; use run_sandboxed() for real isolation.
|
|
193
|
+
"""
|
|
194
|
+
if resource is None or limits is None or not _IN_SANDBOX_CHILD:
|
|
195
|
+
yield
|
|
196
|
+
return
|
|
197
|
+
_apply_rlimits(limits)
|
|
198
|
+
yield
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _sandbox_available() -> bool:
|
|
202
|
+
import multiprocessing
|
|
203
|
+
return resource is not None and 'fork' in multiprocessing.get_all_start_methods()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def run_sandboxed(fn: Callable[[], Any], limits: ComputeLimits, *, allow_unsandboxed: bool = False) -> Any:
|
|
207
|
+
"""Run fn() in a forked child with CPU/memory rlimits and a wall-clock kill.
|
|
208
|
+
|
|
209
|
+
Limit breaches kill only the child and surface as ComputeError. The return
|
|
210
|
+
value must be picklable. Where fork/rlimits are unavailable the job is
|
|
211
|
+
refused unless ``allow_unsandboxed`` is set, in which case it runs
|
|
212
|
+
in-process with no resource limits: an untrusted module must never get
|
|
213
|
+
that silently, because the operator would believe it was contained.
|
|
214
|
+
"""
|
|
215
|
+
limits = clamp_limits(limits)
|
|
216
|
+
if not _sandbox_available():
|
|
217
|
+
if not allow_unsandboxed:
|
|
218
|
+
raise ComputeError('sandbox unavailable on this host (no fork/rlimits); '
|
|
219
|
+
'refusing to run the job in-process without allow_unsandboxed=True')
|
|
220
|
+
return fn()
|
|
221
|
+
import multiprocessing
|
|
222
|
+
ctx = multiprocessing.get_context('fork')
|
|
223
|
+
parent_conn, child_conn = ctx.Pipe(duplex=False)
|
|
224
|
+
|
|
225
|
+
# The child's stderr goes to a file so that a native crash - which sends
|
|
226
|
+
# nothing down the pipe, because it never reaches Python again - still
|
|
227
|
+
# leaves its reason somewhere the parent can read. Nothing is swallowed:
|
|
228
|
+
# on success the parent copies whatever was written back to the real
|
|
229
|
+
# stderr, so a job's own warnings still reach the journal.
|
|
230
|
+
err_file = tempfile.TemporaryFile()
|
|
231
|
+
|
|
232
|
+
def _child() -> None:
|
|
233
|
+
global _IN_SANDBOX_CHILD
|
|
234
|
+
_IN_SANDBOX_CHILD = True
|
|
235
|
+
try:
|
|
236
|
+
os.dup2(err_file.fileno(), 2)
|
|
237
|
+
except OSError:
|
|
238
|
+
pass # no stderr to redirect: carry on, the pipe still works
|
|
239
|
+
try:
|
|
240
|
+
_apply_rlimits(limits)
|
|
241
|
+
child_conn.send(('ok', fn()))
|
|
242
|
+
except MemoryError:
|
|
243
|
+
child_conn.send(('err', 'job exceeded memory limit (%d bytes)' % limits.max_memory_bytes))
|
|
244
|
+
except BaseException as e: # noqa: BLE001 - report anything to the parent
|
|
245
|
+
try:
|
|
246
|
+
child_conn.send(('err', '%s: %s' % (type(e).__name__, e)))
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
finally:
|
|
250
|
+
child_conn.close()
|
|
251
|
+
|
|
252
|
+
proc = ctx.Process(target=_child, daemon=True)
|
|
253
|
+
proc.start()
|
|
254
|
+
child_conn.close()
|
|
255
|
+
try:
|
|
256
|
+
deadline = limits.timeout_seconds + _SANDBOX_KILL_GRACE_S
|
|
257
|
+
if parent_conn.poll(deadline):
|
|
258
|
+
try:
|
|
259
|
+
status, value = parent_conn.recv()
|
|
260
|
+
except EOFError:
|
|
261
|
+
status, value = 'err', None
|
|
262
|
+
else:
|
|
263
|
+
status, value = 'timeout', None
|
|
264
|
+
finally:
|
|
265
|
+
if proc.is_alive():
|
|
266
|
+
proc.kill()
|
|
267
|
+
proc.join(5)
|
|
268
|
+
parent_conn.close()
|
|
269
|
+
said = _read_child_stderr(err_file)
|
|
270
|
+
if status == 'ok':
|
|
271
|
+
if said:
|
|
272
|
+
# The job ran and had something to say. It is not an error, so it
|
|
273
|
+
# goes where it would have gone without the redirection.
|
|
274
|
+
with contextlib.suppress(Exception):
|
|
275
|
+
sys.stderr.write(said)
|
|
276
|
+
return value
|
|
277
|
+
if status == 'timeout':
|
|
278
|
+
raise ComputeError('job exceeded wall-clock limit (%ss)' % limits.timeout_seconds)
|
|
279
|
+
if value is None:
|
|
280
|
+
sig = -proc.exitcode if proc.exitcode and proc.exitcode < 0 else proc.exitcode
|
|
281
|
+
raise ComputeError(_died_message(sig, said))
|
|
282
|
+
raise ComputeError('job failed in sandbox: %s' % value)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Types and records for the Compute compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class JobRunResult(TypedDict, total=False):
|
|
8
|
+
"""What run_job hands back to receipt builders and the wire layer.
|
|
9
|
+
|
|
10
|
+
result_cid is absent on the custom_runner path (no blob store there); all
|
|
11
|
+
other keys are always present.
|
|
12
|
+
"""
|
|
13
|
+
result_hash: str
|
|
14
|
+
result_cid: Optional[str]
|
|
15
|
+
raw_output: bytes
|
|
16
|
+
fuel_used: int
|
|
17
|
+
engine: str
|
|
18
|
+
wasi_profile_digest: str
|
|
19
|
+
counters: Mapping[str, int | float]
|
|
20
|
+
verification_mode: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatMessage(TypedDict, total=False):
|
|
24
|
+
"""Chat completion message."""
|
|
25
|
+
role: str
|
|
26
|
+
content: str
|
|
27
|
+
name: Optional[str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OpenAIChatRequest(TypedDict, total=False):
|
|
31
|
+
"""Normalized OpenAI chat completions payload."""
|
|
32
|
+
model: str
|
|
33
|
+
messages: list[ChatMessage]
|
|
34
|
+
max_tokens: int
|
|
35
|
+
temperature: float
|
|
36
|
+
top_p: float
|
|
37
|
+
stream: bool
|
|
38
|
+
seed: Optional[int]
|
|
39
|
+
response_format: Optional[Mapping[str, Any]]
|
|
40
|
+
guided_regex: Optional[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BacalhauJobSummary(TypedDict, total=False):
|
|
44
|
+
"""Summary of a Bacalhau job specification."""
|
|
45
|
+
status: str
|
|
46
|
+
engine: str
|
|
47
|
+
image: str
|
|
48
|
+
entrypoint: str | list[str]
|
|
49
|
+
parameters: list[str]
|
|
50
|
+
mounted_paths: list[str]
|
|
51
|
+
outputs: list[str]
|
|
52
|
+
bytes_processed: int
|
|
53
|
+
results: Optional[Mapping[str, Any]]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AgentTraceStep(TypedDict, total=False):
|
|
57
|
+
"""Step in an autonomous agent execution trace."""
|
|
58
|
+
step: int
|
|
59
|
+
thought: str
|
|
60
|
+
action: str
|
|
61
|
+
observation: str
|
|
62
|
+
output: Optional[str | Mapping[str, str | int | float | bool]]
|
|
63
|
+
sub_ask_id: Optional[str]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AgentSubAsk(TypedDict, total=False):
|
|
67
|
+
"""Sub-delegation ask in an agent orchestration tree."""
|
|
68
|
+
ask_id: str
|
|
69
|
+
task: str
|
|
70
|
+
budget_msat: int
|
|
71
|
+
status: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ComputeReceiptPayload(TypedDict, total=False):
|
|
75
|
+
"""Payload of a decrypted compute receipt (kind 30811)."""
|
|
76
|
+
result_hash: str
|
|
77
|
+
result_cid: Optional[str]
|
|
78
|
+
raw_output: bytes | str
|
|
79
|
+
fuel_used: int
|
|
80
|
+
engine: str
|
|
81
|
+
wasi_profile_digest: str
|
|
82
|
+
counters: Mapping[str, int | float]
|
|
83
|
+
verification_mode: str
|
|
84
|
+
blinding: Optional[str]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""compute/1.0: Wire protocol, DIDComm transport lifecycle, accountable settlement, and dispute automation.
|
|
2
|
+
|
|
3
|
+
Implements the complete compute market wire lifecycle:
|
|
4
|
+
- DIDComm compute/1.0 message types (quote, accept, sealed, settled, refute)
|
|
5
|
+
- Cryptographic sealing under Lightning payment preimage k (NIP-44 v2 / ChaCha20+HMAC)
|
|
6
|
+
- Accountable delivery verification and fault classification (sealed-key-does-not-open,
|
|
7
|
+
result-hash-mismatch-after-decrypt, rerun-divergence, proof-invalid)
|
|
8
|
+
- Automated bilateral negotiation state machine (buyer and seller sessions)
|
|
9
|
+
- Conformance report generation (Kind 30802) citing signed sealed evidence under fleetmesh_core/reports.py
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from .messages import (
|
|
15
|
+
PIURI,
|
|
16
|
+
PIURI_V11,
|
|
17
|
+
QUOTE_TYPE,
|
|
18
|
+
ACCEPT_TYPE,
|
|
19
|
+
SEALED_TYPE,
|
|
20
|
+
SETTLED_TYPE,
|
|
21
|
+
REFUTE_TYPE,
|
|
22
|
+
STREAM_TYPE,
|
|
23
|
+
PROGRESS_TYPE,
|
|
24
|
+
CANCEL_TYPE,
|
|
25
|
+
MESSAGES,
|
|
26
|
+
ComputeWireError,
|
|
27
|
+
make_quote,
|
|
28
|
+
make_accept,
|
|
29
|
+
make_sealed,
|
|
30
|
+
make_settled,
|
|
31
|
+
make_refute,
|
|
32
|
+
make_stream_chunk,
|
|
33
|
+
make_progress,
|
|
34
|
+
make_cancel,
|
|
35
|
+
)
|
|
36
|
+
from .sealing import seal_result, open_sealed, encrypt_stream_frame, decrypt_stream_frame
|
|
37
|
+
from .verify import vk_is_trusted, evaluate_job_evidence, build_compute_dispute_report
|
|
38
|
+
from .types import (
|
|
39
|
+
ComputeDisputeRecord,
|
|
40
|
+
ComputeSessionSummary,
|
|
41
|
+
ComputeWireStateReport,
|
|
42
|
+
EvidenceDetail,
|
|
43
|
+
SealedResult,
|
|
44
|
+
)
|
|
45
|
+
from .session import ComputeSession
|
|
46
|
+
from .compartment import ComputeWire
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
'PIURI', 'PIURI_V11', 'QUOTE_TYPE', 'ACCEPT_TYPE', 'SEALED_TYPE', 'SETTLED_TYPE',
|
|
50
|
+
'REFUTE_TYPE', 'STREAM_TYPE', 'PROGRESS_TYPE', 'CANCEL_TYPE', 'MESSAGES',
|
|
51
|
+
'ComputeWireError', 'make_quote', 'make_accept', 'make_sealed', 'make_settled',
|
|
52
|
+
'make_refute', 'make_stream_chunk', 'make_progress', 'make_cancel',
|
|
53
|
+
'seal_result', 'open_sealed', 'encrypt_stream_frame', 'decrypt_stream_frame',
|
|
54
|
+
'vk_is_trusted', 'evaluate_job_evidence', 'build_compute_dispute_report',
|
|
55
|
+
'ComputeSession', 'ComputeWire',
|
|
56
|
+
'ComputeDisputeRecord', 'ComputeSessionSummary', 'ComputeWireStateReport',
|
|
57
|
+
'EvidenceDetail', 'SealedResult',
|
|
58
|
+
]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""ComputeWire: the subsystem that routes compute/1.0 envelopes to ComputeSession.
|
|
2
|
+
|
|
3
|
+
Separate so the subsystem wiring stays a thin adapter over the session and message modules.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Mapping, Optional
|
|
8
|
+
|
|
9
|
+
from node.config import ComputeConfig
|
|
10
|
+
from node.compartment import Command, Handler, Need, Outbound, WireProtocol
|
|
11
|
+
from node import channels as ch_mod
|
|
12
|
+
from node import metrics as mx_mod
|
|
13
|
+
from node.compartment import stores as st
|
|
14
|
+
from node.planes.services.compute import executors as compute_executors
|
|
15
|
+
from .messages import PIURI, PIURI_V11
|
|
16
|
+
from .session import ComputeSession
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from .types import (
|
|
20
|
+
ComputeDisputeRecord,
|
|
21
|
+
ComputeSessionSummary,
|
|
22
|
+
ComputeWireStateReport,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# -------------------------------------------------------------------------
|
|
27
|
+
# Compute Wire Compartment (Transport Compartment)
|
|
28
|
+
# -------------------------------------------------------------------------
|
|
29
|
+
class ComputeWire(WireProtocol):
|
|
30
|
+
"""The compute/1.0 and compute/1.1 wire transport compartment for meshnode."""
|
|
31
|
+
|
|
32
|
+
optional: bool = True
|
|
33
|
+
enabled_by: Optional[str] = 'compute'
|
|
34
|
+
name: str = 'compute_wire'
|
|
35
|
+
reads: tuple[str, ...] = ('compute', 'relays', 'transport')
|
|
36
|
+
config_cls = ComputeConfig
|
|
37
|
+
piuris: tuple[str, ...] = (PIURI, PIURI_V11)
|
|
38
|
+
OUTBOUND = (
|
|
39
|
+
Outbound('quote', reply=('accept', 'cancel'), doc='quote price and terms for compute task'),
|
|
40
|
+
Outbound('accept', reply=('sealed', 'cancel'), doc='accept quote and escrow payment'),
|
|
41
|
+
Outbound('sealed', reply=('settled', 'refute'), doc='deliver encrypted execution payload'),
|
|
42
|
+
Outbound('settled', doc='reveal payment preimage upon verified settlement'),
|
|
43
|
+
Outbound('refute', doc='refute execution proof or result divergence'),
|
|
44
|
+
Outbound('stream', doc='stream output tokens or chunk deltas'),
|
|
45
|
+
Outbound('progress', doc='stream execution progress heartbeat'),
|
|
46
|
+
Outbound('cancel', doc='cancel compute task execution'),
|
|
47
|
+
)
|
|
48
|
+
COMMANDS = (
|
|
49
|
+
Command(
|
|
50
|
+
key='sessions',
|
|
51
|
+
name='Compute Sessions',
|
|
52
|
+
description='list active bilateral compute sessions',
|
|
53
|
+
tags=('compute', 'sessions'),
|
|
54
|
+
permission='compute_wire/sessions',
|
|
55
|
+
run='list_sessions',
|
|
56
|
+
reply='dict',
|
|
57
|
+
),
|
|
58
|
+
Command(
|
|
59
|
+
key='disputes',
|
|
60
|
+
name='Compute Disputes',
|
|
61
|
+
description='list compute refutations and dispute reports filed',
|
|
62
|
+
tags=('compute', 'disputes'),
|
|
63
|
+
permission='compute_wire/disputes',
|
|
64
|
+
run='list_disputes',
|
|
65
|
+
reply='list',
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
HANDLERS = (
|
|
70
|
+
Handler('quote', run='handle_quote', doc='receive compute quotation'),
|
|
71
|
+
Handler('accept', run='handle_accept', doc='receive compute job acceptance'),
|
|
72
|
+
Handler('sealed', run='handle_sealed', doc='receive sealed compute payload'),
|
|
73
|
+
Handler('settled', run='handle_settled', doc='receive compute settlement preimage'),
|
|
74
|
+
Handler('refute', run='handle_refute', writes=('disputes',), doc='receive compute refutation dispute'),
|
|
75
|
+
Handler('stream', run='handle_stream', doc='receive real-time streaming output chunk'),
|
|
76
|
+
Handler('progress', run='handle_progress', doc='receive long-running progress heartbeat'),
|
|
77
|
+
Handler('cancel', run='handle_cancel', doc='receive compute job cancellation'),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# What it keeps. Each refute a peer sends is kept whole, and any peer
|
|
81
|
+
# can send one; this was a list that grew by one per refute for the life
|
|
82
|
+
# of the process. The newest DISPUTES_KEPT now, and `disputes_filed`
|
|
83
|
+
# still counts every one (the ring's inserts).
|
|
84
|
+
DISPUTES_KEPT = 256
|
|
85
|
+
STORES = (
|
|
86
|
+
st.Store(key='disputes', description='refutes received, as {peer, refute}, newest last',
|
|
87
|
+
kind=st.RING, shape=dict, bound=DISPUTES_KEPT),
|
|
88
|
+
)
|
|
89
|
+
CHANNELS = (
|
|
90
|
+
ch_mod.Channel('refute_received', 'A compute refutation dispute was received from a peer',
|
|
91
|
+
level=ch_mod.TROUBLE, say='compute dispute from {peer} for receipt {receipt}',
|
|
92
|
+
tags=('compute', 'wire', 'dispute')),
|
|
93
|
+
ch_mod.Channel('quote_received', 'A compute quotation was received from an executor peer',
|
|
94
|
+
level=ch_mod.NOTE, say='compute quote from {peer} price {price} msat',
|
|
95
|
+
tags=('compute', 'wire')),
|
|
96
|
+
)
|
|
97
|
+
METRICS = (
|
|
98
|
+
mx_mod.Metric(name='compute_wire_disputes', description='Compute disputes and refutations received.',
|
|
99
|
+
kind=mx_mod.COUNTER, counts='refute_received', tags=('compute', 'wire', 'disputes')),
|
|
100
|
+
mx_mod.Metric(name='compute_wire_quotes', description='Compute quotes received from peers.',
|
|
101
|
+
kind=mx_mod.COUNTER, counts='quote_received', tags=('compute', 'wire', 'quotes')),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
NEEDS = (
|
|
105
|
+
Need('ks', 'ks'),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def __init__(self, node: Any = None, ks: Any = None) -> None:
|
|
109
|
+
super().__init__(node)
|
|
110
|
+
self.ks: Any = ks if ks is not None else getattr(node, 'ks', None)
|
|
111
|
+
self.sessions: dict[str, ComputeSession] = {} # not a store: never written - nothing constructs a ComputeSession yet; the sessions command reads it
|
|
112
|
+
self.disputes: st.Ring[ComputeDisputeRecord] # built and bound by the base (see STORES)
|
|
113
|
+
self.registry: compute_executors.ExecutorRegistry = compute_executors.DEFAULT_REGISTRY
|
|
114
|
+
|
|
115
|
+
def pinned_vk_digests(self) -> list[str]:
|
|
116
|
+
"""Operator-pinned Groth16 verification-key digests (cfg compute.zk_pinned_vks)."""
|
|
117
|
+
return list(ComputeConfig.from_dict(self.config('compute') or {}).zk_pinned_vks)
|
|
118
|
+
|
|
119
|
+
def state(self) -> ComputeWireStateReport:
|
|
120
|
+
return {
|
|
121
|
+
'active_sessions': len(self.sessions),
|
|
122
|
+
'disputes_filed': self.disputes.inserts,
|
|
123
|
+
'refusals': self.refusals.total(),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def list_sessions(self) -> dict[str, ComputeSessionSummary]:
|
|
127
|
+
return {
|
|
128
|
+
sid: {'role': s.role, 'peer': s.peer_pk, 'state': s.state}
|
|
129
|
+
for sid, s in self.sessions.items()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def list_disputes(self) -> list[ComputeDisputeRecord]:
|
|
133
|
+
return list(self.disputes)
|
|
134
|
+
|
|
135
|
+
def _my_sk(self) -> str:
|
|
136
|
+
ks = self.ks
|
|
137
|
+
if ks is not None and getattr(ks, 'node_sk', None):
|
|
138
|
+
return str(ks.node_sk)
|
|
139
|
+
return '11' * 32
|
|
140
|
+
|
|
141
|
+
def _peer_pk(self, sender: str) -> str:
|
|
142
|
+
s = str(sender or '').strip()
|
|
143
|
+
if s.startswith('did:nostr:'):
|
|
144
|
+
s = s[len('did:nostr:'):]
|
|
145
|
+
if len(s) == 64:
|
|
146
|
+
return s.lower()
|
|
147
|
+
return '22' * 32
|
|
148
|
+
|
|
149
|
+
def open_buyer_session(
|
|
150
|
+
self,
|
|
151
|
+
ask_id: str,
|
|
152
|
+
peer_pk: str,
|
|
153
|
+
job_payload: Optional[Mapping[str, Any]] = None,
|
|
154
|
+
) -> ComputeSession:
|
|
155
|
+
sess = ComputeSession(
|
|
156
|
+
role='buyer',
|
|
157
|
+
my_sk=self._my_sk(),
|
|
158
|
+
peer_pk=self._peer_pk(peer_pk),
|
|
159
|
+
ask_id=ask_id,
|
|
160
|
+
job_payload=job_payload,
|
|
161
|
+
)
|
|
162
|
+
self.sessions[ask_id] = sess
|
|
163
|
+
return sess
|
|
164
|
+
|
|
165
|
+
def open_seller_session(
|
|
166
|
+
self,
|
|
167
|
+
ask_id: str,
|
|
168
|
+
peer_pk: str,
|
|
169
|
+
) -> ComputeSession:
|
|
170
|
+
sess = ComputeSession(
|
|
171
|
+
role='seller',
|
|
172
|
+
my_sk=self._my_sk(),
|
|
173
|
+
peer_pk=self._peer_pk(peer_pk),
|
|
174
|
+
ask_id=ask_id,
|
|
175
|
+
)
|
|
176
|
+
self.sessions[ask_id] = sess
|
|
177
|
+
return sess
|
|
178
|
+
|
|
179
|
+
def handle_quote(self, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
180
|
+
msg = getattr(inbound, 'message', inbound)
|
|
181
|
+
body = msg.get('body') or {}
|
|
182
|
+
price = body.get('price_msat', 0)
|
|
183
|
+
thid = str(msg.get('thid') or body.get('ask') or msg.get('id') or '')
|
|
184
|
+
if thid:
|
|
185
|
+
sess = self.sessions.get(thid)
|
|
186
|
+
if sess is None:
|
|
187
|
+
sess = self.open_buyer_session(thid, sender)
|
|
188
|
+
sess.quote_msg = dict(msg)
|
|
189
|
+
sess.payment_hash = body.get('payment_hash')
|
|
190
|
+
sess.state = 'QUOTED'
|
|
191
|
+
self.say('quote_received', peer=sender, price=price)
|
|
192
|
+
return ('quote_received', sender, price)
|
|
193
|
+
|
|
194
|
+
def handle_accept(self, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
195
|
+
msg = getattr(inbound, 'message', inbound)
|
|
196
|
+
thid = msg.get('thid') or msg.get('id')
|
|
197
|
+
if thid:
|
|
198
|
+
sid = str(thid)
|
|
199
|
+
sess = self.sessions.get(sid)
|
|
200
|
+
if sess is None:
|
|
201
|
+
sess = self.open_seller_session(sid, sender)
|
|
202
|
+
sess.accept_msg = dict(msg)
|
|
203
|
+
sess.state = 'ACCEPTED'
|
|
204
|
+
return ('accept_received', sender, thid)
|
|
205
|
+
|
|
206
|
+
def handle_sealed(self, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
207
|
+
msg = getattr(inbound, 'message', inbound)
|
|
208
|
+
body = msg.get('body') or {}
|
|
209
|
+
thid = str(msg.get('thid') or msg.get('id') or '')
|
|
210
|
+
if thid:
|
|
211
|
+
sess = self.sessions.get(thid)
|
|
212
|
+
if sess is None:
|
|
213
|
+
sess = self.open_buyer_session(thid, sender)
|
|
214
|
+
sess.sealed_msg = dict(msg)
|
|
215
|
+
sess.payment_hash = body.get('payment_hash')
|
|
216
|
+
sess.result_hash = body.get('result')
|
|
217
|
+
sess.state = 'SEALED'
|
|
218
|
+
return ('sealed_received', sender, body.get('payment_hash'))
|
|
219
|
+
|
|
220
|
+
def handle_settled(self, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
221
|
+
msg = getattr(inbound, 'message', inbound)
|
|
222
|
+
thid = msg.get('thid') or msg.get('id')
|
|
223
|
+
if thid and str(thid) in self.sessions:
|
|
224
|
+
self.sessions[str(thid)].settled_msg = dict(msg)
|
|
225
|
+
self.sessions[str(thid)].state = 'SETTLED'
|
|
226
|
+
return ('settled_received', sender, thid)
|
|
227
|
+
|
|
228
|
+
def handle_refute(self, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
229
|
+
msg = getattr(inbound, 'message', inbound)
|
|
230
|
+
self.disputes.append({'peer': sender, 'refute': msg})
|
|
231
|
+
receipt = (msg.get('body') or {}).get('job_receipt', '')
|
|
232
|
+
thid = msg.get('thid') or msg.get('id')
|
|
233
|
+
if thid and str(thid) in self.sessions:
|
|
234
|
+
self.sessions[str(thid)].state = 'REFUTED'
|
|
235
|
+
self.say('refute_received', peer=sender, receipt=str(receipt)[:16])
|
|
236
|
+
return ('refute_received', sender, receipt)
|
|
237
|
+
|
|
238
|
+
def handle_stream(self, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
239
|
+
msg = getattr(inbound, 'message', inbound)
|
|
240
|
+
thid = msg.get('thid') or msg.get('id')
|
|
241
|
+
return ('stream_received', sender, thid, (msg.get('body') or {}).get('seq'))
|
|
242
|
+
|
|
243
|
+
def handle_progress(self, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
244
|
+
msg = getattr(inbound, 'message', inbound)
|
|
245
|
+
thid = msg.get('thid') or msg.get('id')
|
|
246
|
+
if thid and str(thid) in self.sessions:
|
|
247
|
+
self.sessions[str(thid)].progress_history.append(dict(msg))
|
|
248
|
+
return ('progress_received', sender, thid, (msg.get('body') or {}).get('progress'))
|
|
249
|
+
|
|
250
|
+
def handle_cancel(self, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
251
|
+
msg = getattr(inbound, 'message', inbound)
|
|
252
|
+
thid = msg.get('thid') or msg.get('id')
|
|
253
|
+
if thid and str(thid) in self.sessions:
|
|
254
|
+
self.sessions[str(thid)].state = 'CANCELLED'
|
|
255
|
+
return ('cancel_received', sender, thid, (msg.get('body') or {}).get('reason'))
|
|
256
|
+
|