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,20 @@
|
|
|
1
|
+
"""Types and records for the Mediation compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DeliveredMessage(TypedDict):
|
|
8
|
+
"""Message unwrapped and delivered via mediation pickup."""
|
|
9
|
+
id: str
|
|
10
|
+
recipient_key: str
|
|
11
|
+
sender: str
|
|
12
|
+
attachments: list[Any]
|
|
13
|
+
enqueued_at: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MediationGrantInfo(TypedDict):
|
|
17
|
+
"""Information regarding a granted mediation contract."""
|
|
18
|
+
routing_keys: list[str]
|
|
19
|
+
endpoint: Optional[str]
|
|
20
|
+
granted_at: int
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Direct libp2p P2P transport, reached through a sidecar, as a compartment.
|
|
2
|
+
|
|
3
|
+
Protocol: `/fleetmesh/didcomm/1.0.0` as declared in `schema/messages.json`.
|
|
4
|
+
One stream carries one JWE; the other side answers one ack line once it has
|
|
5
|
+
queued the message for its node. The libp2p process is `sidecar/fleetmesh_p2p.py`,
|
|
6
|
+
talked to over `p2p.sock` in the node's directory, and it holds the Ed25519
|
|
7
|
+
transport key: its PeerId *is* the `key transport` Multikey the descriptor
|
|
8
|
+
publishes, which is how a dial is bound to a descriptor and not to whatever
|
|
9
|
+
multiaddr the descriptor happened to carry (see `target_of`).
|
|
10
|
+
|
|
11
|
+
What this compartment does with a stream it drained is what the FIPS path
|
|
12
|
+
does with a datagram: hands the bytes to the router's one envelope-opening
|
|
13
|
+
rule (`didcomm_inbound.open_inbound`). The sidecar's word on who wrote a
|
|
14
|
+
stream is a hint the router checks against the sender's descriptor, never a
|
|
15
|
+
proof of who sent the message; that proof is the ECDH-1PU agreement or nothing.
|
|
16
|
+
|
|
17
|
+
Like `node/announce.py`, if the sidecar is absent the node operates over the
|
|
18
|
+
relays, which is the rung the specification says always works.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import tempfile
|
|
26
|
+
from typing import Any, Callable, Mapping, Optional
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
from node.compartment import stores as st
|
|
30
|
+
from node.compartment import Command, Need
|
|
31
|
+
from node.config import P2PConfig
|
|
32
|
+
from node.config.parameter import Parameter
|
|
33
|
+
from node.compartment.lifecycle import Lifecycle
|
|
34
|
+
from node.compartment.sidecar import Bridge, Sidecar, SidecarError, jsonline_call
|
|
35
|
+
from fleetmesh_core.crypto import keys
|
|
36
|
+
from fleetmesh_core.identity.did import b58encode as _b58 # base58btc, written once (audit D9)
|
|
37
|
+
|
|
38
|
+
SOCK: str = 'p2p.sock'
|
|
39
|
+
PROTOCOL: str = 'fleetmesh-p2p/1'
|
|
40
|
+
STREAM_PROTOCOL: str = '/fleetmesh/didcomm/1.0.0'
|
|
41
|
+
SCHEME: str = 'libp2p'
|
|
42
|
+
MAX_LINE: int = 1 << 20
|
|
43
|
+
DRAIN_MAX: int = 64
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def socket_path(directory: str) -> str:
|
|
48
|
+
"""Derive the unix socket path for the p2p sidecar."""
|
|
49
|
+
real = os.path.realpath(directory)
|
|
50
|
+
p = os.path.join(real, SOCK)
|
|
51
|
+
if len(p.encode('utf-8')) < 100:
|
|
52
|
+
return p
|
|
53
|
+
return os.path.join(
|
|
54
|
+
tempfile.gettempdir(),
|
|
55
|
+
'fleetmesh-p2p-%s.sock' % hashlib.sha256(real.encode('utf-8')).hexdigest()[:16]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def peer_id_of(raw_ed25519: bytes) -> str:
|
|
60
|
+
"""The libp2p PeerId of an Ed25519 public key: the key itself, inlined.
|
|
61
|
+
|
|
62
|
+
A 32-byte key fits under the identity multihash, so the id is the
|
|
63
|
+
protobuf `PublicKey{Type: Ed25519, Data: key}` wrapped as
|
|
64
|
+
`multihash(0x00, len, pb)` and base58btc-encoded - the `12D3KooW...`
|
|
65
|
+
form. No hashing, so a descriptor's `key transport` Multikey and a
|
|
66
|
+
stream's remote PeerId are the same 32 bytes spelled two ways.
|
|
67
|
+
"""
|
|
68
|
+
if len(raw_ed25519) != 32:
|
|
69
|
+
raise ValueError('an Ed25519 public key is 32 bytes')
|
|
70
|
+
pb = b'\x08\x01\x12\x20' + raw_ed25519
|
|
71
|
+
return _b58(b'\x00' + bytes([len(pb)]) + pb)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def transport_key_of(desc: Optional[Mapping[str, Any]]) -> Optional[bytes]:
|
|
75
|
+
"""The raw Ed25519 transport key a descriptor publishes, or None."""
|
|
76
|
+
if desc is None:
|
|
77
|
+
return None
|
|
78
|
+
for t in desc.get('tags', []):
|
|
79
|
+
if len(t) > 2 and t[0] == 'key' and t[1] == 'transport':
|
|
80
|
+
try:
|
|
81
|
+
kind, raw = keys.parse_multikey(t[2])
|
|
82
|
+
except ValueError:
|
|
83
|
+
return None
|
|
84
|
+
return raw if kind == 'transport' else None
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def target_of(desc: Optional[Mapping[str, Any]]) -> tuple[Optional[str], Optional[str]]:
|
|
89
|
+
"""The multiaddr to dial for a descriptor, or why there is none.
|
|
90
|
+
|
|
91
|
+
Returns (target, None) or (None, reason). The target always ends in
|
|
92
|
+
`/p2p/<PeerId>` derived from the descriptor's own `key transport`: the
|
|
93
|
+
libp2p handshake then refuses any host at that address that does not
|
|
94
|
+
hold the key the descriptor signed over. A descriptor whose endpoint
|
|
95
|
+
already names a PeerId must name the same one; a different one is a
|
|
96
|
+
descriptor that contradicts itself, and is not dialled.
|
|
97
|
+
"""
|
|
98
|
+
if desc is None:
|
|
99
|
+
return None, 'no descriptor'
|
|
100
|
+
addr = None
|
|
101
|
+
for t in desc.get('tags', []):
|
|
102
|
+
if len(t) > 2 and t[0] == 'endpoint' and t[1] == SCHEME:
|
|
103
|
+
addr = t[2]
|
|
104
|
+
break
|
|
105
|
+
if not addr:
|
|
106
|
+
return None, 'no libp2p endpoint'
|
|
107
|
+
raw = transport_key_of(desc)
|
|
108
|
+
if raw is None:
|
|
109
|
+
return None, 'no transport key'
|
|
110
|
+
pid = peer_id_of(raw)
|
|
111
|
+
if '/p2p/' in addr:
|
|
112
|
+
head, _, named = addr.rpartition('/p2p/')
|
|
113
|
+
if named != pid:
|
|
114
|
+
return None, 'endpoint names a PeerId that is not the transport key'
|
|
115
|
+
addr = head
|
|
116
|
+
return '%s/p2p/%s' % (addr.rstrip('/'), pid), None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class P2PError(SidecarError):
|
|
120
|
+
"""Raised when communication with the p2p sidecar fails."""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def ask_sidecar(directory: str, cmd: str, timeout: float = 5.0, **args: Any) -> Any:
|
|
124
|
+
"""Send one request to the p2p sidecar over its unix domain socket."""
|
|
125
|
+
path = socket_path(directory)
|
|
126
|
+
try:
|
|
127
|
+
result, _ = jsonline_call(path, cmd, timeout=timeout, **args)
|
|
128
|
+
return result
|
|
129
|
+
except SidecarError as e:
|
|
130
|
+
raise P2PError(str(e)) from e
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
from .types import P2PState, SendResult
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class P2PTransport(Bridge):
|
|
137
|
+
|
|
138
|
+
optional: bool = True
|
|
139
|
+
enabled_by: Optional[str] = 'p2p'
|
|
140
|
+
"""The direct libp2p transport compartment."""
|
|
141
|
+
|
|
142
|
+
name: str = 'p2p'
|
|
143
|
+
reads: tuple[str, ...] = ('p2p', 'endpoints')
|
|
144
|
+
config_cls = P2PConfig
|
|
145
|
+
|
|
146
|
+
SIDECAR = Sidecar(
|
|
147
|
+
name='p2p',
|
|
148
|
+
protocol=PROTOCOL,
|
|
149
|
+
upstream='py-libp2p stream transport on %s' % STREAM_PROTOCOL,
|
|
150
|
+
standalone='fleetmesh-p2p NODE_DIR',
|
|
151
|
+
probe='status',
|
|
152
|
+
)
|
|
153
|
+
CONSEQUENCE: str = 'direct libp2p connections are unavailable; falling back to relays'
|
|
154
|
+
LIFECYCLE = Lifecycle(restart=True)
|
|
155
|
+
|
|
156
|
+
# Declared by the bridge from SIDECAR (p2p_sidecar_reachable / _unreachable / _incompatible).
|
|
157
|
+
SIDECAR_REACHABLE: str = 'p2p.sidecar.reachable'
|
|
158
|
+
SIDECAR_UNREACHABLE: str = 'p2p.sidecar.unreachable'
|
|
159
|
+
SIDECAR_INCOMPATIBLE: str = 'p2p.sidecar.incompatible'
|
|
160
|
+
|
|
161
|
+
NEEDS = (
|
|
162
|
+
Need('directory', 'dir'),
|
|
163
|
+
Need('peers', 'peers'),
|
|
164
|
+
Need('transport', 'transport'),
|
|
165
|
+
Need('ks', 'ks'),
|
|
166
|
+
)
|
|
167
|
+
STORES = (
|
|
168
|
+
st.Store(key='egress_queue', description='outbound frames queued for libp2p sidecar',
|
|
169
|
+
kind=st.RING, shape=bytes, bound=1024),
|
|
170
|
+
)
|
|
171
|
+
COMMANDS = (
|
|
172
|
+
Command(
|
|
173
|
+
key='status',
|
|
174
|
+
name='P2P Status',
|
|
175
|
+
description='display libp2p sidecar connectivity and stream counters',
|
|
176
|
+
tags=('p2p', 'status'),
|
|
177
|
+
permission='p2p/status',
|
|
178
|
+
run='state',
|
|
179
|
+
reply='dict',
|
|
180
|
+
),
|
|
181
|
+
Command(
|
|
182
|
+
key='drain',
|
|
183
|
+
name='Drain P2P Streams',
|
|
184
|
+
description='drain inbound direct P2P messages from the sidecar',
|
|
185
|
+
tags=('p2p', 'drain'),
|
|
186
|
+
permission='p2p/drain',
|
|
187
|
+
run='drain_cmd',
|
|
188
|
+
reply='dict',
|
|
189
|
+
),
|
|
190
|
+
Command(
|
|
191
|
+
key='send',
|
|
192
|
+
name='Send P2P Message',
|
|
193
|
+
description='dispatch one already-sealed JWE over a libp2p stream',
|
|
194
|
+
tags=('p2p', 'send'),
|
|
195
|
+
parameters=(
|
|
196
|
+
Parameter(key='target', name='target', description='target multiaddr', type=str, default='', required=True),
|
|
197
|
+
Parameter(key='payload', name='payload', description='sealed JWE payload', type=dict, default=dict, required=True),
|
|
198
|
+
),
|
|
199
|
+
permission='p2p/send',
|
|
200
|
+
run='send_cmd',
|
|
201
|
+
reply='dict',
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def __init__(self, directory: str, peers: Any = None, transport: Any = None, clock: Optional[Callable[[], int]] = None, node: Any = None,
|
|
206
|
+
ks: Any = None) -> None:
|
|
207
|
+
super().__init__(node, clock)
|
|
208
|
+
# The keystore whose transport key the sidecar's peer id is checked against.
|
|
209
|
+
self.ks: Any = ks if ks is not None else getattr(node, 'ks', None)
|
|
210
|
+
self.dir: str = directory
|
|
211
|
+
self.peers: Any = peers
|
|
212
|
+
self.transport: Any = transport
|
|
213
|
+
self.connected: bool = False
|
|
214
|
+
self.sent_count: int = 0
|
|
215
|
+
self.recv_count: int = 0
|
|
216
|
+
self.refused_count: int = 0
|
|
217
|
+
self.last_drain_at: Optional[int] = None
|
|
218
|
+
self.peer_id: Optional[str] = None
|
|
219
|
+
self.addrs: list[str] = [] # not a store: the local node's p2p multiaddrs from sidecar
|
|
220
|
+
self._said_missing: bool = False
|
|
221
|
+
if transport is not None and hasattr(transport, 'p2p'):
|
|
222
|
+
transport.p2p = self
|
|
223
|
+
|
|
224
|
+
# ---- the bridge -----------------------------------------------------------
|
|
225
|
+
def endpoint(self) -> str:
|
|
226
|
+
return socket_path(self.dir)
|
|
227
|
+
|
|
228
|
+
def wanted(self) -> bool:
|
|
229
|
+
return bool(self.config('p2p', False))
|
|
230
|
+
|
|
231
|
+
def forget(self) -> None:
|
|
232
|
+
self.connected = False
|
|
233
|
+
self.peer_id = None
|
|
234
|
+
self.addrs = [] # not a store: cleared on forget
|
|
235
|
+
|
|
236
|
+
def state(self) -> P2PState:
|
|
237
|
+
st: P2PState = {
|
|
238
|
+
'enabled': bool(self.config('p2p', True)),
|
|
239
|
+
'socket': self.endpoint(),
|
|
240
|
+
'connected': self.connected,
|
|
241
|
+
'peer_id': self.peer_id,
|
|
242
|
+
'addrs': self.addrs,
|
|
243
|
+
'sent_count': self.sent_count,
|
|
244
|
+
'recv_count': self.recv_count,
|
|
245
|
+
'refused_count': self.refused_count,
|
|
246
|
+
'last_drain_at': self.last_drain_at,
|
|
247
|
+
}
|
|
248
|
+
if self.connected:
|
|
249
|
+
try:
|
|
250
|
+
st['sidecar'] = self.call('status', timeout=1.0)
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
return st
|
|
254
|
+
|
|
255
|
+
def sections(self) -> Mapping[str, Any]:
|
|
256
|
+
return {
|
|
257
|
+
'bridge': self.bridge_state(),
|
|
258
|
+
'p2p': self.state(),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
def trouble(self) -> list[str]:
|
|
262
|
+
out = list(super().trouble())
|
|
263
|
+
if self.connected and self.peer_id and self.ks is not None:
|
|
264
|
+
mine = peer_id_of(keys.raw_public(self.ks.transport_pub))
|
|
265
|
+
if mine != self.peer_id:
|
|
266
|
+
out.append('p2p sidecar holds a different transport key (%s) than this node publishes (%s)'
|
|
267
|
+
% (self.peer_id[:16], mine[:16]))
|
|
268
|
+
return out
|
|
269
|
+
|
|
270
|
+
def tick(self, now: int) -> list[tuple[Any, ...]]:
|
|
271
|
+
did: list[tuple[Any, ...]] = []
|
|
272
|
+
if not self.config('p2p', True):
|
|
273
|
+
return did
|
|
274
|
+
|
|
275
|
+
was_connected = self.connected
|
|
276
|
+
# Check sidecar liveness
|
|
277
|
+
try:
|
|
278
|
+
res = self.call('status', timeout=1.0)
|
|
279
|
+
self.peer_id = res.get('peer_id')
|
|
280
|
+
self.addrs = list(res.get('addrs') or []) # not a store: the local node's p2p multiaddrs from sidecar
|
|
281
|
+
if not was_connected:
|
|
282
|
+
self.connected = True
|
|
283
|
+
self._said_missing = False
|
|
284
|
+
did.append(('p2p sidecar connected', (self.peer_id or 'unknown')[:16]))
|
|
285
|
+
except (P2PError, SidecarError) as e:
|
|
286
|
+
if was_connected:
|
|
287
|
+
self.connected = False
|
|
288
|
+
did.append(('p2p sidecar dropped', str(e)))
|
|
289
|
+
elif not self._said_missing and self.config('p2p', False):
|
|
290
|
+
self._said_missing = True
|
|
291
|
+
return did
|
|
292
|
+
|
|
293
|
+
# Drain inbound streams and hand each to the router's envelope rule.
|
|
294
|
+
try:
|
|
295
|
+
res = self.call('drain', timeout=2.0, max_items=DRAIN_MAX)
|
|
296
|
+
except (P2PError, SidecarError) as e:
|
|
297
|
+
self.refusals.refuse('p2p', 'drain failed', detail={'error': str(e)})
|
|
298
|
+
return did
|
|
299
|
+
items = res.get('items') or [] if isinstance(res, dict) else []
|
|
300
|
+
for reason, n in ((res.get('refused') or {}) if isinstance(res, dict) else {}).items():
|
|
301
|
+
self.refusals.refuse('p2p', 'sidecar refused a stream: %s' % reason, detail={'count': n})
|
|
302
|
+
if items:
|
|
303
|
+
self.recv_count += len(items)
|
|
304
|
+
self.last_drain_at = now
|
|
305
|
+
did.append(('p2p drained', len(items)))
|
|
306
|
+
for it in items:
|
|
307
|
+
why = self.deliver(it)
|
|
308
|
+
if why is not None:
|
|
309
|
+
self.refused_count += 1
|
|
310
|
+
self.refusals.refuse('p2p', why, detail={'from_peer': str(it.get('from_peer', ''))[:24]})
|
|
311
|
+
return did
|
|
312
|
+
|
|
313
|
+
def deliver(self, item: Mapping[str, Any]) -> Optional[str]:
|
|
314
|
+
"""One drained stream to the router. Returns None, or why it was refused.
|
|
315
|
+
|
|
316
|
+
`injected` items are what the sidecar's test hook queued: no stream
|
|
317
|
+
was authenticated, so no origin key is asserted for them.
|
|
318
|
+
"""
|
|
319
|
+
msg = item.get('message')
|
|
320
|
+
if not isinstance(msg, dict):
|
|
321
|
+
return 'stream carried no object'
|
|
322
|
+
origin = None if item.get('injected') else item.get('from_key')
|
|
323
|
+
if self.transport is None or not hasattr(self.transport, 'deliver_direct'):
|
|
324
|
+
return 'no router to deliver to'
|
|
325
|
+
wrap_id = 'libp2p-%s' % hashlib.sha256(
|
|
326
|
+
json.dumps(msg, sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()[:16]
|
|
327
|
+
result: Optional[str] = self.transport.deliver_direct(SCHEME, msg, wrap_id, origin=origin)
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
def send_direct(self, target_multiaddr: str, payload: Mapping[str, Any]) -> tuple[bool, str]:
|
|
331
|
+
"""Deliver one sealed JWE to `target_multiaddr`; True only after the peer acked."""
|
|
332
|
+
try:
|
|
333
|
+
res = ask_sidecar(self.dir, 'send', timeout=6.0, target=target_multiaddr, payload=payload)
|
|
334
|
+
except (P2PError, SidecarError) as e:
|
|
335
|
+
self.refusals.refuse('p2p', 'direct send failed', detail={'target': target_multiaddr[:80], 'error': str(e)})
|
|
336
|
+
return False, str(e)
|
|
337
|
+
if not isinstance(res, dict) or not res.get('accepted'):
|
|
338
|
+
return False, 'sidecar did not report an acknowledged delivery'
|
|
339
|
+
self.sent_count += 1
|
|
340
|
+
return True, 'delivered via libp2p stream to %s' % res.get('peer_id', target_multiaddr)[:24]
|
|
341
|
+
|
|
342
|
+
def drain_cmd(self) -> Any:
|
|
343
|
+
return self.call('drain')
|
|
344
|
+
|
|
345
|
+
def send_cmd(self, args: Mapping[str, Any]) -> SendResult:
|
|
346
|
+
ok, note = self.send_direct(str(args['target']), dict(args['payload']))
|
|
347
|
+
return {'accepted': ok, 'note': note}
|
|
348
|
+
|
|
349
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Types and records for the P2P transport compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class P2PState(TypedDict, total=False):
|
|
8
|
+
enabled: bool
|
|
9
|
+
socket: str
|
|
10
|
+
connected: bool
|
|
11
|
+
peer_id: Optional[str]
|
|
12
|
+
addrs: list[str]
|
|
13
|
+
sent_count: int
|
|
14
|
+
recv_count: int
|
|
15
|
+
refused_count: int
|
|
16
|
+
last_drain_at: Optional[int]
|
|
17
|
+
sidecar: Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SendResult(TypedDict):
|
|
21
|
+
accepted: bool
|
|
22
|
+
note: str
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Pluggable protocol suites and package registry for Fleetmesh node.
|
|
2
|
+
|
|
3
|
+
Manages dynamic discovery, configuration, lifecycle, and multiplexing
|
|
4
|
+
across all supported open protocol families:
|
|
5
|
+
- Nostr & Blossom (`fleetmesh-protocol-nostr`)
|
|
6
|
+
- AT Protocol (`fleetmesh-protocol-atproto`)
|
|
7
|
+
- ActivityPub & ActivityStreams 2.0 (`fleetmesh-protocol-activitypub`)
|
|
8
|
+
- Secure Scuttlebutt (`fleetmesh-protocol-ssb`)
|
|
9
|
+
- p2panda (`fleetmesh-protocol-p2panda`)
|
|
10
|
+
- Rhio (`fleetmesh-protocol-rhio`)
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Callable, Mapping, Optional
|
|
17
|
+
|
|
18
|
+
from fleetmesh_core.wire.interfaces import (
|
|
19
|
+
ProtocolHealth,
|
|
20
|
+
ProtocolHostRouter,
|
|
21
|
+
ProtocolPackage,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Registry of built-in protocol packages
|
|
27
|
+
BUILTIN_PACKAGE_LOADERS: dict[str, tuple[str, str]] = {
|
|
28
|
+
'nostr': ('node.protocols.nostr.package', 'NostrProtocolPackage'),
|
|
29
|
+
'atproto': ('node.protocols.atproto.package', 'AtprotoProtocolPackage'),
|
|
30
|
+
'activitypub': ('node.protocols.activitypub.package', 'ActivityPubProtocolPackage'),
|
|
31
|
+
'ssb': ('node.protocols.ssb.package', 'SSBProtocolPackage'),
|
|
32
|
+
'p2panda': ('node.protocols.p2panda.package', 'P2PandaProtocolPackage'),
|
|
33
|
+
'rhio': ('node.protocols.rhio.package', 'RhioProtocolPackage'),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_CUSTOM_PACKAGES: dict[str, type[ProtocolPackage]] = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def register_protocol(name: str, package_cls: type[ProtocolPackage]) -> None:
|
|
40
|
+
"""Register a custom or third-party protocol package class."""
|
|
41
|
+
clean_name = name.lower().strip()
|
|
42
|
+
_CUSTOM_PACKAGES[clean_name] = package_cls
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_available_protocols() -> tuple[str, ...]:
|
|
46
|
+
"""Return all currently registered protocol names."""
|
|
47
|
+
return tuple(sorted(set(BUILTIN_PACKAGE_LOADERS.keys()) | set(_CUSTOM_PACKAGES.keys())))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_protocol_package_class(name: str) -> type[ProtocolPackage]:
|
|
51
|
+
"""Dynamically load and return the ProtocolPackage class for a given name."""
|
|
52
|
+
clean_name = name.lower().strip()
|
|
53
|
+
if clean_name in _CUSTOM_PACKAGES:
|
|
54
|
+
return _CUSTOM_PACKAGES[clean_name]
|
|
55
|
+
|
|
56
|
+
if clean_name in BUILTIN_PACKAGE_LOADERS:
|
|
57
|
+
mod_name, cls_name = BUILTIN_PACKAGE_LOADERS[clean_name]
|
|
58
|
+
try:
|
|
59
|
+
mod = importlib.import_module(mod_name)
|
|
60
|
+
cls = getattr(mod, cls_name)
|
|
61
|
+
return cls
|
|
62
|
+
except Exception as e:
|
|
63
|
+
raise ImportError(f"Failed to load protocol package '{clean_name}' from {mod_name}.{cls_name}: {e}") from e
|
|
64
|
+
|
|
65
|
+
raise KeyError(f"Unknown protocol package '{name}'. Available: {get_available_protocols()}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def create_protocol_package(name: str, **kwargs) -> ProtocolPackage:
|
|
69
|
+
"""Instantiate a protocol package by name with provided arguments."""
|
|
70
|
+
cls = load_protocol_package_class(name)
|
|
71
|
+
return cls(**kwargs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ProtocolManager:
|
|
75
|
+
"""Orchestrates configuration, lifecycle, and routing across all protocol packages."""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
node_dir: Optional[str] = None,
|
|
80
|
+
keystore: Optional[Any] = None,
|
|
81
|
+
clock: Optional[Callable[[], int]] = None,
|
|
82
|
+
name: str = 'meshnode',
|
|
83
|
+
):
|
|
84
|
+
self.node_dir = node_dir
|
|
85
|
+
self.keystore = keystore
|
|
86
|
+
self.clock = clock
|
|
87
|
+
self.node_name = name
|
|
88
|
+
|
|
89
|
+
self._packages: dict[str, ProtocolPackage] = {}
|
|
90
|
+
self._mounted_host: Optional[ProtocolHostRouter] = None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def packages(self) -> Mapping[str, ProtocolPackage]:
|
|
94
|
+
return dict(self._packages)
|
|
95
|
+
|
|
96
|
+
def get_package(self, name: str) -> Optional[ProtocolPackage]:
|
|
97
|
+
return self._packages.get(name.lower().strip())
|
|
98
|
+
|
|
99
|
+
def load_suites_config(
|
|
100
|
+
self,
|
|
101
|
+
suites_config: Any,
|
|
102
|
+
backend_store: Optional[Any] = None,
|
|
103
|
+
blob_store: Optional[Any] = None,
|
|
104
|
+
grants: Optional[Any] = None,
|
|
105
|
+
node_instance: Optional[Any] = None,
|
|
106
|
+
private_key: Optional[Any] = None,
|
|
107
|
+
strain_ledger: Optional[Any] = None,
|
|
108
|
+
) -> dict[str, ProtocolPackage]:
|
|
109
|
+
"""Initialize protocol packages based on ProtocolSuitesConfig or mapping."""
|
|
110
|
+
suites_dict: Mapping[str, Any] = {}
|
|
111
|
+
if hasattr(suites_config, 'to_dict'):
|
|
112
|
+
suites_dict = suites_config.to_dict()
|
|
113
|
+
elif hasattr(suites_config, 'as_dict'):
|
|
114
|
+
suites_dict = suites_config.as_dict()
|
|
115
|
+
elif hasattr(suites_config, '__dict__'):
|
|
116
|
+
suites_dict = {
|
|
117
|
+
k: getattr(suites_config, k).to_dict() if hasattr(getattr(suites_config, k), 'to_dict') else (getattr(suites_config, k).as_dict() if hasattr(getattr(suites_config, k), 'as_dict') else getattr(suites_config, k))
|
|
118
|
+
for k in ('nostr', 'atproto', 'activitypub', 'ssb', 'p2panda', 'rhio')
|
|
119
|
+
if getattr(suites_config, k, None) is not None
|
|
120
|
+
}
|
|
121
|
+
elif isinstance(suites_config, Mapping):
|
|
122
|
+
suites_dict = dict(suites_config)
|
|
123
|
+
|
|
124
|
+
if self._packages:
|
|
125
|
+
self.stop_all()
|
|
126
|
+
self._packages.clear()
|
|
127
|
+
|
|
128
|
+
# 1. Nostr
|
|
129
|
+
cfg_nostr = suites_dict.get('nostr')
|
|
130
|
+
if cfg_nostr is not None and (isinstance(cfg_nostr, Mapping) and cfg_nostr.get('enabled', True)):
|
|
131
|
+
from node.protocols.nostr.package import NostrProtocolPackage
|
|
132
|
+
pkg_nostr = NostrProtocolPackage(
|
|
133
|
+
backend=backend_store,
|
|
134
|
+
blob_store=blob_store,
|
|
135
|
+
grants=grants,
|
|
136
|
+
node=node_instance,
|
|
137
|
+
clock=self.clock,
|
|
138
|
+
name=self.node_name,
|
|
139
|
+
pubkey=getattr(self.keystore, 'node_pk', None) if self.keystore else None,
|
|
140
|
+
)
|
|
141
|
+
pkg_nostr.configure(cfg_nostr if isinstance(cfg_nostr, Mapping) else {})
|
|
142
|
+
self._packages['nostr'] = pkg_nostr
|
|
143
|
+
|
|
144
|
+
# 2. ATProto
|
|
145
|
+
cfg_atp = suites_dict.get('atproto')
|
|
146
|
+
if cfg_atp is not None and (isinstance(cfg_atp, Mapping) and cfg_atp.get('enabled', True)):
|
|
147
|
+
from node.protocols.atproto.package import AtprotoProtocolPackage
|
|
148
|
+
pkg_atp = AtprotoProtocolPackage(
|
|
149
|
+
blob_store=blob_store,
|
|
150
|
+
keystore=self.keystore,
|
|
151
|
+
)
|
|
152
|
+
pkg_atp.configure(cfg_atp if isinstance(cfg_atp, Mapping) else {})
|
|
153
|
+
self._packages['atproto'] = pkg_atp
|
|
154
|
+
|
|
155
|
+
# 3. ActivityPub
|
|
156
|
+
cfg_ap = suites_dict.get('activitypub')
|
|
157
|
+
if cfg_ap is not None and (isinstance(cfg_ap, Mapping) and cfg_ap.get('enabled', True)):
|
|
158
|
+
from node.protocols.activitypub.package import ActivityPubProtocolPackage
|
|
159
|
+
domain = cfg_ap.get('domain', 'node.fleetmesh.local') if isinstance(cfg_ap, Mapping) else 'node.fleetmesh.local'
|
|
160
|
+
pkg_ap = ActivityPubProtocolPackage(
|
|
161
|
+
domain=domain,
|
|
162
|
+
username="node",
|
|
163
|
+
private_key=private_key or getattr(self.keystore, 'ed25519_sk', None),
|
|
164
|
+
blob_store=blob_store,
|
|
165
|
+
strain_ledger=strain_ledger,
|
|
166
|
+
keystore=self.keystore,
|
|
167
|
+
)
|
|
168
|
+
pkg_ap.configure(cfg_ap if isinstance(cfg_ap, Mapping) else {})
|
|
169
|
+
self._packages['activitypub'] = pkg_ap
|
|
170
|
+
|
|
171
|
+
# 4. SSB
|
|
172
|
+
cfg_ssb = suites_dict.get('ssb')
|
|
173
|
+
if cfg_ssb is not None and (isinstance(cfg_ssb, Mapping) and cfg_ssb.get('enabled', True)):
|
|
174
|
+
from node.protocols.ssb.package import SSBProtocolPackage
|
|
175
|
+
pkg_ssb = SSBProtocolPackage(
|
|
176
|
+
node_dir=self.node_dir or "/tmp/fleetmesh-ssb",
|
|
177
|
+
private_key=private_key or getattr(self.keystore, 'ed25519_sk', None),
|
|
178
|
+
)
|
|
179
|
+
pkg_ssb.configure(cfg_ssb if isinstance(cfg_ssb, Mapping) else {})
|
|
180
|
+
self._packages['ssb'] = pkg_ssb
|
|
181
|
+
|
|
182
|
+
# 5. P2Panda
|
|
183
|
+
cfg_p2p = suites_dict.get('p2panda')
|
|
184
|
+
if cfg_p2p is not None and (isinstance(cfg_p2p, Mapping) and cfg_p2p.get('enabled', False)):
|
|
185
|
+
if 'p2panda' in _CUSTOM_PACKAGES or 'p2panda' in BUILTIN_PACKAGE_LOADERS:
|
|
186
|
+
try:
|
|
187
|
+
pkg_p2p = create_protocol_package(
|
|
188
|
+
'p2panda',
|
|
189
|
+
node_dir=self.node_dir,
|
|
190
|
+
keystore=self.keystore,
|
|
191
|
+
blob_store=blob_store,
|
|
192
|
+
)
|
|
193
|
+
pkg_p2p.configure(cfg_p2p if isinstance(cfg_p2p, Mapping) else {})
|
|
194
|
+
self._packages['p2panda'] = pkg_p2p
|
|
195
|
+
except Exception as e:
|
|
196
|
+
log.warning("Could not instantiate p2panda protocol package: %s", e)
|
|
197
|
+
|
|
198
|
+
# 6. Rhio
|
|
199
|
+
cfg_rhio = suites_dict.get('rhio')
|
|
200
|
+
if cfg_rhio is not None and (isinstance(cfg_rhio, Mapping) and cfg_rhio.get('enabled', False)):
|
|
201
|
+
if 'rhio' in _CUSTOM_PACKAGES or 'rhio' in BUILTIN_PACKAGE_LOADERS:
|
|
202
|
+
try:
|
|
203
|
+
pkg_rhio = create_protocol_package(
|
|
204
|
+
'rhio',
|
|
205
|
+
node_dir=self.node_dir,
|
|
206
|
+
keystore=self.keystore,
|
|
207
|
+
blob_store=blob_store,
|
|
208
|
+
)
|
|
209
|
+
pkg_rhio.configure(cfg_rhio if isinstance(cfg_rhio, Mapping) else {})
|
|
210
|
+
self._packages['rhio'] = pkg_rhio
|
|
211
|
+
except Exception as e:
|
|
212
|
+
log.warning("Could not instantiate rhio protocol package: %s", e)
|
|
213
|
+
|
|
214
|
+
return dict(self._packages)
|
|
215
|
+
|
|
216
|
+
def mount_ingress(self, host: ProtocolHostRouter) -> None:
|
|
217
|
+
"""Mount all active protocol packages onto the shared host multiplexer."""
|
|
218
|
+
self._mounted_host = host
|
|
219
|
+
for name, pkg in self._packages.items():
|
|
220
|
+
try:
|
|
221
|
+
pkg.mount_ingress(host)
|
|
222
|
+
log.info("Mounted ingress for protocol '%s'", name)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
log.exception("Failed to mount ingress for protocol '%s': %s", name, e)
|
|
225
|
+
|
|
226
|
+
def mount_transport_rungs(self, router: Any) -> list[Any]:
|
|
227
|
+
"""Mount outbound rungs into TransportRouter ladder."""
|
|
228
|
+
mounted = []
|
|
229
|
+
for name, pkg in self._packages.items():
|
|
230
|
+
try:
|
|
231
|
+
rung = pkg.transport_rung(router)
|
|
232
|
+
if rung is not None:
|
|
233
|
+
if hasattr(router, 'register_rung'):
|
|
234
|
+
router.register_rung(rung)
|
|
235
|
+
elif hasattr(router, 'add_rung'):
|
|
236
|
+
router.add_rung(rung)
|
|
237
|
+
mounted.append(rung)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
log.exception("Failed to build transport rung for protocol '%s': %s", name, e)
|
|
240
|
+
return mounted
|
|
241
|
+
|
|
242
|
+
def start_all(self) -> None:
|
|
243
|
+
"""Start all configured protocol packages."""
|
|
244
|
+
for name, pkg in self._packages.items():
|
|
245
|
+
try:
|
|
246
|
+
pkg.start()
|
|
247
|
+
except Exception as e:
|
|
248
|
+
log.exception("Error starting protocol package '%s': %s", name, e)
|
|
249
|
+
|
|
250
|
+
def stop_all(self) -> None:
|
|
251
|
+
"""Stop all configured protocol packages and unmount ingress."""
|
|
252
|
+
for name, pkg in self._packages.items():
|
|
253
|
+
try:
|
|
254
|
+
pkg.stop()
|
|
255
|
+
except Exception as e:
|
|
256
|
+
log.exception("Error stopping protocol package '%s': %s", name, e)
|
|
257
|
+
if self._mounted_host is not None:
|
|
258
|
+
for name in self._packages:
|
|
259
|
+
try:
|
|
260
|
+
self._mounted_host.unmount_all(name)
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
self._mounted_host = None
|
|
264
|
+
|
|
265
|
+
def health(self) -> dict[str, ProtocolHealth]:
|
|
266
|
+
"""Collect health telemetry across all loaded protocol packages."""
|
|
267
|
+
return {name: pkg.health() for name, pkg in self._packages.items()}
|
|
268
|
+
|
|
269
|
+
def descriptor_capabilities(self) -> tuple[tuple[str, str], ...]:
|
|
270
|
+
"""Aggregate descriptor capabilities from all active packages."""
|
|
271
|
+
caps: list[tuple[str, str]] = []
|
|
272
|
+
for pkg in self._packages.values():
|
|
273
|
+
try:
|
|
274
|
+
caps.extend(pkg.descriptor_capabilities())
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
return tuple(caps)
|