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,526 @@
|
|
|
1
|
+
"""The DVM gateway compartment: takes a NIP-90 request off the relay face,
|
|
2
|
+
runs (or delegates) the job, answers with feedback and a result.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import queue
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Any, Callable, Mapping, Optional, Union
|
|
11
|
+
from node import channels as ch_mod
|
|
12
|
+
from node.planes.services import compute # noqa: E402
|
|
13
|
+
from node import metrics as mx_mod
|
|
14
|
+
from node.compartment import stores as st
|
|
15
|
+
from node.compartment import Command, Compartment, Need, Worker # noqa: E402
|
|
16
|
+
from node.config.parameter import Parameter # noqa: E402
|
|
17
|
+
from node.config import DVMConfig # noqa: E402
|
|
18
|
+
from node.interfaces import NostrEvent # noqa: E402
|
|
19
|
+
from .protocol import DVMError, build_dvm_feedback, build_dvm_response, is_dvm_request, parse_dvm_request # noqa: E402
|
|
20
|
+
from .translate import dvm_request_to_mesh_job # noqa: E402
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from .types import DVMDelegationResult, DVMStateReport
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _module_sha(job: Mapping[str, Any]) -> str:
|
|
27
|
+
m = job.get('module')
|
|
28
|
+
sha = m.get('sha256') if isinstance(m, dict) else None
|
|
29
|
+
return sha if isinstance(sha, str) else ''
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _verify_mode(job: Mapping[str, Any]) -> str:
|
|
33
|
+
v = job.get('verify')
|
|
34
|
+
mode = v.get('mode') if isinstance(v, dict) else None
|
|
35
|
+
return mode if isinstance(mode, str) else 'none'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DVMGateway(Compartment):
|
|
39
|
+
optional: bool = True
|
|
40
|
+
enabled_by: Optional[str] = 'dvm'
|
|
41
|
+
"""The NIP-90 Data Vending Machine Gateway compartment."""
|
|
42
|
+
|
|
43
|
+
name: str = 'dvm'
|
|
44
|
+
# Both optional: the order book only for `delegate_request`, the bridge
|
|
45
|
+
# only so results reach the wider relay network. Local service needs neither.
|
|
46
|
+
uses: tuple[str, ...] = ('compute', 'bridge')
|
|
47
|
+
reads: tuple[str, ...] = ('compute', 'dvm')
|
|
48
|
+
config_cls = DVMConfig
|
|
49
|
+
live: tuple[str, ...] = ('dvm',)
|
|
50
|
+
STORES = (
|
|
51
|
+
st.Store(key='recent_jobs', description='recent DVM job results, newest last',
|
|
52
|
+
kind=st.RING, shape=tuple, bound=128),
|
|
53
|
+
)
|
|
54
|
+
recent_jobs: st.Ring[tuple[Any, ...]]
|
|
55
|
+
|
|
56
|
+
CHANNELS = (
|
|
57
|
+
ch_mod.Channel('job_received', 'A NIP-90 DVM request event was received for processing',
|
|
58
|
+
level=ch_mod.INFO, say='received DVM job {job_id} (kind {kind})',
|
|
59
|
+
tags=('dvm', 'job')),
|
|
60
|
+
ch_mod.Channel('job_succeeded', 'A NIP-90 DVM job completed successfully',
|
|
61
|
+
level=ch_mod.INFO, say='completed DVM job {job_id} using {fuel} fuel',
|
|
62
|
+
tags=('dvm', 'job')),
|
|
63
|
+
ch_mod.Channel('job_failed', 'A NIP-90 DVM job failed during execution',
|
|
64
|
+
level=ch_mod.TROUBLE, say='DVM job {job_id} failed: {error}',
|
|
65
|
+
tags=('dvm', 'job')),
|
|
66
|
+
ch_mod.Channel('job_delegated', 'A NIP-90 DVM job was delegated to compute order book',
|
|
67
|
+
level=ch_mod.INFO, say='delegated DVM job {job_id} to compute order book',
|
|
68
|
+
tags=('dvm', 'delegate')),
|
|
69
|
+
)
|
|
70
|
+
METRICS = (
|
|
71
|
+
mx_mod.Metric(name='dvm_jobs_received', description='DVM job requests received.',
|
|
72
|
+
kind=mx_mod.COUNTER, counts='job_received', tags=('dvm', 'job')),
|
|
73
|
+
mx_mod.Metric(name='dvm_jobs_succeeded', description='DVM jobs completed successfully.',
|
|
74
|
+
kind=mx_mod.COUNTER, counts='job_succeeded', tags=('dvm', 'job')),
|
|
75
|
+
mx_mod.Metric(name='dvm_jobs_failed', description='DVM jobs that failed.',
|
|
76
|
+
kind=mx_mod.COUNTER, counts='job_failed', tags=('dvm', 'job')),
|
|
77
|
+
mx_mod.Metric(name='dvm_jobs_delegated', description='DVM jobs delegated to compute order book.',
|
|
78
|
+
kind=mx_mod.COUNTER, counts='job_delegated', tags=('dvm', 'delegate')),
|
|
79
|
+
)
|
|
80
|
+
WORKERS = (
|
|
81
|
+
Worker('_dvm_worker_loop', name='dvm_requests'),
|
|
82
|
+
)
|
|
83
|
+
NEEDS = (
|
|
84
|
+
Need('cfg', block=DVMConfig),
|
|
85
|
+
Need('ks'),
|
|
86
|
+
Need('face'),
|
|
87
|
+
Need('blob_store', 'stores.blobs'),
|
|
88
|
+
Need('order_book', 'compute'),
|
|
89
|
+
Need('bridge'),
|
|
90
|
+
)
|
|
91
|
+
COMMANDS = (
|
|
92
|
+
Command(
|
|
93
|
+
key='show',
|
|
94
|
+
name='DVM Gateway Status',
|
|
95
|
+
description='display DVM gateway state, supported kinds, and job counters',
|
|
96
|
+
tags=('dvm', 'status'),
|
|
97
|
+
permission='dvm/show',
|
|
98
|
+
run='state',
|
|
99
|
+
reply='dict',
|
|
100
|
+
),
|
|
101
|
+
Command(
|
|
102
|
+
key='delegate',
|
|
103
|
+
name='Delegate DVM Request',
|
|
104
|
+
description='delegate a NIP-90 request to the compute order book',
|
|
105
|
+
tags=('dvm', 'delegate'),
|
|
106
|
+
parameters=(
|
|
107
|
+
Parameter(key='request', name='request', description='NIP-90 request event dictionary', type=dict, default=dict, required=True),
|
|
108
|
+
),
|
|
109
|
+
permission='dvm/delegate',
|
|
110
|
+
run='delegate_cmd',
|
|
111
|
+
reply='dict',
|
|
112
|
+
),
|
|
113
|
+
Command(
|
|
114
|
+
key='process',
|
|
115
|
+
name='Process DVM Request',
|
|
116
|
+
description='execute a NIP-90 request locally or via delegated runner',
|
|
117
|
+
tags=('dvm', 'process'),
|
|
118
|
+
parameters=(
|
|
119
|
+
Parameter(key='request', name='request', description='NIP-90 request event dictionary', type=dict, default=dict, required=True),
|
|
120
|
+
),
|
|
121
|
+
permission='dvm/process',
|
|
122
|
+
run='process_cmd',
|
|
123
|
+
reply='dict',
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Removes on_event from the node's face; set while started.
|
|
128
|
+
_face_unsubscribe: Optional[Callable[[], None]] = None
|
|
129
|
+
|
|
130
|
+
def start(self) -> None:
|
|
131
|
+
super().start()
|
|
132
|
+
add = getattr(self.face, 'add_event_handler', None)
|
|
133
|
+
if add is not None:
|
|
134
|
+
self._face_unsubscribe = add(self.on_event)
|
|
135
|
+
|
|
136
|
+
def stop(self) -> None:
|
|
137
|
+
super().stop()
|
|
138
|
+
if self._face_unsubscribe is not None:
|
|
139
|
+
self._face_unsubscribe()
|
|
140
|
+
self._face_unsubscribe = None
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
node: Optional[Any] = None,
|
|
145
|
+
cfg: Optional[Union[DVMConfig, Mapping[str, Any]]] = None,
|
|
146
|
+
custom_runner: Optional[Callable[[Mapping[str, Any]], bytes]] = None,
|
|
147
|
+
blob_store: Optional[Any] = None,
|
|
148
|
+
registry: Optional[compute.ExecutorRegistry] = None,
|
|
149
|
+
order_book: Optional[Any] = None,
|
|
150
|
+
bridge: Optional[Any] = None,
|
|
151
|
+
ks: Optional[Any] = None,
|
|
152
|
+
face: Optional[Any] = None,
|
|
153
|
+
clock: Optional[Callable[[], int]] = None,
|
|
154
|
+
payment_rail: Optional[Any] = None,
|
|
155
|
+
settlement: Optional[Any] = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
super().__init__(node, clock)
|
|
158
|
+
# The keystore results are signed with and the relay face they fan
|
|
159
|
+
# out to; named from ctx in a node, read off a fake node in tests.
|
|
160
|
+
self.ks: Optional[Any] = ks if ks is not None else getattr(node, 'ks', None)
|
|
161
|
+
self.face: Optional[Any] = face if face is not None else getattr(node, 'face', None)
|
|
162
|
+
# The Compute compartment (or anything with `command('ask', ...)` and
|
|
163
|
+
# `find_offers_for_ask`) that delegated requests are posted to.
|
|
164
|
+
self.order_book: Optional[Any] = order_book
|
|
165
|
+
# The relay bridge (anything with `enqueue_egress`) results fan out to.
|
|
166
|
+
self.bridge: Optional[Any] = bridge
|
|
167
|
+
# Payment rail wiring (Lightning hold invoices)
|
|
168
|
+
if payment_rail is not None:
|
|
169
|
+
self.payment_rail: Optional[Any] = payment_rail
|
|
170
|
+
elif settlement is not None:
|
|
171
|
+
try:
|
|
172
|
+
self.payment_rail = getattr(settlement, 'engine', settlement).get_rail('lightning')
|
|
173
|
+
except Exception:
|
|
174
|
+
self.payment_rail = None
|
|
175
|
+
elif node is not None and getattr(node, 'settle_engine', None) is not None:
|
|
176
|
+
try:
|
|
177
|
+
self.payment_rail = node.settle_engine.get_rail('lightning')
|
|
178
|
+
except Exception:
|
|
179
|
+
self.payment_rail = None
|
|
180
|
+
elif node is not None and getattr(node, 'settlement', None) is not None:
|
|
181
|
+
try:
|
|
182
|
+
self.payment_rail = getattr(node.settlement, 'engine', node.settlement).get_rail('lightning')
|
|
183
|
+
except Exception:
|
|
184
|
+
self.payment_rail = None
|
|
185
|
+
else:
|
|
186
|
+
self.payment_rail = None
|
|
187
|
+
# Tests hand a dict; the node hands the typed block. Either way the
|
|
188
|
+
# block is the shape, and the fields below read from it.
|
|
189
|
+
c = cfg if isinstance(cfg, DVMConfig) else DVMConfig.from_dict(cfg or {})
|
|
190
|
+
self.blob_store: Optional[Any] = blob_store if blob_store is not None else getattr(node, 'blobs', None)
|
|
191
|
+
# Which executors serve requests. Defaults to the process-wide
|
|
192
|
+
# registry (live drivers); tests pass one built with mock_mode=True
|
|
193
|
+
# so the LLM/agent/Bacalhau fixtures can answer without a backend.
|
|
194
|
+
self.registry: compute.ExecutorRegistry = registry or compute.DEFAULT_REGISTRY
|
|
195
|
+
self._face_unsubscribe = None
|
|
196
|
+
self.blossom_servers: list[str] = list(c.blossom_servers) # not a store: configured blossom servers from config
|
|
197
|
+
self.enabled: bool = c.enabled
|
|
198
|
+
self.supported_kinds: set[int] = set(c.supported_kinds) # not a store: configured supported kinds from config
|
|
199
|
+
self.rate_msat_per_mfuel: int = c.rate_msat_per_mfuel
|
|
200
|
+
self.max_fuel: int = c.max_fuel
|
|
201
|
+
self.delegate_to_mesh: bool = c.delegate_to_mesh
|
|
202
|
+
self.custom_runner: Optional[Callable[[Mapping[str, Any]], bytes]] = custom_runner
|
|
203
|
+
|
|
204
|
+
self.jobs_received: int = 0
|
|
205
|
+
self.jobs_succeeded: int = 0
|
|
206
|
+
self.jobs_failed: int = 0
|
|
207
|
+
self.total_fuel_used: int = 0
|
|
208
|
+
self.last_job_at: Optional[int] = None
|
|
209
|
+
|
|
210
|
+
self._lock: threading.Lock = threading.Lock()
|
|
211
|
+
self._request_queue: queue.Queue[NostrEvent] = queue.Queue(maxsize=128)
|
|
212
|
+
# No copy of the results is kept here. One was, keyed by request, for
|
|
213
|
+
# every job served and never read: the result is returned to the
|
|
214
|
+
# caller and fanned out to the face's subscribers and the bridge's
|
|
215
|
+
# relays (see _fanout), and that is what answers a client.
|
|
216
|
+
|
|
217
|
+
def state(self) -> DVMStateReport:
|
|
218
|
+
return {
|
|
219
|
+
'enabled': self.enabled,
|
|
220
|
+
'supported_kinds': sorted(list(self.supported_kinds)),
|
|
221
|
+
'rate_msat_per_mfuel': self.rate_msat_per_mfuel,
|
|
222
|
+
'delegate_to_mesh': self.delegate_to_mesh,
|
|
223
|
+
'jobs_received': self.jobs_received,
|
|
224
|
+
'jobs_succeeded': self.jobs_succeeded,
|
|
225
|
+
'jobs_failed': self.jobs_failed,
|
|
226
|
+
'total_fuel_used': self.total_fuel_used,
|
|
227
|
+
'last_job_at': self.last_job_at,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
def trouble(self) -> list[str]:
|
|
231
|
+
out: list[str] = []
|
|
232
|
+
recent_errs = self.refusals.within(1800, self.clock(), kind='dvm')
|
|
233
|
+
if len(recent_errs) > 10:
|
|
234
|
+
out.append(f'{len(recent_errs)} DVM job failure(s) in the last 30 minutes')
|
|
235
|
+
return out
|
|
236
|
+
|
|
237
|
+
def process_request(
|
|
238
|
+
self,
|
|
239
|
+
request_ev: NostrEvent,
|
|
240
|
+
signer_sk: Optional[str] = None,
|
|
241
|
+
) -> tuple[Optional[NostrEvent], NostrEvent]:
|
|
242
|
+
"""Process a NIP-90 request, returning (response_event_or_None, feedback_event)."""
|
|
243
|
+
sk = signer_sk or (self.ks.node_sk if self.ks is not None else None)
|
|
244
|
+
if not sk:
|
|
245
|
+
raise DVMError('no signer private key available for DVM gateway')
|
|
246
|
+
|
|
247
|
+
now = self.clock()
|
|
248
|
+
req_id = request_ev.get('id', '')
|
|
249
|
+
req_kind = request_ev.get('kind', 0)
|
|
250
|
+
|
|
251
|
+
with self._lock:
|
|
252
|
+
self.jobs_received += 1
|
|
253
|
+
self.last_job_at = now
|
|
254
|
+
self.say('job_received', job_id=req_id[:16], kind=req_kind)
|
|
255
|
+
|
|
256
|
+
if not is_dvm_request(request_ev) or req_kind not in self.supported_kinds:
|
|
257
|
+
fb_err = build_dvm_feedback(sk, request_ev, 'error', 'unsupported job kind', now=now)
|
|
258
|
+
self.refusals.refuse('dvm', 'unsupported kind', subject=req_id[:16], detail={'kind': req_kind})
|
|
259
|
+
with self._lock:
|
|
260
|
+
self.jobs_failed += 1
|
|
261
|
+
self.recent_jobs.append((req_id[:16], 'unsupported', req_kind))
|
|
262
|
+
self.say('job_failed', job_id=req_id[:16], error='unsupported job kind')
|
|
263
|
+
return None, fb_err
|
|
264
|
+
|
|
265
|
+
# Tell the requester we have started. This used to be built and then
|
|
266
|
+
# dropped on the floor; NIP-90 clients rely on the 'processing'
|
|
267
|
+
# status to distinguish a slow job from an ignored one, so it goes
|
|
268
|
+
# out through the same fan-out the final events use.
|
|
269
|
+
fb_proc = build_dvm_feedback(sk, request_ev, 'processing', now=now)
|
|
270
|
+
self._fanout(fb_proc)
|
|
271
|
+
|
|
272
|
+
# Check if client provided an escrow / hold invoice payment hash tag
|
|
273
|
+
client_pay_hash = None
|
|
274
|
+
for tag in request_ev.get('tags', []):
|
|
275
|
+
if len(tag) >= 2 and tag[0] in ('payment_hash', 'hold_invoice', 'escrow'):
|
|
276
|
+
client_pay_hash = tag[1]
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
if client_pay_hash and self.payment_rail is not None:
|
|
280
|
+
st = self.payment_rail.get_status(client_pay_hash)
|
|
281
|
+
if getattr(st, 'status', '') == 'CREATED':
|
|
282
|
+
self.payment_rail.hold_payment(client_pay_hash)
|
|
283
|
+
|
|
284
|
+
# Map to mesh job
|
|
285
|
+
parsed = parse_dvm_request(request_ev)
|
|
286
|
+
job_payload = dvm_request_to_mesh_job(request_ev, fuel_max=self.max_fuel)
|
|
287
|
+
|
|
288
|
+
if self.blob_store is not None:
|
|
289
|
+
job_payload['blob_store'] = self.blob_store
|
|
290
|
+
if self.blossom_servers:
|
|
291
|
+
job_payload.setdefault('blossom_servers', self.blossom_servers)
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
if (self.custom_runner is None
|
|
295
|
+
and isinstance(self.registry.select_for_job(job_payload), compute.WasmExecutor)
|
|
296
|
+
and not _module_sha(job_payload)):
|
|
297
|
+
raise DVMError("WASM job names no module: add ['param', 'module', <sha256>]")
|
|
298
|
+
exec_res = compute.run_job(job_payload, custom_runner=self.custom_runner, registry=self.registry)
|
|
299
|
+
fuel = exec_res['fuel_used']
|
|
300
|
+
raw_out = exec_res['raw_output']
|
|
301
|
+
out_str = raw_out.decode('utf-8', errors='replace') if isinstance(raw_out, bytes) else str(raw_out)
|
|
302
|
+
|
|
303
|
+
# Calculate price - support zero-cost / free compute jobs
|
|
304
|
+
if self.rate_msat_per_mfuel == 0 or parsed.get('bid_msat', 0) == 0:
|
|
305
|
+
msat = 0
|
|
306
|
+
else:
|
|
307
|
+
msat = max(1, (fuel * self.rate_msat_per_mfuel) // 1000000)
|
|
308
|
+
|
|
309
|
+
commit = exec_res['result_hash']
|
|
310
|
+
|
|
311
|
+
extra_tags: list[list[str]] = []
|
|
312
|
+
counters = exec_res.get('counters') or {}
|
|
313
|
+
if 'total_tokens' in counters:
|
|
314
|
+
extra_tags.append(['tokens', str(counters['total_tokens'])])
|
|
315
|
+
if 'prompt_tokens' in counters:
|
|
316
|
+
extra_tags.append(['prompt_tokens', str(counters['prompt_tokens'])])
|
|
317
|
+
if 'completion_tokens' in counters:
|
|
318
|
+
extra_tags.append(['completion_tokens', str(counters['completion_tokens'])])
|
|
319
|
+
if 'model' in job_payload:
|
|
320
|
+
extra_tags.append(['model', str(job_payload['model'])])
|
|
321
|
+
if 'executor' in job_payload:
|
|
322
|
+
extra_tags.append(['executor', str(job_payload['executor'])])
|
|
323
|
+
|
|
324
|
+
# Live Lightning HTLC hold-invoice settlement:
|
|
325
|
+
if msat > 0 and self.payment_rail is not None:
|
|
326
|
+
preimage_bytes = os.urandom(32)
|
|
327
|
+
preimage = preimage_bytes.hex()
|
|
328
|
+
pay_hash = client_pay_hash or hashlib.sha256(preimage_bytes).hexdigest()
|
|
329
|
+
|
|
330
|
+
inv_status = self.payment_rail.get_status(pay_hash)
|
|
331
|
+
if getattr(inv_status, 'status', 'UNKNOWN') == 'UNKNOWN':
|
|
332
|
+
inv = self.payment_rail.create_hold_invoice(
|
|
333
|
+
msat,
|
|
334
|
+
pay_hash,
|
|
335
|
+
description=f"DVM job {req_id[:16]}",
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
inv = inv_status
|
|
339
|
+
|
|
340
|
+
if getattr(inv, 'status', '') == 'CREATED':
|
|
341
|
+
self.payment_rail.hold_payment(pay_hash)
|
|
342
|
+
|
|
343
|
+
if hashlib.sha256(bytes.fromhex(preimage)).hexdigest().lower() == pay_hash.lower():
|
|
344
|
+
self.payment_rail.settle_payment(pay_hash, preimage)
|
|
345
|
+
extra_tags.append(['preimage', preimage])
|
|
346
|
+
|
|
347
|
+
bolt11_val = getattr(inv, 'bolt11', None)
|
|
348
|
+
if bolt11_val:
|
|
349
|
+
extra_tags.append(['bolt11', bolt11_val])
|
|
350
|
+
extra_tags.append(['payment_hash', pay_hash])
|
|
351
|
+
|
|
352
|
+
resp_ev = build_dvm_response(
|
|
353
|
+
sk, request_ev, out_str, amount_msat=msat, commit=commit, extra_tags=extra_tags, now=now
|
|
354
|
+
)
|
|
355
|
+
fb_succ = build_dvm_feedback(sk, request_ev, 'success', now=now)
|
|
356
|
+
|
|
357
|
+
with self._lock:
|
|
358
|
+
self.jobs_succeeded += 1
|
|
359
|
+
self.total_fuel_used += fuel
|
|
360
|
+
requester_pk = str(request_ev.get('pubkey', ''))
|
|
361
|
+
self.recent_jobs.append((req_id[:16], 'success', requester_pk, fuel))
|
|
362
|
+
self.say('job_succeeded', job_id=req_id[:16], peer=requester_pk, fuel=fuel)
|
|
363
|
+
|
|
364
|
+
# Fan out to local face and bridge if wired
|
|
365
|
+
self._fanout(resp_ev)
|
|
366
|
+
self._fanout(fb_succ)
|
|
367
|
+
|
|
368
|
+
return resp_ev, fb_succ
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
if client_pay_hash and self.payment_rail is not None:
|
|
372
|
+
try:
|
|
373
|
+
st = self.payment_rail.get_status(client_pay_hash)
|
|
374
|
+
if getattr(st, 'status', '') == 'HELD':
|
|
375
|
+
self.payment_rail.cancel_payment(client_pay_hash, reason=str(e)[:40])
|
|
376
|
+
except Exception:
|
|
377
|
+
pass
|
|
378
|
+
with self._lock:
|
|
379
|
+
self.jobs_failed += 1
|
|
380
|
+
self.recent_jobs.append((req_id[:16], 'failed', str(e)[:40]))
|
|
381
|
+
self.say('job_failed', job_id=req_id[:16], error=str(e)[:40])
|
|
382
|
+
self.refusals.refuse('dvm', 'job execution failed', subject=req_id[:16], detail={'error': str(e)})
|
|
383
|
+
fb_fail = build_dvm_feedback(sk, request_ev, 'error', str(e), now=now)
|
|
384
|
+
return None, fb_fail
|
|
385
|
+
|
|
386
|
+
def create_payment_required_feedback(
|
|
387
|
+
self,
|
|
388
|
+
request_ev: NostrEvent,
|
|
389
|
+
amount_msat: int,
|
|
390
|
+
signer_sk: Optional[str] = None,
|
|
391
|
+
payment_hash: Optional[str] = None,
|
|
392
|
+
preimage: Optional[str] = None,
|
|
393
|
+
) -> tuple[NostrEvent, Optional[Any], str]:
|
|
394
|
+
"""Build and fan out a Kind 7000 payment-required feedback event with a Lightning hold invoice."""
|
|
395
|
+
sk = signer_sk or (self.ks.node_sk if self.ks is not None else None)
|
|
396
|
+
if not sk:
|
|
397
|
+
raise DVMError('no signer private key available for DVM gateway')
|
|
398
|
+
now = self.clock()
|
|
399
|
+
req_id = str(request_ev.get('id', ''))
|
|
400
|
+
|
|
401
|
+
if not preimage:
|
|
402
|
+
preimage = os.urandom(32).hex()
|
|
403
|
+
if not payment_hash:
|
|
404
|
+
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
|
405
|
+
|
|
406
|
+
inv = None
|
|
407
|
+
bolt11 = ''
|
|
408
|
+
if self.payment_rail is not None:
|
|
409
|
+
inv = self.payment_rail.create_hold_invoice(
|
|
410
|
+
amount_msat,
|
|
411
|
+
payment_hash,
|
|
412
|
+
description=f"DVM job {req_id[:16]}",
|
|
413
|
+
)
|
|
414
|
+
bolt11 = getattr(inv, 'bolt11', '') or ''
|
|
415
|
+
else:
|
|
416
|
+
bolt11 = f"lnbc{amount_msat}u1p{payment_hash[:32]}"
|
|
417
|
+
|
|
418
|
+
extra_tags: list[list[str]] = [
|
|
419
|
+
['amount', str(amount_msat), bolt11] if bolt11 else ['amount', str(amount_msat)],
|
|
420
|
+
]
|
|
421
|
+
if bolt11:
|
|
422
|
+
extra_tags.append(['bolt11', bolt11])
|
|
423
|
+
extra_tags.append(['payment_hash', payment_hash])
|
|
424
|
+
|
|
425
|
+
fb_ev = build_dvm_feedback(
|
|
426
|
+
sk,
|
|
427
|
+
request_ev,
|
|
428
|
+
'payment-required',
|
|
429
|
+
extra=f"{amount_msat} msat required",
|
|
430
|
+
extra_tags=extra_tags,
|
|
431
|
+
now=now,
|
|
432
|
+
)
|
|
433
|
+
self._fanout(fb_ev)
|
|
434
|
+
return fb_ev, inv, preimage
|
|
435
|
+
|
|
436
|
+
def _fanout(self, ev: NostrEvent) -> None:
|
|
437
|
+
"""Deliver a DVM event to the local relay face and the relay bridge, if wired."""
|
|
438
|
+
if self.face is not None:
|
|
439
|
+
self.face._fanout(ev)
|
|
440
|
+
if self.bridge is not None:
|
|
441
|
+
self.bridge.enqueue_egress(ev)
|
|
442
|
+
|
|
443
|
+
def delegate_request(
|
|
444
|
+
self,
|
|
445
|
+
request_ev: NostrEvent,
|
|
446
|
+
) -> DVMDelegationResult:
|
|
447
|
+
"""Delegate a NIP-90 request to the mesh compute order book."""
|
|
448
|
+
book = self.order_book
|
|
449
|
+
if book is None:
|
|
450
|
+
raise DVMError('compute subsystem not available for order book delegation')
|
|
451
|
+
parsed = parse_dvm_request(request_ev)
|
|
452
|
+
job_payload = dvm_request_to_mesh_job(request_ev, fuel_max=self.max_fuel)
|
|
453
|
+
ask_res = book.command('ask', {
|
|
454
|
+
'id': parsed['id'],
|
|
455
|
+
'job': job_payload,
|
|
456
|
+
'bid': parsed['bid_msat'],
|
|
457
|
+
'executor': job_payload.get('executor', 'wasm'),
|
|
458
|
+
'verify': _verify_mode(job_payload),
|
|
459
|
+
})
|
|
460
|
+
matches = book.find_offers_for_ask(parsed['id'])
|
|
461
|
+
self.say('job_delegated', job_id=parsed['id'])
|
|
462
|
+
return {
|
|
463
|
+
'ask_id': parsed['id'],
|
|
464
|
+
'ask_res': ask_res,
|
|
465
|
+
'matches_found': len(matches),
|
|
466
|
+
'candidates': matches,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
def on_event(self, ev: NostrEvent) -> None:
|
|
470
|
+
"""Callback to handle events observed on the local face or relay bridge."""
|
|
471
|
+
if not self.enabled:
|
|
472
|
+
return
|
|
473
|
+
if is_dvm_request(ev) and ev.get('kind') in self.supported_kinds:
|
|
474
|
+
try:
|
|
475
|
+
self._request_queue.put_nowait(ev)
|
|
476
|
+
except queue.Full:
|
|
477
|
+
self.refusals.refuse('dvm', 'request queue full; dropping event', subject=str(ev.get('id', ''))[:16])
|
|
478
|
+
|
|
479
|
+
def _dvm_worker_loop(self, stop_event: threading.Event) -> None:
|
|
480
|
+
while not stop_event.is_set():
|
|
481
|
+
try:
|
|
482
|
+
ev = self._request_queue.get(timeout=0.2)
|
|
483
|
+
except queue.Empty:
|
|
484
|
+
continue
|
|
485
|
+
try:
|
|
486
|
+
self.process_request(ev)
|
|
487
|
+
except Exception as e:
|
|
488
|
+
self.refusals.refuse('dvm', f'worker error processing request: {e}', subject=str(ev.get('id', ''))[:16])
|
|
489
|
+
finally:
|
|
490
|
+
self._request_queue.task_done()
|
|
491
|
+
|
|
492
|
+
@staticmethod
|
|
493
|
+
def _request_event(args: Mapping[str, Any]) -> NostrEvent:
|
|
494
|
+
"""The 'request' argument as a NostrEvent, or a DVMError.
|
|
495
|
+
|
|
496
|
+
Control-plane callers hand us untyped JSON; every signed-event field
|
|
497
|
+
must be present before it is treated as an event, otherwise a missing
|
|
498
|
+
'id' or 'pubkey' would surface later as a KeyError deep in processing.
|
|
499
|
+
"""
|
|
500
|
+
ev = args.get('request')
|
|
501
|
+
if not isinstance(ev, dict):
|
|
502
|
+
raise DVMError('request must be an event dictionary')
|
|
503
|
+
missing = [k for k in ('id', 'pubkey', 'created_at', 'kind', 'tags', 'content', 'sig') if k not in ev]
|
|
504
|
+
if missing:
|
|
505
|
+
raise DVMError('request event is missing fields: %s' % ', '.join(missing))
|
|
506
|
+
event: NostrEvent = {
|
|
507
|
+
'id': str(ev['id']),
|
|
508
|
+
'pubkey': str(ev['pubkey']),
|
|
509
|
+
'created_at': int(ev['created_at']),
|
|
510
|
+
'kind': int(ev['kind']),
|
|
511
|
+
'tags': list(ev['tags']),
|
|
512
|
+
'content': str(ev['content']),
|
|
513
|
+
'sig': str(ev['sig']),
|
|
514
|
+
}
|
|
515
|
+
return event
|
|
516
|
+
|
|
517
|
+
def delegate_cmd(self, args: Mapping[str, Any]) -> Any:
|
|
518
|
+
return self.delegate_request(self._request_event(args))
|
|
519
|
+
|
|
520
|
+
def process_cmd(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
521
|
+
resp, fb = self.process_request(self._request_event(args))
|
|
522
|
+
return {
|
|
523
|
+
'response': resp,
|
|
524
|
+
'feedback': fb,
|
|
525
|
+
'success': resp is not None,
|
|
526
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""NIP-90 kinds, parsing and the events a gateway emits.
|
|
2
|
+
|
|
3
|
+
The bytes live in fleetmesh_core/dvm.py (core) and are pinned by vectors/nip90.json;
|
|
4
|
+
this module keeps the gateway's names and defaults the clock, which the
|
|
5
|
+
reference deliberately takes as an argument.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
11
|
+
from fleetmesh_core.services import dvm as _ref # noqa: E402
|
|
12
|
+
from fleetmesh_core.services.dvm import (DVMError, FEEDBACK_KIND, ParsedRequest, is_request as is_dvm_request, # noqa: E402
|
|
13
|
+
is_result as is_dvm_response, parse_request as parse_dvm_request, result_kind as dvm_response_kind)
|
|
14
|
+
from fleetmesh_core.wire.interfaces import NostrEvent # noqa: E402
|
|
15
|
+
from node.config import DEFAULT_DVM_KINDS # noqa: E402
|
|
16
|
+
|
|
17
|
+
DEFAULT_SUPPORTED_KINDS: list[int] = list(DEFAULT_DVM_KINDS) # the block owns the list
|
|
18
|
+
|
|
19
|
+
__all__ = ['DVMError', 'FEEDBACK_KIND', 'ParsedRequest', 'is_dvm_request', 'is_dvm_response', 'parse_dvm_request',
|
|
20
|
+
'dvm_response_kind', 'DEFAULT_SUPPORTED_KINDS', 'build_dvm_feedback', 'build_dvm_response']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_dvm_feedback(signer_sk: str, request_ev: Mapping[str, Any], status: str, extra: str = '',
|
|
24
|
+
extra_tags: Optional[Sequence[Sequence[str]]] = None,
|
|
25
|
+
now: Optional[int] = None) -> NostrEvent:
|
|
26
|
+
"""Build a signed Kind 7000 DVM feedback/status event."""
|
|
27
|
+
return _ref.feedback(signer_sk, request_ev, status, int(now if now is not None else time.time()), extra, extra_tags=extra_tags)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_dvm_response(signer_sk: str, request_ev: Mapping[str, Any], result_content: str, amount_msat: int = 0,
|
|
31
|
+
commit: str = '', extra_tags: Optional[Sequence[Sequence[str]]] = None,
|
|
32
|
+
now: Optional[int] = None) -> NostrEvent:
|
|
33
|
+
"""Build a signed Kind 6000..6999 DVM job response event."""
|
|
34
|
+
return _ref.result(signer_sk, request_ev, result_content, int(now if now is not None else time.time()),
|
|
35
|
+
amount_msat, commit, extra_tags)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""A NIP-90 request becomes a fleetmesh job - the mapping is fleetmesh_core/dvm.py's,
|
|
2
|
+
pinned by vectors/nip90.json. Re-exported under the gateway's names."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
from fleetmesh_core.services.dvm import (agent_job, bacalhau_job, is_agent, is_bacalhau, is_llm, is_media, is_zk, llm_job, media_job, wasm_job, zk_job, # noqa: E402,F401
|
|
7
|
+
request_to_job, Job, DEFAULT_FUEL, TRANSCODE_KIND)
|
|
8
|
+
|
|
9
|
+
__all__ = ['agent_job', 'bacalhau_job', 'is_agent', 'is_bacalhau', 'is_llm', 'is_media', 'is_zk', 'llm_job', 'media_job', 'wasm_job', 'zk_job',
|
|
10
|
+
'request_to_job', 'Job', 'DEFAULT_FUEL', 'TRANSCODE_KIND', 'dvm_request_to_mesh_job']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def dvm_request_to_mesh_job(request_ev: Mapping[str, Any], executor: str = 'wasm', fuel_max: int = DEFAULT_FUEL) -> Job:
|
|
14
|
+
"""Map a NIP-90 request into a deterministic fleetmesh job payload."""
|
|
15
|
+
return request_to_job(request_ev, executor, fuel_max)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Types and records for the DVM (Data Vending Machine) compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DVMStateReport(TypedDict):
|
|
8
|
+
"""Telemetry report of DVM gateway operations and fuel consumption."""
|
|
9
|
+
enabled: bool
|
|
10
|
+
supported_kinds: list[int]
|
|
11
|
+
rate_msat_per_mfuel: int
|
|
12
|
+
delegate_to_mesh: bool
|
|
13
|
+
jobs_received: int
|
|
14
|
+
jobs_succeeded: int
|
|
15
|
+
jobs_failed: int
|
|
16
|
+
total_fuel_used: int
|
|
17
|
+
last_job_at: Optional[float]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DVMDelegationResult(TypedDict):
|
|
21
|
+
"""Result of delegating a DVM job into the peer-to-peer compute mesh."""
|
|
22
|
+
ask_id: str
|
|
23
|
+
ask_res: Any
|
|
24
|
+
matches_found: int
|
|
25
|
+
candidates: list[Any]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Cross-protocol gateway services for Fleetmesh.
|
|
2
|
+
|
|
3
|
+
Provides bidirectional translation, identity bridging, unified settlement,
|
|
4
|
+
and multi-protocol federation across Nostr, ATProto, ActivityPub, SSB, p2panda, and Rhio.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from node.planes.services.gateway.translate import (
|
|
9
|
+
ProtocolTranslator,
|
|
10
|
+
UnifiedPost,
|
|
11
|
+
activitypub_to_unified,
|
|
12
|
+
atproto_to_unified,
|
|
13
|
+
nostr_to_unified,
|
|
14
|
+
p2panda_to_unified,
|
|
15
|
+
rhio_to_unified,
|
|
16
|
+
ssb_to_unified,
|
|
17
|
+
unified_to_activitypub,
|
|
18
|
+
unified_to_atproto,
|
|
19
|
+
unified_to_nostr,
|
|
20
|
+
unified_to_p2panda,
|
|
21
|
+
unified_to_rhio,
|
|
22
|
+
unified_to_ssb,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
'ProtocolTranslator',
|
|
27
|
+
'UnifiedPost',
|
|
28
|
+
'activitypub_to_unified',
|
|
29
|
+
'atproto_to_unified',
|
|
30
|
+
'nostr_to_unified',
|
|
31
|
+
'p2panda_to_unified',
|
|
32
|
+
'rhio_to_unified',
|
|
33
|
+
'ssb_to_unified',
|
|
34
|
+
'unified_to_activitypub',
|
|
35
|
+
'unified_to_atproto',
|
|
36
|
+
'unified_to_nostr',
|
|
37
|
+
'unified_to_p2panda',
|
|
38
|
+
'unified_to_rhio',
|
|
39
|
+
'unified_to_ssb',
|
|
40
|
+
]
|