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,204 @@
|
|
|
1
|
+
"""What this node has survived, and the reserves it sets.
|
|
2
|
+
|
|
3
|
+
`fleetmesh_core/strain.py` is the arithmetic and states why this is safe to act on. This is
|
|
4
|
+
the part that records and remembers.
|
|
5
|
+
|
|
6
|
+
Everything here was already being thrown away. A restart, a peer that went quiet
|
|
7
|
+
mid-session, a relay that dropped its subscription, a clock that drifted past
|
|
8
|
+
tolerance - the node correctly refuses to make any of them a finding about a
|
|
9
|
+
peer, and then discards them, so the information goes out with the accusation.
|
|
10
|
+
|
|
11
|
+
Nothing recorded here crosses the wire and nothing here is a finding. What it
|
|
12
|
+
does is thicken what this node holds in hand: how long it keeps accounting a
|
|
13
|
+
vanished peer might still need, and how long it goes on trying a door that was
|
|
14
|
+
closed. A node that has weathered a thing becomes better prepared for that thing,
|
|
15
|
+
which is the only kind of adaptation an attacker gains nothing by provoking.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Callable, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from .types import FlagTreeReport, StrainStateReport
|
|
23
|
+
|
|
24
|
+
import dataclasses
|
|
25
|
+
from fleetmesh_core.accounting import strain as ref_strain
|
|
26
|
+
from fleetmesh_core.wire.interfaces import StrainDirection
|
|
27
|
+
from node.compartment import stores as st
|
|
28
|
+
from node.channels import Channel, NOTE
|
|
29
|
+
from node.metrics import COUNTER, Metric
|
|
30
|
+
from node.compartment import Command, Compartment, Need
|
|
31
|
+
from node.config.parameter import Parameter, ParamBlock
|
|
32
|
+
|
|
33
|
+
# `ref_strain.direction` normalises None to '' so a direction is always three
|
|
34
|
+
# strings; the ledger keys on the same shape so the ref arithmetic can take
|
|
35
|
+
# its dicts unchanged.
|
|
36
|
+
DirectionTuple = StrainDirection
|
|
37
|
+
|
|
38
|
+
# fleetmesh_core/strain.py annotates KINDS as tuple[int, ...] but constants.json lists
|
|
39
|
+
# strings; keep a correctly-typed view here so the membership check below is
|
|
40
|
+
# checked as str-in-str. Fix belongs in fleetmesh_core/ (not owned here).
|
|
41
|
+
_KINDS: frozenset[str] = frozenset(str(k) for k in ref_strain.KINDS)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclasses.dataclass(frozen=True)
|
|
45
|
+
class StrainConfig(ParamBlock):
|
|
46
|
+
key = 'strain'
|
|
47
|
+
name = 'strain'
|
|
48
|
+
DERIVED: bool = False
|
|
49
|
+
PARAMS = (
|
|
50
|
+
Parameter(key='max_directions', name='Max Directions', description='Maximum directions tracked in strain ledger', type=int, default=4096, tags=('strain', 'reserves')),
|
|
51
|
+
)
|
|
52
|
+
max_directions: int = 4096
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class StrainLedger(Compartment):
|
|
56
|
+
"""What this node has survived, as a compartment the operator can watch.
|
|
57
|
+
|
|
58
|
+
It reads no config and changes nothing about any peer: what it holds is the
|
|
59
|
+
node's own reserves. Its trouble is the flag tree - a node whose strain is
|
|
60
|
+
nearly all one direction is structurally dependent on that peer and will
|
|
61
|
+
snap when it leaves, and nothing else in the protocol says so.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
name: str = 'strain'
|
|
65
|
+
config_cls = StrainConfig
|
|
66
|
+
CHANNELS = (
|
|
67
|
+
Channel('strain_recorded', 'Strain event recorded in ledger', level=NOTE, say='strain event {kind} recorded', tags=('strain', 'reserves')),
|
|
68
|
+
)
|
|
69
|
+
METRICS = (
|
|
70
|
+
Metric(name='strain_events_recorded', description='Times strain events were recorded.', kind=COUNTER, counts='strain_recorded', tags=('strain', 'reserves')),
|
|
71
|
+
)
|
|
72
|
+
NEEDS = (
|
|
73
|
+
Need('accounts', 'stores.accounts'),
|
|
74
|
+
Need('ledger', 'ledger'),
|
|
75
|
+
)
|
|
76
|
+
COMMANDS = (
|
|
77
|
+
Command(
|
|
78
|
+
key='show',
|
|
79
|
+
name='Strain',
|
|
80
|
+
description='what this node has survived, by direction, and the reserves it sets',
|
|
81
|
+
tags=('strain', 'status'),
|
|
82
|
+
permission='strain/show',
|
|
83
|
+
run='state',
|
|
84
|
+
reply='{kinds, directions, weighted_directions, reserve_multiple, dominant_share, flag_tree}',
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
STORES = (
|
|
88
|
+
st.Store(key='counts', description='strain counts per direction and kind',
|
|
89
|
+
kind=st.TABLE, shape=dict, bound=4096, keyed_by='direction', evict=st.LRU),
|
|
90
|
+
st.Store(key='last', description='strain last timestamp per direction and kind',
|
|
91
|
+
kind=st.TABLE, shape=dict, bound=4096, keyed_by='direction', evict=st.LRU),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
accounts: Optional[Any] = None,
|
|
97
|
+
clock: Optional[Callable[[], int]] = None,
|
|
98
|
+
tier_of: Optional[Callable[[str], str]] = None,
|
|
99
|
+
node: Optional[Any] = None,
|
|
100
|
+
ledger: Optional[Any] = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
super().__init__(node, clock)
|
|
103
|
+
self.accounts: Optional[Any] = accounts
|
|
104
|
+
if tier_of is None and ledger is not None:
|
|
105
|
+
self.tier_of: Callable[[str], str] = lambda pk: getattr(ledger, 'tier', {}).get(pk, 'probation')
|
|
106
|
+
else:
|
|
107
|
+
self.tier_of = tier_of or (lambda _pk: 'probation')
|
|
108
|
+
self.counts: st.Table[DirectionTuple, dict[str, int]] # direction -> kind -> n
|
|
109
|
+
self.last: st.Table[DirectionTuple, dict[str, int]] # direction -> kind -> when
|
|
110
|
+
if accounts is not None:
|
|
111
|
+
for peer, shard, transport, kind, n, at in accounts.strain():
|
|
112
|
+
d = ref_strain.direction(peer, shard, transport)
|
|
113
|
+
self.counts.setdefault(d, {})[kind] = n
|
|
114
|
+
self.last.setdefault(d, {})[kind] = at
|
|
115
|
+
|
|
116
|
+
def record(
|
|
117
|
+
self,
|
|
118
|
+
kind: str,
|
|
119
|
+
peer: Optional[str] = None,
|
|
120
|
+
shard: Optional[str] = None,
|
|
121
|
+
transport: Optional[str] = None,
|
|
122
|
+
at: Optional[int] = None,
|
|
123
|
+
n: int = 1,
|
|
124
|
+
) -> Optional[DirectionTuple]:
|
|
125
|
+
"""One thing survived. Silently ignores a kind the digest does not name,
|
|
126
|
+
so a caller cannot invent a category that no reserve reads."""
|
|
127
|
+
if kind not in _KINDS:
|
|
128
|
+
return None
|
|
129
|
+
at_val = at if at is not None else self.clock()
|
|
130
|
+
d = ref_strain.direction(peer, shard, transport)
|
|
131
|
+
self.counts.setdefault(d, {})[kind] = self.counts.get(d, {}).get(kind, 0) + n
|
|
132
|
+
self.last.setdefault(d, {})[kind] = at_val
|
|
133
|
+
if self.accounts is not None:
|
|
134
|
+
self.accounts.put_strain(peer, shard, transport, kind, at_val, n)
|
|
135
|
+
return d
|
|
136
|
+
|
|
137
|
+
# ---- the direction rule ---------------------------------------------------
|
|
138
|
+
def by_direction(self, kind: Optional[str] = None) -> dict[DirectionTuple, int]:
|
|
139
|
+
"""direction -> how much stress came from it. Magnitude is collected
|
|
140
|
+
here and deliberately thrown away by `ref_strain.weigh`, which caps each
|
|
141
|
+
direction: one peer moves this node by at most one direction's worth."""
|
|
142
|
+
out: dict[DirectionTuple, int] = {}
|
|
143
|
+
for d, kinds in self.counts.items():
|
|
144
|
+
n = kinds.get(kind, 0) if kind else sum(kinds.values())
|
|
145
|
+
if n:
|
|
146
|
+
out[d] = n
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
def weight(self, kind: Optional[str] = None) -> float:
|
|
150
|
+
return ref_strain.weigh(self.by_direction(kind), self.tier_of)
|
|
151
|
+
|
|
152
|
+
def multiple(self, kind: Optional[str] = None) -> float:
|
|
153
|
+
return ref_strain.multiple(self.weight(kind))
|
|
154
|
+
|
|
155
|
+
def reserve(self, base: float | int, kind: Optional[str] = None) -> float:
|
|
156
|
+
return ref_strain.reserve(base, self.by_direction(kind), self.tier_of)
|
|
157
|
+
|
|
158
|
+
# ---- the compartment contract -------------------------------------------------
|
|
159
|
+
def tick(self, now: int) -> list[tuple[Any, ...]]:
|
|
160
|
+
return [] # it records what happens to it; it does not act
|
|
161
|
+
|
|
162
|
+
def trouble(self) -> list[str]:
|
|
163
|
+
ft = self.state().get('flag_tree')
|
|
164
|
+
if not ft:
|
|
165
|
+
return []
|
|
166
|
+
return [
|
|
167
|
+
'%.0f%% of the strain this node has seen is one direction (%s): it is '
|
|
168
|
+
'structurally dependent on it and will snap when it leaves'
|
|
169
|
+
% (
|
|
170
|
+
100 * self.state().get('dominant_share', 0),
|
|
171
|
+
' / '.join(x for x in (ft.get('direction') or []) if x)[:40],
|
|
172
|
+
)
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
# ---- the operator's view --------------------------------------------------
|
|
176
|
+
def flag_tree(self) -> FlagTreeReport:
|
|
177
|
+
key, share = ref_strain.flag_tree(self.by_direction())
|
|
178
|
+
return {'direction': list(key) if key else None, 'share': round(share, 4)}
|
|
179
|
+
|
|
180
|
+
def state(self) -> StrainStateReport:
|
|
181
|
+
by = self.by_direction()
|
|
182
|
+
totals: dict[str, int] = {}
|
|
183
|
+
for kinds in self.counts.values():
|
|
184
|
+
for k, n in kinds.items():
|
|
185
|
+
totals[k] = totals.get(k, 0) + n
|
|
186
|
+
key, share = ref_strain.flag_tree(by)
|
|
187
|
+
st: StrainStateReport = {
|
|
188
|
+
'kinds': totals,
|
|
189
|
+
'directions': len(by),
|
|
190
|
+
'weighted_directions': round(self.weight(), 4),
|
|
191
|
+
'reserve_multiple': round(self.multiple(), 3),
|
|
192
|
+
'dominant_share': round(share, 4),
|
|
193
|
+
}
|
|
194
|
+
if key is not None:
|
|
195
|
+
st['flag_tree'] = {
|
|
196
|
+
'direction': list(key),
|
|
197
|
+
'note': (
|
|
198
|
+
'nearly all the strain this node has seen comes from one direction, '
|
|
199
|
+
'so it is structurally dependent on it and will snap when it leaves. '
|
|
200
|
+
'This is not a finding about that peer, which has done nothing but be '
|
|
201
|
+
'the only one there'
|
|
202
|
+
),
|
|
203
|
+
}
|
|
204
|
+
return st
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Types and records for the Strain compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FlagTreeReport(TypedDict, total=False):
|
|
8
|
+
"""Dominant strain direction and its share."""
|
|
9
|
+
direction: Optional[list[str]]
|
|
10
|
+
share: float
|
|
11
|
+
note: Optional[str]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StrainStateReport(TypedDict, total=False):
|
|
15
|
+
"""The report Strain.state() gives of itself."""
|
|
16
|
+
kinds: dict[str, int]
|
|
17
|
+
directions: int
|
|
18
|
+
weighted_directions: float
|
|
19
|
+
reserve_multiple: float
|
|
20
|
+
dominant_share: float
|
|
21
|
+
flag_tree: Optional[FlagTreeReport]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""The operator's own decisions about peers, carried out once each.
|
|
2
|
+
|
|
3
|
+
One-module compartment: everything is in compartment.py.
|
|
4
|
+
"""
|
|
5
|
+
from .compartment import Trust
|
|
6
|
+
from .types import TrustDecisionDetail, TrustStandingRow, TrustStateReport
|
|
7
|
+
|
|
8
|
+
__all__ = ['Trust', 'TrustDecisionDetail', 'TrustStandingRow', 'TrustStateReport']
|
|
9
|
+
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""The operator's own decisions about peers, carried out once each.
|
|
2
|
+
|
|
3
|
+
A tier is only ever raised by a person. The ladder earns standing with observed
|
|
4
|
+
time; the specification says an operator MAY seed a tier for a node it knows out
|
|
5
|
+
of band, recorded as a signed event and reversible. `meshnode trust` writes that
|
|
6
|
+
decision to the config file and this compartment is what acts on it.
|
|
7
|
+
|
|
8
|
+
It ran inside the node's tick before, which meant a seed that could not be
|
|
9
|
+
applied - no peering yet, or a regrant that came back empty - was skipped in
|
|
10
|
+
silence. An operator who had written a decision and watched nothing happen had
|
|
11
|
+
no way to ask why. Every skip is a refusal here.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import dataclasses
|
|
16
|
+
from typing import Any, Callable, Mapping, Optional
|
|
17
|
+
|
|
18
|
+
from node.storage import grants as grants_mod
|
|
19
|
+
from node.compartment import stores as st
|
|
20
|
+
from node.channels import Channel, NOTE, TROUBLE
|
|
21
|
+
from node.metrics import COUNTER, Metric
|
|
22
|
+
from node.compartment import Command, Compartment, Need
|
|
23
|
+
from node.config.parameter import Parameter, ParamBlock
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
from .types import (
|
|
27
|
+
TrustStandingRow,
|
|
28
|
+
TrustStateReport,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def decision(entry: Any) -> tuple[str, int]:
|
|
33
|
+
"""A trust entry is {'tier': t, 'at': unix} - or, from before decisions
|
|
34
|
+
were dated, the bare tier, which counts as decided at time zero."""
|
|
35
|
+
if isinstance(entry, dict):
|
|
36
|
+
return entry.get('tier', 'probation'), int(entry.get('at') or 0)
|
|
37
|
+
return entry, 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def tiers(trust: Optional[Mapping[str, Any]]) -> dict[str, str]:
|
|
41
|
+
"""The policy's view: peer -> tier."""
|
|
42
|
+
return {pk: decision(e)[0] for pk, e in (trust or {}).items()}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclasses.dataclass(frozen=True)
|
|
46
|
+
class TrustConfig(ParamBlock):
|
|
47
|
+
key = 'trust'
|
|
48
|
+
name = 'trust'
|
|
49
|
+
DERIVED: bool = False
|
|
50
|
+
PARAMS = (
|
|
51
|
+
Parameter(key='max_decisions', name='Max Decisions', description='Maximum trust decisions remembered', type=int, default=4096, live=True, tags=('trust', 'policy')),
|
|
52
|
+
)
|
|
53
|
+
max_decisions: int = 4096
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Trust(Compartment):
|
|
57
|
+
|
|
58
|
+
name: str = 'trust'
|
|
59
|
+
reads: tuple[str, ...] = ('trust',)
|
|
60
|
+
live: tuple[str, ...] = ('trust',)
|
|
61
|
+
config_cls = TrustConfig
|
|
62
|
+
CHANNELS = (
|
|
63
|
+
Channel('trust_applied', 'Trust decision applied for peer', level=NOTE, say='trust decision applied for peer {peer}', tags=('trust', 'policy')),
|
|
64
|
+
Channel('trust_refused', 'Trust decision refused for peer', level=TROUBLE, say='trust decision refused for peer {peer}: {reason}', tags=('trust', 'policy')),
|
|
65
|
+
)
|
|
66
|
+
METRICS = (
|
|
67
|
+
Metric(name='trust_decisions_applied', description='Times trust decisions were applied.', kind=COUNTER, counts='trust_applied', tags=('trust', 'policy')),
|
|
68
|
+
Metric(name='trust_decisions_refused', description='Times trust decisions were refused.', kind=COUNTER, counts='trust_refused', tags=('trust', 'policy')),
|
|
69
|
+
)
|
|
70
|
+
NEEDS = (
|
|
71
|
+
Need('grant_store', 'stores.grants'),
|
|
72
|
+
Need('policy', 'policy'),
|
|
73
|
+
Need('regrant', 'regrant'),
|
|
74
|
+
)
|
|
75
|
+
COMMANDS = (
|
|
76
|
+
Command(
|
|
77
|
+
key='show',
|
|
78
|
+
name='Trust',
|
|
79
|
+
description='the decisions this operator has made, and which of them are still in '
|
|
80
|
+
'force: carried out is not the same as true, and the ladder can move a '
|
|
81
|
+
'seeded peer',
|
|
82
|
+
tags=('trust', 'status'),
|
|
83
|
+
permission='trust/show',
|
|
84
|
+
run='state',
|
|
85
|
+
reply='{decided, in_force, pending, overridden, waiting}',
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
STORES = (
|
|
89
|
+
st.Store(key='_said', description='refused decisions said once, per peer',
|
|
90
|
+
kind=st.TABLE, shape=tuple, bound=4096, keyed_by='peer', evict=st.LRU),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
grant_store: grants_mod.GrantStore,
|
|
96
|
+
policy: Any,
|
|
97
|
+
regrant: Callable[[str, str, int], Any],
|
|
98
|
+
clock: Optional[Callable[[], int]] = None,
|
|
99
|
+
node: Optional[Any] = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
super().__init__(node, clock)
|
|
102
|
+
self.grants: grants_mod.GrantStore = grant_store
|
|
103
|
+
self.policy: Any = policy
|
|
104
|
+
self.regrant: Callable[[str, str, int], Any] = regrant
|
|
105
|
+
self._said: st.Table[str, tuple[tuple[str, int], str]] # peer -> the decision already refused, so a wait is said once
|
|
106
|
+
|
|
107
|
+
def tick(self, now: int) -> list[tuple[Any, ...]]:
|
|
108
|
+
"""Carry out each dated decision in the config once.
|
|
109
|
+
|
|
110
|
+
A decision is compared with the record of decisions applied, kept
|
|
111
|
+
beside the grants, and not with the grant's tier: comparing tiers made
|
|
112
|
+
the seed re-issue `trusted` on the tick after every demotion the ladder
|
|
113
|
+
earned, so the ladder never held and every re-issue was one more
|
|
114
|
+
amend. A decision written while the node was down is still found on
|
|
115
|
+
the first tick back, because the record is on disk too. Applying a
|
|
116
|
+
seed also drops any grant the ladder had waiting for the boundary: the
|
|
117
|
+
operator's word is the later one.
|
|
118
|
+
"""
|
|
119
|
+
did: list[tuple[Any, ...]] = []
|
|
120
|
+
trust = self.config('trust') or {}
|
|
121
|
+
self.policy.tiers = tiers(trust)
|
|
122
|
+
for peer_pk, entry in trust.items():
|
|
123
|
+
tier, at = decision(entry)
|
|
124
|
+
if self.grants.seed_applied(peer_pk) == (tier, at):
|
|
125
|
+
continue
|
|
126
|
+
row = self.grants.get(peer_pk, grants_mod.ISSUED)
|
|
127
|
+
if row is None or row['status'] != 'live':
|
|
128
|
+
# No peering yet: the policy seeds the next one. Recorded once
|
|
129
|
+
# per decision and not once a tick - a refusal repeated every
|
|
130
|
+
# thirty seconds rolls the buffer that holds the real ones, and
|
|
131
|
+
# makes trouble() shout about a wait that is not a fault.
|
|
132
|
+
self._say(peer_pk, (tier, at), 'no live peering to amend', now)
|
|
133
|
+
continue
|
|
134
|
+
if row['payload'].get('tier') == tier and self.grants.pending(peer_pk) is None:
|
|
135
|
+
self.grants.put_seed(peer_pk, tier, at) # already so; nothing to re-issue
|
|
136
|
+
continue
|
|
137
|
+
if self.regrant(peer_pk, tier, now) is not None:
|
|
138
|
+
self.grants.put_seed(peer_pk, tier, at)
|
|
139
|
+
self._said.pop(peer_pk, None)
|
|
140
|
+
did.append(('granted', peer_pk, tier))
|
|
141
|
+
else:
|
|
142
|
+
self._say(peer_pk, (tier, at), 'the grant could not be re-issued', now)
|
|
143
|
+
return did
|
|
144
|
+
|
|
145
|
+
def _say(self, peer_pk: str, decided: tuple[str, int], why: str, now: int) -> None:
|
|
146
|
+
if self._said.get(peer_pk) == (decided, why):
|
|
147
|
+
return
|
|
148
|
+
self._said[peer_pk] = (decided, why)
|
|
149
|
+
self.refusals.refuse('seed', why, subject=peer_pk,
|
|
150
|
+
detail={'tier': decided[0], 'at': decided[1]}, at=now)
|
|
151
|
+
|
|
152
|
+
def standing(self) -> list[TrustStandingRow]:
|
|
153
|
+
"""Each decision, and what has become of it.
|
|
154
|
+
|
|
155
|
+
Three states, not two. `applied` used to mean "this dated decision was
|
|
156
|
+
carried out at some point", and an operator read it as "the tier I
|
|
157
|
+
chose is the tier in force". Those diverge the moment the ladder
|
|
158
|
+
demotes a seeded peer: the seed is recorded as applied, this compartment
|
|
159
|
+
correctly declines to re-issue it - re-issuing on every demotion is what
|
|
160
|
+
made the ladder never hold, and the dated record is the fix for that -
|
|
161
|
+
and the operator's decision quietly stops being true.
|
|
162
|
+
|
|
163
|
+
Seen on the mesh. A node seeded `trusted` was demoted to probation in a
|
|
164
|
+
run of S1 windows, and twelve hours later `trust` still said
|
|
165
|
+
`applied: 1, waiting: []` while every window ran under a probation
|
|
166
|
+
ceiling the operator had never chosen. Nothing was wrong with the
|
|
167
|
+
mechanism. The report was answering a different question from the one
|
|
168
|
+
being asked of it.
|
|
169
|
+
|
|
170
|
+
waiting decided, not carried out yet
|
|
171
|
+
pending carried out, and published at the next window boundary
|
|
172
|
+
in force carried out, and the grant says so
|
|
173
|
+
overridden carried out, and the ladder has since moved it
|
|
174
|
+
|
|
175
|
+
Only the last needs a person, which is why only the last is trouble.
|
|
176
|
+
`pending` exists because a decision no longer lands the moment it is
|
|
177
|
+
made: publishing a grant inside a running window breaks the subject's
|
|
178
|
+
receipt chain for the rest of it, so every decision waits for the
|
|
179
|
+
boundary. Without this state, the hour between deciding and landing
|
|
180
|
+
would read as overridden and shout for a person who need do nothing.
|
|
181
|
+
"""
|
|
182
|
+
out: list[TrustStandingRow] = []
|
|
183
|
+
for pk, entry in (self.config('trust') or {}).items():
|
|
184
|
+
tier, at = decision(entry)
|
|
185
|
+
row = self.grants.get(pk, grants_mod.ISSUED)
|
|
186
|
+
held = row['payload'].get('tier') if row else None
|
|
187
|
+
pend = self.grants.pending(pk)
|
|
188
|
+
if self.grants.seed_applied(pk) != (tier, at):
|
|
189
|
+
state = 'waiting'
|
|
190
|
+
elif held == tier:
|
|
191
|
+
state = 'in force'
|
|
192
|
+
elif pend is not None and (pend.get('payload') or {}).get('tier') == tier:
|
|
193
|
+
state = 'pending'
|
|
194
|
+
else:
|
|
195
|
+
state = 'overridden'
|
|
196
|
+
out.append({'peer': pk, 'tier': tier, 'at': at, 'held': held, 'state': state})
|
|
197
|
+
return out
|
|
198
|
+
|
|
199
|
+
def state(self) -> TrustStateReport:
|
|
200
|
+
rows = self.standing()
|
|
201
|
+
return {
|
|
202
|
+
'decided': {
|
|
203
|
+
r['peer'][:16]: {
|
|
204
|
+
'tier': r['tier'],
|
|
205
|
+
'at': r['at'],
|
|
206
|
+
'held': r['held'],
|
|
207
|
+
'state': r['state'],
|
|
208
|
+
}
|
|
209
|
+
for r in rows
|
|
210
|
+
},
|
|
211
|
+
'in_force': sum(1 for r in rows if r['state'] == 'in force'),
|
|
212
|
+
'pending': sorted(r['peer'][:16] for r in rows if r['state'] == 'pending'),
|
|
213
|
+
'overridden': sorted(r['peer'][:16] for r in rows if r['state'] == 'overridden'),
|
|
214
|
+
'waiting': sorted(r['peer'][:16] for r in rows if r['state'] == 'waiting'),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
def trouble(self) -> list[str]:
|
|
218
|
+
"""A decision waiting on a peering that does not exist yet is not
|
|
219
|
+
trouble - it is what a seed written before the peering looks like, and
|
|
220
|
+
the policy will apply it to the next handshake. A decision that reached
|
|
221
|
+
a live peering and could not be written is. So is one the ladder has
|
|
222
|
+
since overridden, because that one looks applied and is not in force,
|
|
223
|
+
and only a person can decide which of the two answers should stand.
|
|
224
|
+
"""
|
|
225
|
+
out: list[str] = []
|
|
226
|
+
stuck = sorted({(pk or '')[:16] for pk, (_, why) in self._said.items()
|
|
227
|
+
if why == 'the grant could not be re-issued'})
|
|
228
|
+
if stuck:
|
|
229
|
+
out.append('%d trust decision(s) reached a live peering and could not be written: %s. '
|
|
230
|
+
'The tier the operator decided on is not the tier in force'
|
|
231
|
+
% (len(stuck), ', '.join(stuck)))
|
|
232
|
+
gone = [r for r in self.standing() if r['state'] == 'overridden']
|
|
233
|
+
if gone:
|
|
234
|
+
out.append('%d trust decision(s) the ladder has overridden: %s. '
|
|
235
|
+
'Each was carried out and is no longer in force; re-deciding means '
|
|
236
|
+
'writing the tier again with a later date'
|
|
237
|
+
% (len(gone), ', '.join('%s decided %s, holds %s'
|
|
238
|
+
% (r['peer'][:16], r['tier'], r['held']) for r in gone)))
|
|
239
|
+
return out
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Types and records for the Trust compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TrustStandingRow(TypedDict, total=False):
|
|
8
|
+
"""One standing row from trust decisions."""
|
|
9
|
+
peer: str
|
|
10
|
+
tier: str
|
|
11
|
+
at: int
|
|
12
|
+
held: Optional[str]
|
|
13
|
+
state: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TrustDecisionDetail(TypedDict, total=False):
|
|
17
|
+
"""Decision detail for an evaluated peer."""
|
|
18
|
+
tier: str
|
|
19
|
+
at: int
|
|
20
|
+
held: Optional[str]
|
|
21
|
+
state: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TrustStateReport(TypedDict, total=False):
|
|
25
|
+
"""The report Trust.state() gives of itself."""
|
|
26
|
+
decided: dict[str, TrustDecisionDetail]
|
|
27
|
+
in_force: int
|
|
28
|
+
pending: list[str]
|
|
29
|
+
overridden: list[str]
|
|
30
|
+
waiting: list[str]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Services plane: applications, compute, settlement, paid fetch, sidecars, and data vending.
|
|
2
|
+
|
|
3
|
+
The 8 compartments of the services plane:
|
|
4
|
+
- gossip: group-topic epidemic fanout over the transport ladder
|
|
5
|
+
- compute: node-side lifecycle, offer publication and job handling
|
|
6
|
+
- compute_wire: routes compute/1.0 and 1.1 envelopes to ComputeSession
|
|
7
|
+
- settle: multi-rail settlement (Lightning hold invoices & Cashu Nutzap)
|
|
8
|
+
- paidfetch: pricing and payment gating for Blossom blob fetches
|
|
9
|
+
- sidecars: external service bridge and health monitoring
|
|
10
|
+
- dvm: NIP-90 Data Vending Machine compute gateway
|
|
11
|
+
- nsite: static site hosting and Blossom mirroring
|
|
12
|
+
"""
|
|
13
|
+
from .gossip import GossipMesh
|
|
14
|
+
from .compute import Compute
|
|
15
|
+
from .compute_wire import ComputeWire
|
|
16
|
+
from .settle import Settlement
|
|
17
|
+
from .paidfetch import PaidFetch
|
|
18
|
+
from .sidecars import Sidecars
|
|
19
|
+
from .dvm import DVMGateway
|
|
20
|
+
from .nsite import NsiteGateway
|
|
21
|
+
from .video import VideoTransitCompartment
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
'GossipMesh',
|
|
25
|
+
'Compute',
|
|
26
|
+
'ComputeWire',
|
|
27
|
+
'Settlement',
|
|
28
|
+
'PaidFetch',
|
|
29
|
+
'Sidecars',
|
|
30
|
+
'DVMGateway',
|
|
31
|
+
'NsiteGateway',
|
|
32
|
+
'VideoTransitCompartment',
|
|
33
|
+
]
|