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
node/channels/bus.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Internal publish/subscribe event bus for decoupled node communication.
|
|
2
|
+
|
|
3
|
+
Replaces cyclic cross-subsystem dependencies and monkey-patching with an
|
|
4
|
+
in-process event bus. Standard library only.
|
|
5
|
+
|
|
6
|
+
The bus is synchronous and in-process. The `Tap` is how it reaches outside:
|
|
7
|
+
every emit is also serialised through the topic's wire function in WIRE and
|
|
8
|
+
appended to a ring buffer with a sequence number, and each `Subscriber`
|
|
9
|
+
holds a bounded queue the control socket drains. The emitting thread never
|
|
10
|
+
waits on a reader: a full queue drops its oldest entry and the subscriber
|
|
11
|
+
is told how many went by. Nothing here is durable across a restart and
|
|
12
|
+
nothing claims to be - a reader that needs every event replays the relays.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import collections
|
|
17
|
+
import json
|
|
18
|
+
import queue
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, TypedDict
|
|
22
|
+
|
|
23
|
+
# Standard event bus topics
|
|
24
|
+
TOPIC_WINDOW_EVALUATED: str = 'window.evaluated'
|
|
25
|
+
TOPIC_WINDOW_REPRICED: str = 'window.repriced'
|
|
26
|
+
TOPIC_SESSION_COMPLETED: str = 'session.completed'
|
|
27
|
+
TOPIC_GRANT_ISSUED: str = 'grant.issued'
|
|
28
|
+
TOPIC_PEER_CONNECTED: str = 'peer.connected'
|
|
29
|
+
TOPIC_REPORT_ADMITTED: str = 'report.admitted'
|
|
30
|
+
TOPIC_META_DROPPED: str = 'meta.dropped' # only on a subscriber's own stream: {count}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MetaDroppedWire(TypedDict):
|
|
34
|
+
count: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LossyWire(TypedDict, total=False):
|
|
38
|
+
lossy: bool
|
|
39
|
+
repr: str
|
|
40
|
+
error: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WindowEvaluatedWire(TypedDict, total=False):
|
|
44
|
+
peer: str
|
|
45
|
+
epoch: int
|
|
46
|
+
severity: str
|
|
47
|
+
measured: Optional[Mapping[str, int]]
|
|
48
|
+
claimed: Optional[Mapping[str, int]]
|
|
49
|
+
closed: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class WindowRepricedWire(TypedDict):
|
|
53
|
+
peer: str
|
|
54
|
+
epoch: int
|
|
55
|
+
was: str
|
|
56
|
+
now: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SessionCompletedWire(TypedDict, total=False):
|
|
60
|
+
peer: str
|
|
61
|
+
role: str
|
|
62
|
+
status: Optional[str]
|
|
63
|
+
counters: Optional[Mapping[str, int | float]]
|
|
64
|
+
findings: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GrantIssuedWire(TypedDict):
|
|
68
|
+
peer: str
|
|
69
|
+
cause: str
|
|
70
|
+
envelope: str
|
|
71
|
+
tier: Optional[str]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PeerConnectedWire(TypedDict):
|
|
75
|
+
peer: str
|
|
76
|
+
role: str
|
|
77
|
+
transport: Optional[str]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ReportAdmittedWire(TypedDict):
|
|
81
|
+
reporter: str
|
|
82
|
+
target: str
|
|
83
|
+
id: str
|
|
84
|
+
admissible: bool
|
|
85
|
+
contradicted: bool
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
WirePayload = (
|
|
89
|
+
WindowEvaluatedWire
|
|
90
|
+
| WindowRepricedWire
|
|
91
|
+
| SessionCompletedWire
|
|
92
|
+
| GrantIssuedWire
|
|
93
|
+
| PeerConnectedWire
|
|
94
|
+
| ReportAdmittedWire
|
|
95
|
+
| MetaDroppedWire
|
|
96
|
+
| LossyWire
|
|
97
|
+
| Mapping[str, Any]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BusEnvelope(TypedDict, total=False):
|
|
102
|
+
seq: Optional[int]
|
|
103
|
+
ts: int
|
|
104
|
+
topic: str
|
|
105
|
+
data: WirePayload
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class BusState(TypedDict):
|
|
109
|
+
seq: int
|
|
110
|
+
ring: int
|
|
111
|
+
ring_max: Optional[int]
|
|
112
|
+
subscribers: int
|
|
113
|
+
oldest: Optional[int]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
EventHandler = Callable[..., Any]
|
|
117
|
+
WireFn = Callable[..., WirePayload]
|
|
118
|
+
|
|
119
|
+
def _wire_evaluated(rec: Any) -> WindowEvaluatedWire:
|
|
120
|
+
rec = rec if isinstance(rec, dict) else {}
|
|
121
|
+
return {k: rec.get(k) for k in ('peer', 'epoch', 'severity', 'measured', 'claimed', 'closed') if k in rec} # type: ignore[return-value]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _wire_repriced(peer: str, epoch: int, was: str, now: str) -> WindowRepricedWire:
|
|
125
|
+
return {'peer': peer, 'epoch': epoch, 'was': was, 'now': now}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _wire_session(peer: str, role: str, result: Any) -> SessionCompletedWire:
|
|
129
|
+
result = result if isinstance(result, dict) else {}
|
|
130
|
+
return {'peer': peer, 'role': role, 'status': result.get('status'),
|
|
131
|
+
'counters': result.get('counters'), 'findings': len(result.get('findings') or ())}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _wire_grant(peer: str, cause: str, envelope_id: str, tier: Optional[str]) -> GrantIssuedWire:
|
|
135
|
+
return {'peer': peer, 'cause': cause, 'envelope': envelope_id, 'tier': tier}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _wire_peer(peer: str, role: str, transport: Optional[str]) -> PeerConnectedWire:
|
|
139
|
+
return {'peer': peer, 'role': role, 'transport': transport}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _wire_report(reporter: str, target: str, event_id: str, admissible: bool, contradicted: bool) -> ReportAdmittedWire:
|
|
143
|
+
return {'reporter': reporter, 'target': target, 'id': event_id, 'admissible': admissible, 'contradicted': contradicted}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# topic -> how its emit arguments go on the wire. Every topic a node emits
|
|
147
|
+
# has an entry, so what a subscriber sees is a decision made next to the
|
|
148
|
+
# emit, not whatever json.dumps(default=str) makes of a Python object. A
|
|
149
|
+
# topic with no entry is still delivered, as {"lossy": true, "repr": ...},
|
|
150
|
+
# so a missing entry is visible rather than a silent drop. Plugins add
|
|
151
|
+
# their own with register_wire; they may not replace one of these.
|
|
152
|
+
WIRE: Dict[str, WireFn] = {
|
|
153
|
+
TOPIC_WINDOW_EVALUATED: _wire_evaluated,
|
|
154
|
+
TOPIC_WINDOW_REPRICED: _wire_repriced,
|
|
155
|
+
TOPIC_SESSION_COMPLETED: _wire_session,
|
|
156
|
+
TOPIC_GRANT_ISSUED: _wire_grant,
|
|
157
|
+
TOPIC_PEER_CONNECTED: _wire_peer,
|
|
158
|
+
TOPIC_REPORT_ADMITTED: _wire_report,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def register_wire(topic: str, fn: WireFn) -> None:
|
|
163
|
+
"""A plugin's own topic, with how it goes on the wire. Adds, never replaces."""
|
|
164
|
+
if topic in WIRE:
|
|
165
|
+
raise ValueError('topic %s already has a wire form; a plugin may add topics, not redefine them' % topic)
|
|
166
|
+
WIRE[topic] = fn
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def to_wire(topic: str, args: tuple, kwargs: dict) -> WirePayload:
|
|
170
|
+
fn = WIRE.get(topic)
|
|
171
|
+
if fn is None:
|
|
172
|
+
return {'lossy': True, 'repr': repr((args, kwargs))[:512]}
|
|
173
|
+
try:
|
|
174
|
+
data = fn(*args, **kwargs)
|
|
175
|
+
json.dumps(data) # prove it is wire-safe here, on the emitting side
|
|
176
|
+
return data
|
|
177
|
+
except Exception as e: # a wire function must never take the emitter down
|
|
178
|
+
return {'lossy': True, 'repr': repr((args, kwargs))[:512], 'error': '%s: %s' % (type(e).__name__, e)}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Subscriber(object):
|
|
182
|
+
"""One reader's bounded queue. `next(timeout)` blocks for one envelope or
|
|
183
|
+
returns None; `dropped` is how many were pushed out by a full queue since
|
|
184
|
+
the last one the reader took, reported to it as a meta.dropped envelope."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, topics: Optional[set[str]], maxsize: int) -> None:
|
|
187
|
+
self.topics: Optional[set[str]] = topics # None: every topic
|
|
188
|
+
self.q: queue.Queue[BusEnvelope] = queue.Queue(maxsize=maxsize)
|
|
189
|
+
self.dropped: int = 0
|
|
190
|
+
self._lock = threading.Lock()
|
|
191
|
+
|
|
192
|
+
def wants(self, topic: str) -> bool:
|
|
193
|
+
return self.topics is None or topic in self.topics
|
|
194
|
+
|
|
195
|
+
def push(self, env: BusEnvelope) -> None:
|
|
196
|
+
with self._lock:
|
|
197
|
+
while True:
|
|
198
|
+
try:
|
|
199
|
+
self.q.put_nowait(env)
|
|
200
|
+
return
|
|
201
|
+
except queue.Full:
|
|
202
|
+
try:
|
|
203
|
+
self.q.get_nowait()
|
|
204
|
+
self.dropped += 1
|
|
205
|
+
except queue.Empty:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
def next(self, timeout: float) -> Optional[BusEnvelope]:
|
|
209
|
+
with self._lock:
|
|
210
|
+
if self.dropped:
|
|
211
|
+
n, self.dropped = self.dropped, 0
|
|
212
|
+
return {'seq': None, 'ts': int(time.time()), 'topic': TOPIC_META_DROPPED, 'data': {'count': n}}
|
|
213
|
+
try:
|
|
214
|
+
return self.q.get(timeout=timeout)
|
|
215
|
+
except queue.Empty:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Tap(object):
|
|
220
|
+
"""Sequence numbers, a ring of the last `ring` envelopes, and the live subscribers."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, ring: int = 1000, queue_size: int = 1000, clock: Optional[Callable[[], int]] = None) -> None:
|
|
223
|
+
self.seq: int = 0
|
|
224
|
+
self.ring: collections.deque[BusEnvelope] = collections.deque(maxlen=ring)
|
|
225
|
+
self.queue_size: int = queue_size
|
|
226
|
+
self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
227
|
+
self._subs: List[Subscriber] = []
|
|
228
|
+
self._lock = threading.Lock()
|
|
229
|
+
|
|
230
|
+
def record(self, topic: str, args: tuple, kwargs: dict) -> BusEnvelope:
|
|
231
|
+
data = to_wire(topic, args, kwargs)
|
|
232
|
+
with self._lock:
|
|
233
|
+
self.seq += 1
|
|
234
|
+
env: BusEnvelope = {'seq': self.seq, 'ts': self.clock(), 'topic': topic, 'data': data}
|
|
235
|
+
self.ring.append(env)
|
|
236
|
+
subs = [s for s in self._subs if s.wants(topic)]
|
|
237
|
+
for s in subs:
|
|
238
|
+
s.push(env)
|
|
239
|
+
return env
|
|
240
|
+
|
|
241
|
+
def subscribe(self, topics: Optional[set[str]] = None) -> Subscriber:
|
|
242
|
+
s = Subscriber(topics, self.queue_size)
|
|
243
|
+
with self._lock:
|
|
244
|
+
self._subs.append(s)
|
|
245
|
+
return s
|
|
246
|
+
|
|
247
|
+
def unsubscribe(self, s: Subscriber) -> None:
|
|
248
|
+
with self._lock:
|
|
249
|
+
if s in self._subs:
|
|
250
|
+
self._subs.remove(s)
|
|
251
|
+
|
|
252
|
+
def since(self, seq: int, topics: Optional[set[str]] = None, last: int = 0) -> List[BusEnvelope]:
|
|
253
|
+
"""Envelopes after `seq` still in the ring, oldest first; `last` keeps only that many newest."""
|
|
254
|
+
with self._lock:
|
|
255
|
+
out = [e for e in self.ring if (e.get('seq') or 0) > seq and (topics is None or e['topic'] in topics)]
|
|
256
|
+
return out[-last:] if last else out
|
|
257
|
+
|
|
258
|
+
def state(self) -> BusState:
|
|
259
|
+
with self._lock:
|
|
260
|
+
return {'seq': self.seq, 'ring': len(self.ring), 'ring_max': self.ring.maxlen,
|
|
261
|
+
'subscribers': len(self._subs), 'oldest': self.ring[0]['seq'] if self.ring else None}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class NodeEventBus:
|
|
265
|
+
"""Thread-safe synchronous in-process event bus."""
|
|
266
|
+
|
|
267
|
+
def __init__(self) -> None:
|
|
268
|
+
self._subscribers: Dict[str, List[EventHandler]] = collections.defaultdict(list)
|
|
269
|
+
self._lock: threading.RLock = threading.RLock()
|
|
270
|
+
self._emit_counts: collections.Counter[str] = collections.Counter()
|
|
271
|
+
self.tap: Optional[Tap] = None # set by the node; tests may leave it None
|
|
272
|
+
|
|
273
|
+
def subscribe(self, topic: str, handler: EventHandler) -> None:
|
|
274
|
+
"""Subscribe a callable to a topic."""
|
|
275
|
+
with self._lock:
|
|
276
|
+
if handler not in self._subscribers[topic]:
|
|
277
|
+
self._subscribers[topic].append(handler)
|
|
278
|
+
|
|
279
|
+
def unsubscribe(self, topic: str, handler: EventHandler) -> bool:
|
|
280
|
+
"""Unsubscribe a callable from a topic. Returns True if removed."""
|
|
281
|
+
with self._lock:
|
|
282
|
+
if topic in self._subscribers:
|
|
283
|
+
try:
|
|
284
|
+
self._subscribers[topic].remove(handler)
|
|
285
|
+
return True
|
|
286
|
+
except ValueError:
|
|
287
|
+
return False
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def emit(self, topic: str, *args: Any, **kwargs: Any) -> List[Any]:
|
|
291
|
+
"""Emit an event to all subscribers of the topic.
|
|
292
|
+
|
|
293
|
+
Exceptions in handlers are caught and isolated so other subscribers still run.
|
|
294
|
+
Returns a list of handler return values.
|
|
295
|
+
"""
|
|
296
|
+
with self._lock:
|
|
297
|
+
self._emit_counts[topic] += 1
|
|
298
|
+
handlers = list(self._subscribers.get(topic, ()))
|
|
299
|
+
if self.tap is not None:
|
|
300
|
+
self.tap.record(topic, args, kwargs)
|
|
301
|
+
|
|
302
|
+
results = []
|
|
303
|
+
for h in handlers:
|
|
304
|
+
try:
|
|
305
|
+
res = h(*args, **kwargs)
|
|
306
|
+
results.append(res)
|
|
307
|
+
except Exception:
|
|
308
|
+
results.append(None)
|
|
309
|
+
return results
|
|
310
|
+
|
|
311
|
+
def subscribers(self, topic: str) -> List[EventHandler]:
|
|
312
|
+
"""Return a snapshot list of subscribers for a topic."""
|
|
313
|
+
with self._lock:
|
|
314
|
+
return list(self._subscribers.get(topic, ()))
|
|
315
|
+
|
|
316
|
+
def topics(self) -> List[str]:
|
|
317
|
+
"""Return all topics that have at least one active subscriber."""
|
|
318
|
+
with self._lock:
|
|
319
|
+
return [t for t, subs in self._subscribers.items() if subs]
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def counts(self) -> collections.Counter[str]:
|
|
323
|
+
"""Return emission counts by topic."""
|
|
324
|
+
with self._lock:
|
|
325
|
+
return collections.Counter(self._emit_counts)
|
|
326
|
+
|
|
327
|
+
def clear(self) -> None:
|
|
328
|
+
"""Remove all subscriptions."""
|
|
329
|
+
with self._lock:
|
|
330
|
+
self._subscribers.clear()
|
|
331
|
+
self._emit_counts.clear()
|
node/channels/journal.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""How a node says things to its journal.
|
|
2
|
+
|
|
3
|
+
These two lived in app.py, where a compartment could not reach them without
|
|
4
|
+
importing the node that imports the compartment. They are not the node's:
|
|
5
|
+
they are how anything in the node writes a line an operator will read.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def line(*parts: Any) -> None:
|
|
15
|
+
"""One timestamped line on stderr, flushed.
|
|
16
|
+
|
|
17
|
+
A node runs under systemd, where both streams are the journal, and the
|
|
18
|
+
first deployment of two of these found them completely silent: `journalctl
|
|
19
|
+
-u meshnode` held one line, the one systemd wrote itself. An operator could
|
|
20
|
+
not tell whether the node had peered, pulled, been declined or crashed
|
|
21
|
+
without reading a JSON file it had never been told about. A daemon that
|
|
22
|
+
does work says what it did.
|
|
23
|
+
|
|
24
|
+
It says it on **stderr**, and that is not cosmetic. It used to go to
|
|
25
|
+
stdout, which is also where `meshnode <cmd> --json` writes its answer, and
|
|
26
|
+
a node busy enough to say something while a command was answering put a
|
|
27
|
+
line like
|
|
28
|
+
|
|
29
|
+
2026-09-05T21:56:24Z < acked-out 67dd799a6be0… accepted
|
|
30
|
+
[{"epoch": 496527, ...}]
|
|
31
|
+
|
|
32
|
+
on one stream. The reader got `Extra data: line 1 column 5`. node/README.md
|
|
33
|
+
offers `--json` and the control socket to "a dashboard, a metrics exporter,
|
|
34
|
+
a fleet script"; a stream that is usually parseable is not an API. So
|
|
35
|
+
stdout is the answer to what was asked, stderr is the running commentary,
|
|
36
|
+
and systemd captures both exactly as before.
|
|
37
|
+
"""
|
|
38
|
+
print('%s %s' % (time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
39
|
+
' '.join(str(p) for p in parts)), file=sys.stderr, flush=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def short(x: Any) -> str:
|
|
43
|
+
"""Long hex is unreadable in a log and its first bytes identify it."""
|
|
44
|
+
s = str(x)
|
|
45
|
+
return s[:12] + '…' if len(s) > 16 and all(c in '0123456789abcdef' for c in s) else s
|
|
46
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Pluggable command providers for the meshnode CLI.
|
|
2
|
+
|
|
3
|
+
Each module under node/commands owns a group of subcommands and exposes one
|
|
4
|
+
provider class with a `register(subparsers)` classmethod. PROVIDERS, in the
|
|
5
|
+
order below, is the complete parser: meshnode.main iterates it and registers
|
|
6
|
+
nothing else, and scripts/check-cli.py builds the same parser to hold the
|
|
7
|
+
documentation to it. Command functions are imported from their own module
|
|
8
|
+
(`from node.commands.transport_cmds import cmd_fips`), not re-exported here.
|
|
9
|
+
|
|
10
|
+
The modules that share a name with a top-level package (compute, transport,
|
|
11
|
+
cluster) carry a `_cmds` suffix. Inside this package `from .compute import X`
|
|
12
|
+
and `from ..compute import X` are one character apart and name different
|
|
13
|
+
modules; the suffix keeps the relative import from ever shadowing the package
|
|
14
|
+
it wraps.
|
|
15
|
+
|
|
16
|
+
A plugin (docs/extending.md) may bring verbs of its own. It declares them as
|
|
17
|
+
a class attribute `cli = (('verb', register_fn), ...)`; `register_fn(sub)` is
|
|
18
|
+
handed the same subparsers object every provider gets. `plugin_cli_classes`
|
|
19
|
+
finds the plugins a `meshnode VERB DIR ...` invocation refers to by reading
|
|
20
|
+
DIR's config.json (the first argument that is a node directory), or the
|
|
21
|
+
`FLEETMESH_PLUGINS` environment variable (comma-separated refs) when the
|
|
22
|
+
command has no directory yet; `register_plugin_cli` adds their verbs. A verb
|
|
23
|
+
that collides with a core command is refused by name, like a plugin whose
|
|
24
|
+
compartment name collides.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import importlib
|
|
30
|
+
import os
|
|
31
|
+
from typing import Any, Iterable, Protocol, Sequence, Type
|
|
32
|
+
|
|
33
|
+
from .cluster_cmds import ClusterCommandProvider
|
|
34
|
+
from .compute_cmds import ComputeCommandProvider
|
|
35
|
+
from .core import CoreCommandProvider
|
|
36
|
+
from .gossip_cmds import GossipCommandProvider
|
|
37
|
+
from .nsite_cmds import NsiteCommandProvider
|
|
38
|
+
from .peering import PeeringCommandProvider
|
|
39
|
+
from .plugin_cmds import PluginCommandProvider
|
|
40
|
+
from .sync import SyncCommandProvider
|
|
41
|
+
from ..compartment import Alias, BoundCommand, Command, CommandRecord, declared, every_command
|
|
42
|
+
from .sidecar_cmds import SidecarsCommandProvider
|
|
43
|
+
from .transport_cmds import TransportCommandProvider
|
|
44
|
+
from .video_cmds import VideoCommandProvider
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CommandProvider(Protocol):
|
|
48
|
+
"""Protocol for modular CLI command providers."""
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def register(cls, subparsers: argparse._SubParsersAction) -> None:
|
|
52
|
+
"""Register subcommands and argument definitions."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
PROVIDERS: tuple[Type[CommandProvider], ...] = (
|
|
57
|
+
CoreCommandProvider,
|
|
58
|
+
PeeringCommandProvider,
|
|
59
|
+
SyncCommandProvider,
|
|
60
|
+
ComputeCommandProvider,
|
|
61
|
+
GossipCommandProvider,
|
|
62
|
+
TransportCommandProvider,
|
|
63
|
+
ClusterCommandProvider,
|
|
64
|
+
NsiteCommandProvider,
|
|
65
|
+
SidecarsCommandProvider,
|
|
66
|
+
PluginCommandProvider,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
PLUGINS_ENV = 'FLEETMESH_PLUGINS'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _import_ref(ref: str) -> Any:
|
|
73
|
+
mod_name, _, cls_name = ref.partition(':')
|
|
74
|
+
try:
|
|
75
|
+
mod = importlib.import_module(mod_name)
|
|
76
|
+
except ImportError as e:
|
|
77
|
+
raise ValueError('plugin %s: cannot import %s (%s)' % (ref, mod_name, e)) from e
|
|
78
|
+
cls = getattr(mod, cls_name, None)
|
|
79
|
+
if cls is None:
|
|
80
|
+
raise ValueError('plugin %s: %s has no %s' % (ref, mod_name, cls_name))
|
|
81
|
+
return cls
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def plugin_refs_for(argv: Sequence[str] | None) -> list[str]:
|
|
85
|
+
"""The plugin refs an invocation refers to: DIR's config.json `plugins`
|
|
86
|
+
block when an argument is a node directory, else FLEETMESH_PLUGINS."""
|
|
87
|
+
refs: list[str] = []
|
|
88
|
+
for arg in (argv or ()):
|
|
89
|
+
if os.path.isfile(os.path.join(str(arg), 'config.json')):
|
|
90
|
+
try:
|
|
91
|
+
from ..app.config import load_config
|
|
92
|
+
raw = load_config(str(arg)).get('plugins') or []
|
|
93
|
+
except Exception:
|
|
94
|
+
raw = []
|
|
95
|
+
for e in raw:
|
|
96
|
+
ref = e if isinstance(e, str) else (e or {}).get('ref')
|
|
97
|
+
if isinstance(ref, str) and ref not in refs:
|
|
98
|
+
refs.append(ref)
|
|
99
|
+
break
|
|
100
|
+
for ref in (os.environ.get(PLUGINS_ENV) or '').split(','):
|
|
101
|
+
ref = ref.strip()
|
|
102
|
+
if ref and ref not in refs:
|
|
103
|
+
refs.append(ref)
|
|
104
|
+
return refs
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def plugin_cli_classes(argv: Sequence[str] | None) -> list[Any]:
|
|
108
|
+
"""The plugin classes that declare a `cli` hook, for this invocation."""
|
|
109
|
+
out: list[Any] = []
|
|
110
|
+
for ref in plugin_refs_for(argv):
|
|
111
|
+
cls = _import_ref(ref)
|
|
112
|
+
if getattr(cls, 'cli', None):
|
|
113
|
+
out.append(cls)
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def register_plugin_cli(sub: argparse._SubParsersAction, classes: Iterable[Any]) -> list[str]:
|
|
118
|
+
"""Add every `cli` verb the classes declare; returns the verbs added."""
|
|
119
|
+
added: list[str] = []
|
|
120
|
+
for cls in classes:
|
|
121
|
+
for verb, register in (getattr(cls, 'cli', None) or ()):
|
|
122
|
+
if verb in sub.choices:
|
|
123
|
+
raise ValueError('plugin %s: command %r already exists' % (getattr(cls, 'name', cls.__name__), verb))
|
|
124
|
+
register(sub)
|
|
125
|
+
added.append(verb)
|
|
126
|
+
return added
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# A literal list, not built from splats: pyflakes reads __all__ statically and
|
|
130
|
+
# only a literal tells it these re-exports are used. Same reason
|
|
131
|
+
# node/config/__init__.py keeps one.
|
|
132
|
+
__all__ = [
|
|
133
|
+
# the declarative command surface, re-exported for compartments
|
|
134
|
+
'Alias', 'BoundCommand', 'Command', 'CommandRecord', 'declared', 'every_command',
|
|
135
|
+
# the CLI providers
|
|
136
|
+
'CommandProvider', 'PROVIDERS', 'CoreCommandProvider', 'GossipCommandProvider',
|
|
137
|
+
'NsiteCommandProvider', 'PeeringCommandProvider', 'PluginCommandProvider',
|
|
138
|
+
'SidecarsCommandProvider', 'SyncCommandProvider', 'TransportCommandProvider',
|
|
139
|
+
'VideoCommandProvider', 'register_plugin_cli',
|
|
140
|
+
]
|