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,1144 @@
|
|
|
1
|
+
"""One config block per compartment.
|
|
2
|
+
|
|
3
|
+
Each block is owned by the compartment that reads it and declared here so
|
|
4
|
+
NodeConfig can compose them and `Registry.assemble` can hand each compartment
|
|
5
|
+
its own, already checked. The fields are exactly the keys the compartment
|
|
6
|
+
reads today, with the defaults it already applied; nothing an operator has
|
|
7
|
+
written changes meaning. Seven of these blocks (announce, capacity, compute,
|
|
8
|
+
dvm, lan, transport, and most of bridge) had no default and no check before:
|
|
9
|
+
a misspelt key in them was silent.
|
|
10
|
+
|
|
11
|
+
Every block defaults to off or to zero: a node does nothing it was not asked
|
|
12
|
+
to, and charges nothing until a price is set.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import dataclasses
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any, Mapping, MutableMapping, Optional, Union
|
|
19
|
+
|
|
20
|
+
from fleetmesh_core.services import dvm as _ref_dvm
|
|
21
|
+
from .base import (Block, Problems, all_int, all_str, is_hex64, is_http_url, is_relay_url, non_negative_int, opt,
|
|
22
|
+
positive_int)
|
|
23
|
+
from . import parameter as par
|
|
24
|
+
from .parameter import Parameter, ParamBlock
|
|
25
|
+
|
|
26
|
+
# --- bridge --------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
DEFAULT_INGRESS_KINDS: list[int] = [
|
|
29
|
+
0, 1, 3, 6, 7, 1059, 1984, 1985,
|
|
30
|
+
11801, 11802, 11803, 21801, 21802,
|
|
31
|
+
30166, 30385, 30801, 30802, 30803, 30810, 30811,
|
|
32
|
+
]
|
|
33
|
+
DEFAULT_EGRESS_KINDS: list[int] = list(DEFAULT_INGRESS_KINDS)
|
|
34
|
+
DEFAULT_INGRESS_FILTERS: list[Mapping[str, Any]] = [{'kinds': DEFAULT_INGRESS_KINDS}]
|
|
35
|
+
DEFAULT_RATE_LIMIT_PER_MIN: int = 1200
|
|
36
|
+
DEFAULT_MAX_FUTURE_SECONDS: int = 900
|
|
37
|
+
DEFAULT_MAX_EVENT_BYTES: int = 256 * 1024
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclasses.dataclass(frozen=True)
|
|
41
|
+
class BridgeIngress(Block):
|
|
42
|
+
enabled: bool = True
|
|
43
|
+
filters: tuple[Mapping[str, Any], ...] = tuple(DEFAULT_INGRESS_FILTERS)
|
|
44
|
+
rate_limit: int = DEFAULT_RATE_LIMIT_PER_MIN
|
|
45
|
+
max_future_seconds: int = DEFAULT_MAX_FUTURE_SECONDS
|
|
46
|
+
max_bytes: int = DEFAULT_MAX_EVENT_BYTES
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
50
|
+
if not isinstance(raw, Mapping):
|
|
51
|
+
p.add('bridge.ingress must be an object'); return
|
|
52
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'bridge.ingress.enabled must be a boolean')
|
|
53
|
+
opt(raw, 'filters', lambda v: isinstance(v, list) and all(isinstance(f, dict) for f in v), p,
|
|
54
|
+
'bridge.ingress.filters must be a list of filter objects')
|
|
55
|
+
opt(raw, 'rate_limit', positive_int, p, 'bridge.ingress.rate_limit must be a positive integer')
|
|
56
|
+
opt(raw, 'max_future_seconds', non_negative_int, p, 'bridge.ingress.max_future_seconds must be a non-negative integer')
|
|
57
|
+
opt(raw, 'max_bytes', positive_int, p, 'bridge.ingress.max_bytes must be a positive integer')
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_dict(cls, raw: Any) -> 'BridgeIngress':
|
|
61
|
+
return cls(enabled=bool(raw.get('enabled', True)), filters=tuple(raw.get('filters', DEFAULT_INGRESS_FILTERS)),
|
|
62
|
+
rate_limit=int(raw.get('rate_limit', DEFAULT_RATE_LIMIT_PER_MIN)),
|
|
63
|
+
max_future_seconds=int(raw.get('max_future_seconds', DEFAULT_MAX_FUTURE_SECONDS)),
|
|
64
|
+
max_bytes=int(raw.get('max_bytes', DEFAULT_MAX_EVENT_BYTES)))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclasses.dataclass(frozen=True)
|
|
68
|
+
class BridgeEgress(Block):
|
|
69
|
+
enabled: bool = True
|
|
70
|
+
kinds: tuple[int, ...] = tuple(DEFAULT_EGRESS_KINDS)
|
|
71
|
+
auto: bool = True
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
75
|
+
if not isinstance(raw, Mapping):
|
|
76
|
+
p.add('bridge.egress must be an object'); return
|
|
77
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'bridge.egress.enabled must be a boolean')
|
|
78
|
+
opt(raw, 'kinds', all_int, p, 'bridge.egress.kinds must be a list of kind numbers')
|
|
79
|
+
opt(raw, 'auto', lambda v: isinstance(v, bool), p, 'bridge.egress.auto must be a boolean')
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, raw: Any) -> 'BridgeEgress':
|
|
83
|
+
return cls(enabled=bool(raw.get('enabled', True)), kinds=tuple(raw.get('kinds', DEFAULT_EGRESS_KINDS)),
|
|
84
|
+
auto=bool(raw.get('auto', True)))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
STRFRY_PLUGIN_REF = 'fleetmesh_strfry.plugin:StrfryBridgePlugin'
|
|
88
|
+
STRFRY_MOVED = ('bridge.strfry is no longer a part of the bridge: the strfry relay bridge is the '
|
|
89
|
+
'fleetmesh-strfry plugin. Remove the block and add %r to "plugins" (its url and '
|
|
90
|
+
'policy go under extensions.strfry), or run `python -m fleetmesh_strfry run` beside '
|
|
91
|
+
'the node.' % STRFRY_PLUGIN_REF)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclasses.dataclass(frozen=True)
|
|
95
|
+
class BridgeConfig(ParamBlock):
|
|
96
|
+
"""The public-Nostr bridge: which relays, what comes in, what goes out."""
|
|
97
|
+
key = 'bridge'
|
|
98
|
+
DERIVED = False
|
|
99
|
+
enabled: bool = True
|
|
100
|
+
relays: Optional[tuple[str, ...]] = None # None: the node's relays
|
|
101
|
+
ingress: BridgeIngress = BridgeIngress()
|
|
102
|
+
egress: BridgeEgress = BridgeEgress()
|
|
103
|
+
|
|
104
|
+
PARAMS = (
|
|
105
|
+
Parameter[bool](
|
|
106
|
+
key='enabled', name='Public Nostr Relay Bridge',
|
|
107
|
+
description='Bridge external standard Nostr relays with the local fleetmesh node. On by default.',
|
|
108
|
+
type=bool, default=True, must='be a boolean', live=True,
|
|
109
|
+
tags=('bridge', 'relay'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
110
|
+
Parameter[Optional[list]](
|
|
111
|
+
key='relays', name='External Relay URLs',
|
|
112
|
+
description='Designated external standard Nostr relays (ws:// or wss://) to bridge with.',
|
|
113
|
+
type=list, default=None, must='be a list of ws:// or wss:// URLs', live=True,
|
|
114
|
+
tags=('bridge', 'relay'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
119
|
+
if not switch_or_object('bridge', raw, p):
|
|
120
|
+
return
|
|
121
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'bridge.enabled must be a boolean')
|
|
122
|
+
opt(raw, 'relays', lambda v: isinstance(v, list) and all(is_relay_url(r) for r in v), p,
|
|
123
|
+
'bridge.relays must be a list of ws:// or wss:// URLs')
|
|
124
|
+
for sub, blk in (('ingress', BridgeIngress), ('egress', BridgeEgress)):
|
|
125
|
+
if sub in raw:
|
|
126
|
+
blk.check(raw[sub], p)
|
|
127
|
+
if 'strfry' in raw:
|
|
128
|
+
p.add(STRFRY_MOVED)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_dict(cls, raw: Any) -> 'BridgeConfig':
|
|
132
|
+
if isinstance(raw, bool):
|
|
133
|
+
return cls(enabled=raw)
|
|
134
|
+
return cls(enabled=bool(raw.get('enabled', True)),
|
|
135
|
+
relays=tuple(raw['relays']) if raw.get('relays') is not None else None,
|
|
136
|
+
ingress=BridgeIngress.from_dict(raw.get('ingress') or {}),
|
|
137
|
+
egress=BridgeEgress.from_dict(raw.get('egress') or {}))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- the sidecar compartments -------------------------------------------
|
|
141
|
+
|
|
142
|
+
@dataclasses.dataclass(frozen=True)
|
|
143
|
+
class AnnounceConfig(ParamBlock):
|
|
144
|
+
"""The libp2p gossip announce sidecar: off unless enabled."""
|
|
145
|
+
key = 'announce'
|
|
146
|
+
enabled: bool = False
|
|
147
|
+
socket: Optional[str] = None
|
|
148
|
+
pow_bits: int = 0
|
|
149
|
+
|
|
150
|
+
PARAMS = (
|
|
151
|
+
Parameter[bool](
|
|
152
|
+
key='enabled', name='Gossipsub announce',
|
|
153
|
+
description='Republish the descriptor on the gossipsub announce topic through the sidecar. '
|
|
154
|
+
'Off unless enabled.',
|
|
155
|
+
type=bool, default=False, must='be a boolean', live=True,
|
|
156
|
+
tags=('announce', 'discovery'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
157
|
+
Parameter[Optional[str]](
|
|
158
|
+
key='socket', name='Sidecar socket path',
|
|
159
|
+
description='Path to the unix domain socket the announce sidecar listens on. '
|
|
160
|
+
'Defaults to announce.sock in the node directory.',
|
|
161
|
+
type=str, default=None, must='be a path', live=True,
|
|
162
|
+
coerce=lambda v: str(v) if v is not None else None,
|
|
163
|
+
tags=('announce', 'sidecar'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
164
|
+
Parameter[int](
|
|
165
|
+
key='pow_bits', name='Proof of work floor',
|
|
166
|
+
description='Minimum proof-of-work bits required for descriptors heard on the announce topic.',
|
|
167
|
+
type=int, default=0, minimum=0, maximum=40,
|
|
168
|
+
must='be an integer from 0 to 40', live=True,
|
|
169
|
+
coerce=lambda v: int(v) if isinstance(v, (int, str)) and str(v).lstrip('-').isdigit() else 0,
|
|
170
|
+
tags=('announce', 'pow'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclasses.dataclass(frozen=True)
|
|
175
|
+
class LanConfig(ParamBlock):
|
|
176
|
+
"""mDNS discovery on the local network: off unless enabled.
|
|
177
|
+
|
|
178
|
+
Note what this block does NOT do: presence does not turn it on. `p2p`
|
|
179
|
+
below looks identical and does. See ParamBlock.PRESENCE_ENABLES.
|
|
180
|
+
"""
|
|
181
|
+
key = 'lan'
|
|
182
|
+
enabled: bool = False
|
|
183
|
+
|
|
184
|
+
PARAMS = (
|
|
185
|
+
Parameter[bool](
|
|
186
|
+
key='enabled', name='mDNS discovery',
|
|
187
|
+
description='Announce and discover peers over mDNS on the local network. Off '
|
|
188
|
+
'unless set, and unlike p2p the presence of the block alone does '
|
|
189
|
+
'not turn it on.',
|
|
190
|
+
type=bool, default=False, must='be a boolean', live=True,
|
|
191
|
+
tags=('lan', 'discovery'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclasses.dataclass(frozen=True)
|
|
196
|
+
class P2PConfig(ParamBlock):
|
|
197
|
+
"""The libp2p transport sidecar. The key's presence turns it on; the
|
|
198
|
+
block may be `true` or an object.
|
|
199
|
+
|
|
200
|
+
`PRESENCE_ENABLES` is what that sentence means, and it is load-bearing:
|
|
201
|
+
the field default is False but `from_dict({})` yields True, because a
|
|
202
|
+
node that mentions p2p at all wants it. Before this the difference lived
|
|
203
|
+
only in the body of from_dict, where it looked like a typo.
|
|
204
|
+
"""
|
|
205
|
+
key = 'p2p'
|
|
206
|
+
SWITCHABLE = True
|
|
207
|
+
PRESENCE_ENABLES = 'enabled'
|
|
208
|
+
enabled: bool = False
|
|
209
|
+
|
|
210
|
+
PARAMS = (
|
|
211
|
+
Parameter[bool](
|
|
212
|
+
key='enabled', name='libp2p transport',
|
|
213
|
+
description='Run the libp2p transport sidecar. True as soon as the block is '
|
|
214
|
+
'present; write false to name the block and leave it off.',
|
|
215
|
+
type=bool, default=False, must='be a boolean',
|
|
216
|
+
tags=('p2p', 'transport'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# --- capacity -------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
@dataclasses.dataclass(frozen=True)
|
|
223
|
+
class CapacityConfig(ParamBlock):
|
|
224
|
+
"""What this node has to give, {resource: amount}; only positive numbers count.
|
|
225
|
+
|
|
226
|
+
`DERIVED = False`: the raw form is the mapping itself, with no field names
|
|
227
|
+
in the file at all, so `check` and `from_dict` stay hand-written. The
|
|
228
|
+
parameter below is declared anyway so the census is complete - a setting
|
|
229
|
+
`every_parameter()` cannot show is a setting an operator cannot find.
|
|
230
|
+
"""
|
|
231
|
+
key = 'capacity'
|
|
232
|
+
DERIVED = False
|
|
233
|
+
total: dict[str, float] = dataclasses.field(default_factory=dict)
|
|
234
|
+
|
|
235
|
+
PARAMS = (
|
|
236
|
+
Parameter[dict](
|
|
237
|
+
key='total', name='Offered capacity',
|
|
238
|
+
description='What this node has to give, as {resource: amount}. The block is '
|
|
239
|
+
'the mapping itself rather than an object containing one. Only '
|
|
240
|
+
'positive numbers are kept.',
|
|
241
|
+
type=dict, default=dict, must='be an object of {resource: amount}',
|
|
242
|
+
# live: the Capacity compartment re-reads this on every tick, which
|
|
243
|
+
# is why it declares `live = ('capacity',)`. The derivation in
|
|
244
|
+
# Compartment.__init_subclass__ caught this parameter saying
|
|
245
|
+
# otherwise on the first run of the cross-check.
|
|
246
|
+
live=True, tags=('capacity', 'offer'),
|
|
247
|
+
sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
252
|
+
if not isinstance(raw, Mapping):
|
|
253
|
+
p.add('capacity must be an object of {resource: amount}'); return
|
|
254
|
+
for k, v in raw.items():
|
|
255
|
+
if not isinstance(k, str) or isinstance(v, bool) or not isinstance(v, (int, float)):
|
|
256
|
+
p.add('capacity[%s] must be a number' % str(k)[:24])
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def from_dict(cls, raw: Any) -> 'CapacityConfig':
|
|
260
|
+
return cls(total={k: v for k, v in (raw or {}).items() if isinstance(v, (int, float)) and v > 0})
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclasses.dataclass(frozen=True)
|
|
264
|
+
class GossipConfig(ParamBlock):
|
|
265
|
+
"""Group-topic epidemic gossip (graft/prune/publish mesh). The key's
|
|
266
|
+
presence turns it on; the block may be `true` or an object.
|
|
267
|
+
|
|
268
|
+
All four defaults were written twice before this - once in the fields and
|
|
269
|
+
once in `from_dict` - which is the duplication `Parameter` exists to end.
|
|
270
|
+
"""
|
|
271
|
+
key = 'gossip'
|
|
272
|
+
SWITCHABLE = True
|
|
273
|
+
PRESENCE_ENABLES = 'enabled'
|
|
274
|
+
enabled: bool = False
|
|
275
|
+
mesh_degree: int = 6
|
|
276
|
+
seen_ttl_seconds: int = 24 * 3600
|
|
277
|
+
max_topics: int = 64
|
|
278
|
+
|
|
279
|
+
# The ceilings the two settings below may be raised to, and no further.
|
|
280
|
+
# GossipMesh sizes its stores from these (node/stores: a bound is fixed
|
|
281
|
+
# on the class), so a config that asked for more topics than the store
|
|
282
|
+
# holds would be refused at the store with a message about the store,
|
|
283
|
+
# not the setting. Refused here instead, naming the setting.
|
|
284
|
+
TOPIC_CEILING = 1024
|
|
285
|
+
MESH_DEGREE_CEILING = 64
|
|
286
|
+
|
|
287
|
+
PARAMS = (
|
|
288
|
+
Parameter[bool](
|
|
289
|
+
key='enabled', name='Epidemic gossip',
|
|
290
|
+
description='Run the group-topic gossip mesh. True as soon as the block is '
|
|
291
|
+
'present; write false to name the block and leave it off.',
|
|
292
|
+
type=bool, default=False, must='be a boolean',
|
|
293
|
+
tags=('gossip',), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
294
|
+
Parameter[int](
|
|
295
|
+
key='mesh_degree', name='Mesh degree',
|
|
296
|
+
description='How many peers a topic mesh keeps grafted.',
|
|
297
|
+
type=int, default=6, minimum=1, maximum=MESH_DEGREE_CEILING,
|
|
298
|
+
must='be a positive integer, at most %d' % MESH_DEGREE_CEILING,
|
|
299
|
+
tags=('gossip', 'topology'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
300
|
+
Parameter[int](
|
|
301
|
+
key='seen_ttl_seconds', name='Seen TTL',
|
|
302
|
+
description='How long a seen event id is remembered, to suppress a re-flood.',
|
|
303
|
+
type=int, default=24 * 3600, minimum=1, must='be a positive integer',
|
|
304
|
+
# Not live: the value is handed to SeenStore at construction
|
|
305
|
+
# (gossip_wire/core.py), so a reload does not reach it. Claiming
|
|
306
|
+
# otherwise would be the report-what-you-do-not-do defect again.
|
|
307
|
+
tags=('gossip', 'retention'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
308
|
+
Parameter[int](
|
|
309
|
+
key='max_topics', name='Topic ceiling',
|
|
310
|
+
description='How many topics this node will carry at once.',
|
|
311
|
+
type=int, default=64, minimum=1, maximum=TOPIC_CEILING,
|
|
312
|
+
must='be a positive integer, at most %d' % TOPIC_CEILING,
|
|
313
|
+
tags=('gossip', 'capacity'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# --- compute, dvm, settlement, mediation ----------------------------------
|
|
318
|
+
|
|
319
|
+
def switch_or_object(name: str, raw: Any, p: Problems) -> bool:
|
|
320
|
+
"""An optional compartment is turned on by its key's presence, so the
|
|
321
|
+
block may be plain `true` (on, every default) as well as an object.
|
|
322
|
+
Returns whether `raw` is an object the caller should go on checking."""
|
|
323
|
+
if isinstance(raw, bool):
|
|
324
|
+
return False
|
|
325
|
+
if not isinstance(raw, Mapping):
|
|
326
|
+
p.add('%s must be true, false or an object' % name)
|
|
327
|
+
return False
|
|
328
|
+
return True
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclasses.dataclass(frozen=True)
|
|
332
|
+
class ComputeConfig(ParamBlock):
|
|
333
|
+
"""The compute market. `mock_mode` runs the executors as stubs."""
|
|
334
|
+
key = 'compute'
|
|
335
|
+
SWITCHABLE = True
|
|
336
|
+
mock_mode: bool = False
|
|
337
|
+
zk_pinned_vks: tuple[str, ...] = () # Groth16 verification-key digests the operator pins
|
|
338
|
+
|
|
339
|
+
PARAMS = (
|
|
340
|
+
Parameter[bool](
|
|
341
|
+
key='mock_mode', name='Mock executors',
|
|
342
|
+
description='Run the compute executors as stubs instead of executing anything. '
|
|
343
|
+
'A node in this state sells work it does not do, so it is a host '
|
|
344
|
+
'decision and never a remote one.',
|
|
345
|
+
type=bool, default=False, must='be a boolean',
|
|
346
|
+
tags=('compute', 'testing'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
347
|
+
Parameter[tuple](
|
|
348
|
+
key='zk_pinned_vks', name='Pinned verification keys',
|
|
349
|
+
description='Groth16 verification-key digests this operator pins. A proof '
|
|
350
|
+
'verifying against an unpinned key is not accepted.',
|
|
351
|
+
type=tuple, item=str, default=(), must='be a list of 64-hex digests',
|
|
352
|
+
verify_regex=r'[0-9a-fA-F]{64}',
|
|
353
|
+
coerce=lambda v: tuple(str(x).lower() for x in v),
|
|
354
|
+
# A trust anchor: what this node will believe a proof against. Not
|
|
355
|
+
# from a manifest, for the reason updates.release_key is not.
|
|
356
|
+
tags=('compute', 'zk', 'trust'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
DEFAULT_DVM_KINDS: list[int] = sorted({*_ref_dvm.DEFAULT_KINDS, _ref_dvm.TRANSCODE_KIND}) # fleetmesh_core/dvm.py owns the default + NIP-90 5201 transcode
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@dataclasses.dataclass(frozen=True)
|
|
364
|
+
class DVMConfig(ParamBlock):
|
|
365
|
+
"""NIP-90 data-vending: which kinds, what a mega-fuel costs, the fuel cap."""
|
|
366
|
+
key = 'dvm'
|
|
367
|
+
DERIVED = False
|
|
368
|
+
enabled: bool = True
|
|
369
|
+
supported_kinds: tuple[int, ...] = tuple(DEFAULT_DVM_KINDS)
|
|
370
|
+
rate_msat_per_mfuel: int = 45
|
|
371
|
+
max_fuel: int = 50_000_000_000
|
|
372
|
+
blossom_servers: tuple[str, ...] = ()
|
|
373
|
+
delegate_to_mesh: bool = False
|
|
374
|
+
|
|
375
|
+
PARAMS = (
|
|
376
|
+
Parameter[bool](
|
|
377
|
+
key='enabled', name='DVM Gateway',
|
|
378
|
+
description='Enable the NIP-90 Data Vending Machine gateway.',
|
|
379
|
+
type=bool, default=True, must='be a boolean', live=True,
|
|
380
|
+
tags=('dvm', 'gateway'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
381
|
+
Parameter[tuple](
|
|
382
|
+
key='supported_kinds', name='Supported DVM Kinds',
|
|
383
|
+
description='NIP-90 job request kinds this DVM node handles.',
|
|
384
|
+
type=tuple, default=tuple, must='be a list of integer kind numbers', live=True,
|
|
385
|
+
tags=('dvm', 'kinds'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
386
|
+
Parameter[int](
|
|
387
|
+
key='rate_msat_per_mfuel', name='Rate msat per Mfuel',
|
|
388
|
+
description='Pricing in millisatoshis per mega-fuel unit.',
|
|
389
|
+
type=int, default=45, must='be a non-negative integer', live=True,
|
|
390
|
+
tags=('dvm', 'pricing'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
391
|
+
Parameter[int](
|
|
392
|
+
key='max_fuel', name='Maximum Fuel Limit',
|
|
393
|
+
description='Upper bound on fuel consumption per job.',
|
|
394
|
+
type=int, default=50_000_000_000, must='be a positive integer', live=True,
|
|
395
|
+
tags=('dvm', 'limits'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
396
|
+
Parameter[tuple](
|
|
397
|
+
key='blossom_servers', name='Blossom Servers',
|
|
398
|
+
description='Blossom servers used for DVM input/output blob transfers.',
|
|
399
|
+
type=tuple, default=tuple, must='be a list of http(s) URLs', live=True,
|
|
400
|
+
tags=('dvm', 'blossom'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
401
|
+
Parameter[bool](
|
|
402
|
+
key='delegate_to_mesh', name='Delegate to Mesh',
|
|
403
|
+
description='Whether to delegate requests to mesh compute order book.',
|
|
404
|
+
type=bool, default=False, must='be a boolean', live=True,
|
|
405
|
+
tags=('dvm', 'delegation'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
@classmethod
|
|
409
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
410
|
+
if not switch_or_object('dvm', raw, p):
|
|
411
|
+
return
|
|
412
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'dvm.enabled must be a boolean')
|
|
413
|
+
opt(raw, 'delegate_to_mesh', lambda v: isinstance(v, bool), p, 'dvm.delegate_to_mesh must be a boolean')
|
|
414
|
+
opt(raw, 'supported_kinds', all_int, p, 'dvm.supported_kinds must be a list of kind numbers')
|
|
415
|
+
opt(raw, 'rate_msat_per_mfuel', non_negative_int, p, 'dvm.rate_msat_per_mfuel must be a non-negative integer')
|
|
416
|
+
opt(raw, 'max_fuel', positive_int, p, 'dvm.max_fuel must be a positive integer')
|
|
417
|
+
opt(raw, 'blossom_servers', lambda v: isinstance(v, list) and all(is_http_url(s) for s in v), p,
|
|
418
|
+
'dvm.blossom_servers must be a list of http(s) URLs')
|
|
419
|
+
|
|
420
|
+
@classmethod
|
|
421
|
+
def from_dict(cls, raw: Any) -> 'DVMConfig':
|
|
422
|
+
if isinstance(raw, bool):
|
|
423
|
+
return cls()
|
|
424
|
+
return cls(enabled=bool(raw.get('enabled', True)), supported_kinds=tuple(raw.get('supported_kinds', DEFAULT_DVM_KINDS)),
|
|
425
|
+
rate_msat_per_mfuel=int(raw.get('rate_msat_per_mfuel', 45)), max_fuel=int(raw.get('max_fuel', 50_000_000_000)),
|
|
426
|
+
blossom_servers=tuple(raw.get('blossom_servers') or ()),
|
|
427
|
+
delegate_to_mesh=bool(raw.get('delegate_to_mesh', False)))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@dataclasses.dataclass(frozen=True)
|
|
431
|
+
class NsiteConfig(ParamBlock):
|
|
432
|
+
"""NIP-5A static-site gateway: which hostnames are sites, where blobs
|
|
433
|
+
come from when the author's servers are silent, how much to cache, and
|
|
434
|
+
whether a custom domain must be claimed in the manifest.
|
|
435
|
+
|
|
436
|
+
`suffixes`: hostnames under which the left-most label is a site
|
|
437
|
+
(`npub1….nsite.example.org`). `custom_domains`: serve a domain whose
|
|
438
|
+
`_nsite.<domain>` TXT names a site. `require_domain_tag`: that site's
|
|
439
|
+
manifest must also carry `["domain", "<domain>"]` - on by default so a
|
|
440
|
+
DNS record alone cannot front someone else's site. `allow_domains` turns
|
|
441
|
+
the gateway closed (only these), `deny_domains` blocks named ones.
|
|
442
|
+
Quotas are per cached blob, per site and for the whole cache.
|
|
443
|
+
"""
|
|
444
|
+
key = 'nsite'
|
|
445
|
+
DERIVED = False
|
|
446
|
+
enabled: bool = True
|
|
447
|
+
suffixes: tuple[str, ...] = ()
|
|
448
|
+
custom_domains: bool = True
|
|
449
|
+
require_domain_tag: bool = True
|
|
450
|
+
allow_domains: tuple[str, ...] = ()
|
|
451
|
+
deny_domains: tuple[str, ...] = ()
|
|
452
|
+
fallback_servers: tuple[str, ...] = ()
|
|
453
|
+
relays: tuple[str, ...] = ()
|
|
454
|
+
allow_legacy_kind: bool = False
|
|
455
|
+
frame_ancestors: tuple[str, ...] = ()
|
|
456
|
+
cross_origin_isolation: bool = False
|
|
457
|
+
max_blob_bytes: int = 16 * 1024 * 1024
|
|
458
|
+
max_site_bytes: int = 256 * 1024 * 1024
|
|
459
|
+
max_cache_bytes: int = 4 * 1024 * 1024 * 1024
|
|
460
|
+
manifest_ttl_seconds: int = 300
|
|
461
|
+
domain_ttl_seconds: int = 6 * 3600
|
|
462
|
+
fetch_timeout: float = 10.0
|
|
463
|
+
|
|
464
|
+
PARAMS = (
|
|
465
|
+
Parameter[bool](
|
|
466
|
+
key='enabled', name='Nsite Gateway',
|
|
467
|
+
description='Enable NIP-5A static-site hosting gateway.',
|
|
468
|
+
type=bool, default=True, must='be a boolean', live=True,
|
|
469
|
+
tags=('nsite', 'gateway'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
470
|
+
Parameter[tuple](
|
|
471
|
+
key='suffixes', name='Gateway Hostname Suffixes',
|
|
472
|
+
description='Host suffixes under which subdomains are routed to nsite static sites.',
|
|
473
|
+
type=tuple, default=tuple, must='be a list of host names', live=True,
|
|
474
|
+
tags=('nsite', 'routing'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
475
|
+
Parameter[bool](
|
|
476
|
+
key='custom_domains', name='Custom Domains Support',
|
|
477
|
+
description='Allow custom domains via CNAME and _nsite DNS TXT records.',
|
|
478
|
+
type=bool, default=True, must='be a boolean', live=True,
|
|
479
|
+
tags=('nsite', 'dns'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
480
|
+
Parameter[bool](
|
|
481
|
+
key='require_domain_tag', name='Require Domain Tag in Manifest',
|
|
482
|
+
description='Require site manifest to carry a matching domain tag to bind.',
|
|
483
|
+
type=bool, default=True, must='be a boolean', live=True,
|
|
484
|
+
tags=('nsite', 'security'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
@classmethod
|
|
488
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
489
|
+
if not switch_or_object('nsite', raw, p):
|
|
490
|
+
return
|
|
491
|
+
for k in ('enabled', 'custom_domains', 'require_domain_tag', 'allow_legacy_kind', 'cross_origin_isolation'):
|
|
492
|
+
opt(raw, k, lambda v: isinstance(v, bool), p, 'nsite.%s must be a boolean' % k)
|
|
493
|
+
for k in ('suffixes', 'allow_domains', 'deny_domains', 'frame_ancestors'):
|
|
494
|
+
opt(raw, k, all_str, p, 'nsite.%s must be a list of host names' % k)
|
|
495
|
+
opt(raw, 'fallback_servers', lambda v: isinstance(v, list) and all(is_http_url(s) for s in v), p,
|
|
496
|
+
'nsite.fallback_servers must be a list of http(s) URLs')
|
|
497
|
+
opt(raw, 'relays', lambda v: isinstance(v, list) and all(is_relay_url(s) for s in v), p,
|
|
498
|
+
'nsite.relays must be a list of ws(s) URLs')
|
|
499
|
+
for k in ('max_blob_bytes', 'max_site_bytes', 'max_cache_bytes', 'manifest_ttl_seconds', 'domain_ttl_seconds'):
|
|
500
|
+
opt(raw, k, positive_int, p, 'nsite.%s must be a positive integer' % k)
|
|
501
|
+
opt(raw, 'fetch_timeout', lambda v: isinstance(v, (int, float)) and not isinstance(v, bool) and v > 0, p,
|
|
502
|
+
'nsite.fetch_timeout must be a positive number of seconds')
|
|
503
|
+
if isinstance(raw, dict) and raw.get('enabled', True) and not raw.get('suffixes') and not raw.get('custom_domains', True):
|
|
504
|
+
p.add('nsite: with no suffixes and custom_domains off the gateway can serve nothing')
|
|
505
|
+
|
|
506
|
+
@classmethod
|
|
507
|
+
def from_dict(cls, raw: Any) -> 'NsiteConfig':
|
|
508
|
+
if isinstance(raw, bool):
|
|
509
|
+
return cls()
|
|
510
|
+
g = lambda k, d: raw.get(k, d) # noqa: E731
|
|
511
|
+
return cls(enabled=bool(g('enabled', True)), suffixes=tuple(g('suffixes', ()) or ()),
|
|
512
|
+
custom_domains=bool(g('custom_domains', True)), require_domain_tag=bool(g('require_domain_tag', True)),
|
|
513
|
+
allow_domains=tuple(g('allow_domains', ()) or ()), deny_domains=tuple(g('deny_domains', ()) or ()),
|
|
514
|
+
fallback_servers=tuple(g('fallback_servers', ()) or ()), relays=tuple(g('relays', ()) or ()),
|
|
515
|
+
allow_legacy_kind=bool(g('allow_legacy_kind', False)),
|
|
516
|
+
frame_ancestors=tuple(g('frame_ancestors', ()) or ()),
|
|
517
|
+
cross_origin_isolation=bool(g('cross_origin_isolation', False)),
|
|
518
|
+
max_blob_bytes=int(g('max_blob_bytes', cls.max_blob_bytes)),
|
|
519
|
+
max_site_bytes=int(g('max_site_bytes', cls.max_site_bytes)),
|
|
520
|
+
max_cache_bytes=int(g('max_cache_bytes', cls.max_cache_bytes)),
|
|
521
|
+
manifest_ttl_seconds=int(g('manifest_ttl_seconds', cls.manifest_ttl_seconds)),
|
|
522
|
+
domain_ttl_seconds=int(g('domain_ttl_seconds', cls.domain_ttl_seconds)),
|
|
523
|
+
fetch_timeout=float(g('fetch_timeout', cls.fetch_timeout)))
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@dataclasses.dataclass(frozen=True)
|
|
527
|
+
class SettlementConfig(ParamBlock):
|
|
528
|
+
"""How compute is paid for: a NWC wallet, or a live Cashu mint."""
|
|
529
|
+
key = 'settlement'
|
|
530
|
+
nwc_uri: Optional[str] = None
|
|
531
|
+
live_cashu: bool = False
|
|
532
|
+
mint_url: Optional[str] = None # None: payment.DEFAULT_MINT_URL
|
|
533
|
+
|
|
534
|
+
PARAMS = (
|
|
535
|
+
Parameter[Optional[str]](
|
|
536
|
+
key='nwc_uri', name='Wallet connection',
|
|
537
|
+
description='A Nostr Wallet Connect URI this node pays and is paid through.',
|
|
538
|
+
type=str, default=None, must='be a nostr+walletconnect:// URI',
|
|
539
|
+
verify_regex=r'nostr\+walletconnect://.+',
|
|
540
|
+
# A credential. `secret` keeps it out of every report and every
|
|
541
|
+
# audit row; host-only sources keep it out of anything that could
|
|
542
|
+
# set it remotely.
|
|
543
|
+
secret=True, tags=('settlement', 'money', 'credential'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
544
|
+
Parameter[bool](
|
|
545
|
+
key='live_cashu', name='Live Cashu mint',
|
|
546
|
+
description='Use a real Cashu mint rather than the offline token path. Real '
|
|
547
|
+
'money moves when this is on.',
|
|
548
|
+
type=bool, default=False, must='be a boolean',
|
|
549
|
+
tags=('settlement', 'money'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
550
|
+
Parameter[Optional[str]](
|
|
551
|
+
key='mint_url', name='Mint URL',
|
|
552
|
+
description='The Cashu mint to use. Unset means payment.DEFAULT_MINT_URL.',
|
|
553
|
+
type=str, default=None, must='be an http(s) URL',
|
|
554
|
+
verify_regex=r'https?://.+',
|
|
555
|
+
tags=('settlement', 'money'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@dataclasses.dataclass(frozen=True)
|
|
560
|
+
class MediationConfig(ParamBlock):
|
|
561
|
+
"""The mediator / leaf-phone compartment. Reserved: no operator keys yet.
|
|
562
|
+
|
|
563
|
+
An empty PARAMS is the correct and final answer here, not work left
|
|
564
|
+
over: `converted()` asks whether a block states its settings as
|
|
565
|
+
parameters, not whether it has any.
|
|
566
|
+
"""
|
|
567
|
+
key = 'mediation'
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@dataclasses.dataclass(frozen=True)
|
|
571
|
+
class TransportConfig(ParamBlock):
|
|
572
|
+
"""compute-wire transport preferences. Reserved: no operator keys yet."""
|
|
573
|
+
key = 'transport'
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
# --- blobs ------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
BLOB_WIRE_CAPS: tuple[str, ...] = ('bytes_per_day', 'max_concurrent', 'max_blob_bytes', 'max_total_bytes')
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@dataclasses.dataclass(frozen=True)
|
|
582
|
+
class BlobWireConfig(ParamBlock):
|
|
583
|
+
"""blob/1.0: who this node accepts offers from, the chunk size, the caps.
|
|
584
|
+
|
|
585
|
+
`accept_from` is `none`, `grantees`, or a list of pubkeys. The caps are
|
|
586
|
+
per peer and default to the window constants (node/blob_wire.py reads
|
|
587
|
+
them from window.C, so they are None here and resolved there): zero is
|
|
588
|
+
"no bytes", not "unlimited".
|
|
589
|
+
"""
|
|
590
|
+
key = 'blob_wire'
|
|
591
|
+
accept_from: Union[str, tuple[str, ...]] = 'none'
|
|
592
|
+
chunk_bytes: Optional[int] = None
|
|
593
|
+
bytes_per_day: Optional[int] = None
|
|
594
|
+
max_concurrent: Optional[int] = None
|
|
595
|
+
max_blob_bytes: Optional[int] = None
|
|
596
|
+
max_total_bytes: Optional[int] = None
|
|
597
|
+
DERIVED: bool = False
|
|
598
|
+
PARAMS = (
|
|
599
|
+
Parameter(key='accept_from', name='Accept From', description='Whom to accept offers from', type=str, default='none', tags=('blob', 'wire')),
|
|
600
|
+
Parameter(key='chunk_bytes', name='Chunk Bytes', description='Transfer chunk size', type=int, default=None, tags=('blob', 'wire')),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
@classmethod
|
|
604
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
605
|
+
if not isinstance(raw, Mapping):
|
|
606
|
+
p.add('blob_wire must be an object'); return
|
|
607
|
+
opt(raw, 'accept_from', lambda v: v in ('none', 'grantees') or (isinstance(v, list) and all(is_hex64(x) for x in v)),
|
|
608
|
+
p, 'blob_wire.accept_from must be none, grantees, or a list of 64-hex pubkeys')
|
|
609
|
+
opt(raw, 'chunk_bytes', positive_int, p, 'blob_wire.chunk_bytes must be a positive integer')
|
|
610
|
+
for k in BLOB_WIRE_CAPS:
|
|
611
|
+
opt(raw, k, non_negative_int, p, 'blob_wire.%s must be a non-negative integer' % k)
|
|
612
|
+
|
|
613
|
+
@classmethod
|
|
614
|
+
def from_dict(cls, raw: Any) -> 'BlobWireConfig':
|
|
615
|
+
af = raw.get('accept_from', 'none')
|
|
616
|
+
return cls(accept_from=tuple(str(x).lower() for x in af) if isinstance(af, list) else af,
|
|
617
|
+
chunk_bytes=raw.get('chunk_bytes'), **{k: raw.get(k) for k in BLOB_WIRE_CAPS})
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@dataclasses.dataclass(frozen=True)
|
|
621
|
+
class BlobReplicationConfig(ParamBlock):
|
|
622
|
+
"""How many copies of each blob to keep across partners, and where from."""
|
|
623
|
+
key = 'blob_replication'
|
|
624
|
+
copies: int = 2
|
|
625
|
+
allow_private: Union[bool, tuple[str, ...]] = False # true, or the hosts a LAN cluster allows
|
|
626
|
+
source_url: Optional[str] = None
|
|
627
|
+
servers: tuple[str, ...] = ()
|
|
628
|
+
DERIVED: bool = False
|
|
629
|
+
PARAMS = (
|
|
630
|
+
Parameter(key='copies', name='Target Copies', description='Target copy count across network', type=int, default=2, tags=('blob', 'replication')),
|
|
631
|
+
Parameter(key='allow_private', name='Allow Private', description='Allow private LAN addresses', type=bool, default=False, tags=('blob', 'replication')),
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
@classmethod
|
|
635
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
636
|
+
if not isinstance(raw, Mapping):
|
|
637
|
+
p.add('blob_replication must be an object'); return
|
|
638
|
+
opt(raw, 'copies', non_negative_int, p, 'blob_replication.copies must be a non-negative integer')
|
|
639
|
+
opt(raw, 'allow_private', lambda v: isinstance(v, bool) or all_str(v), p,
|
|
640
|
+
'blob_replication.allow_private must be a boolean or a list of hosts')
|
|
641
|
+
opt(raw, 'source_url', is_http_url, p, 'blob_replication.source_url must be an http(s) URL')
|
|
642
|
+
opt(raw, 'servers', all_str, p, 'blob_replication.servers must be a list of URLs')
|
|
643
|
+
|
|
644
|
+
@classmethod
|
|
645
|
+
def from_dict(cls, raw: Any) -> 'BlobReplicationConfig':
|
|
646
|
+
ap = raw.get('allow_private', False)
|
|
647
|
+
return cls(copies=int(raw.get('copies', 2)), allow_private=tuple(ap) if isinstance(ap, list) else bool(ap),
|
|
648
|
+
source_url=raw.get('source_url'), servers=tuple(raw.get('servers') or ()))
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@dataclasses.dataclass(frozen=True)
|
|
652
|
+
class StorageConfig(Block):
|
|
653
|
+
"""The MeshNode manifest's storage block, as the node reads it."""
|
|
654
|
+
key = 'storage'
|
|
655
|
+
max_bytes: Optional[int] = None
|
|
656
|
+
allow_private_mirror: Union[bool, tuple[str, ...]] = False # true, or the LAN hosts a /mirror may read from
|
|
657
|
+
|
|
658
|
+
@classmethod
|
|
659
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
660
|
+
if not isinstance(raw, Mapping):
|
|
661
|
+
p.add('storage must be an object'); return
|
|
662
|
+
if 'maxBytes' in raw and raw['maxBytes'] not in (None, ''):
|
|
663
|
+
try:
|
|
664
|
+
int(raw['maxBytes'])
|
|
665
|
+
except (TypeError, ValueError):
|
|
666
|
+
p.add('blob storage cap is not an integer: %r' % (raw['maxBytes'],))
|
|
667
|
+
opt(raw, 'allowPrivateMirror', lambda v: isinstance(v, bool) or all_str(v), p,
|
|
668
|
+
'storage.allowPrivateMirror must be a boolean or a list of hosts')
|
|
669
|
+
|
|
670
|
+
@classmethod
|
|
671
|
+
def from_dict(cls, raw: Any) -> 'StorageConfig':
|
|
672
|
+
mb = raw.get('maxBytes')
|
|
673
|
+
cap = int(mb) if mb not in (None, '') else 0
|
|
674
|
+
ap = raw.get('allowPrivateMirror', False)
|
|
675
|
+
return cls(max_bytes=cap if cap > 0 else None,
|
|
676
|
+
allow_private_mirror=tuple(ap) if isinstance(ap, list) else bool(ap))
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@dataclasses.dataclass(frozen=True)
|
|
680
|
+
class BlobsConfig(Block):
|
|
681
|
+
"""The node's own blob store: cap (wins over storage.maxBytes) and on-disk layout.
|
|
682
|
+
|
|
683
|
+
``fanout`` (0..8, default 1) and ``data_dir`` (default ``<node>/blobs/data``)
|
|
684
|
+
let an operator share a sha256 tree with another CAS laid out the same way
|
|
685
|
+
(magpie: its ``objects/`` at fanout 2). Both are one-way doors: the store
|
|
686
|
+
records them on first open and refuses a later change.
|
|
687
|
+
"""
|
|
688
|
+
key = 'blobs'
|
|
689
|
+
max_storage_bytes: Optional[int] = None
|
|
690
|
+
fanout: int = 1
|
|
691
|
+
data_dir: Optional[str] = None
|
|
692
|
+
|
|
693
|
+
@classmethod
|
|
694
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
695
|
+
if not isinstance(raw, Mapping):
|
|
696
|
+
p.add('blobs must be an object'); return
|
|
697
|
+
if 'max_storage_bytes' in raw and raw['max_storage_bytes'] not in (None, ''):
|
|
698
|
+
try:
|
|
699
|
+
int(raw['max_storage_bytes'])
|
|
700
|
+
except (TypeError, ValueError):
|
|
701
|
+
p.add('blob storage cap is not an integer: %r' % (raw['max_storage_bytes'],))
|
|
702
|
+
if 'fanout' in raw and raw['fanout'] not in (None, ''):
|
|
703
|
+
f = raw['fanout']
|
|
704
|
+
if isinstance(f, bool) or not isinstance(f, int) or not 0 <= f <= 8:
|
|
705
|
+
p.add('blobs.fanout must be an integer 0..8: %r' % (f,))
|
|
706
|
+
if 'data_dir' in raw and raw['data_dir'] not in (None, '') and not isinstance(raw['data_dir'], str):
|
|
707
|
+
p.add('blobs.data_dir must be a path string: %r' % (raw['data_dir'],))
|
|
708
|
+
|
|
709
|
+
@classmethod
|
|
710
|
+
def from_dict(cls, raw: Any) -> 'BlobsConfig':
|
|
711
|
+
v = raw.get('max_storage_bytes')
|
|
712
|
+
cap = int(v) if v not in (None, '') else 0
|
|
713
|
+
f = raw.get('fanout')
|
|
714
|
+
d = raw.get('data_dir')
|
|
715
|
+
return cls(max_storage_bytes=cap if cap > 0 else None,
|
|
716
|
+
fanout=int(f) if f not in (None, '') else 1,
|
|
717
|
+
data_dir=str(d) if d not in (None, '') else None)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
# --- the external engines ------------------------------------------------
|
|
721
|
+
|
|
722
|
+
_PLUGIN_REF = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*:[A-Za-z_][A-Za-z0-9_]*$')
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
@dataclasses.dataclass(frozen=True)
|
|
726
|
+
class PluginSpec:
|
|
727
|
+
"""One out-of-tree compartment: `ref` is `package.module:ClassName`;
|
|
728
|
+
`after` names the manifest compartment it ticks right after (default: last)."""
|
|
729
|
+
ref: str
|
|
730
|
+
after: Optional[str] = None
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@dataclasses.dataclass(frozen=True)
|
|
734
|
+
class PluginsConfig(Block):
|
|
735
|
+
"""Out-of-tree compartments loaded into the manifest at startup.
|
|
736
|
+
|
|
737
|
+
"plugins": [{"ref": "fleetmesh_foo.node:Foo", "after": "budget"}]
|
|
738
|
+
|
|
739
|
+
A plugin is operator-trusted code: it is imported into the node's
|
|
740
|
+
process and sees everything a core compartment sees, keys included.
|
|
741
|
+
Installing one is the same act of trust as editing node/app/core.py;
|
|
742
|
+
the config block exists so that act is declared in one file an operator
|
|
743
|
+
reads, not hidden in a site-packages directory. Untrusted or
|
|
744
|
+
third-party extensions belong out of process (a sidecar) - see
|
|
745
|
+
docs/extending.md. Each plugin reads its own config under
|
|
746
|
+
`extensions.<name>`.
|
|
747
|
+
"""
|
|
748
|
+
key = 'plugins'
|
|
749
|
+
specs: tuple[PluginSpec, ...] = ()
|
|
750
|
+
|
|
751
|
+
@classmethod
|
|
752
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
753
|
+
if not isinstance(raw, list):
|
|
754
|
+
p.add('plugins must be a list of {ref, after?}'); return
|
|
755
|
+
for i, e in enumerate(raw):
|
|
756
|
+
if isinstance(e, str):
|
|
757
|
+
e = {'ref': e}
|
|
758
|
+
if not isinstance(e, Mapping) or not isinstance(e.get('ref'), str):
|
|
759
|
+
p.add('plugins[%d] needs a string ref "package.module:Class"' % i); continue
|
|
760
|
+
if not _PLUGIN_REF.match(e['ref']):
|
|
761
|
+
p.add('plugins[%d].ref is not package.module:Class: %r' % (i, e['ref']))
|
|
762
|
+
if 'after' in e and e['after'] is not None and not isinstance(e['after'], str):
|
|
763
|
+
p.add('plugins[%d].after must be a compartment name' % i)
|
|
764
|
+
|
|
765
|
+
@classmethod
|
|
766
|
+
def from_dict(cls, raw: Any) -> 'PluginsConfig':
|
|
767
|
+
specs = []
|
|
768
|
+
for e in raw or ():
|
|
769
|
+
if isinstance(e, str):
|
|
770
|
+
e = {'ref': e}
|
|
771
|
+
specs.append(PluginSpec(ref=e['ref'], after=e.get('after')))
|
|
772
|
+
return cls(specs=tuple(specs))
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
@dataclasses.dataclass(frozen=True)
|
|
776
|
+
class SidecarEntry:
|
|
777
|
+
"""One sidecar an operator runs: on only with an endpoint. `manifest` is
|
|
778
|
+
where its description comes from - absent for a built-in (grasp,
|
|
779
|
+
ncontainer, magpie: the node ships those), `@/path/file.json` or an
|
|
780
|
+
inline object for anyone else's."""
|
|
781
|
+
enabled: bool = False
|
|
782
|
+
endpoint: Optional[str] = None
|
|
783
|
+
manifest: Any = None # None | '@/path.json' | dict
|
|
784
|
+
|
|
785
|
+
@property
|
|
786
|
+
def on(self) -> bool:
|
|
787
|
+
return bool(self.enabled and self.endpoint)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
# What `grasp:`/`ncontainer:`/`magpie:` were before they moved under `sidecars`.
|
|
791
|
+
SIDECAR_KEYS_MOVED = ('grasp', 'ncontainer', 'magpie')
|
|
792
|
+
SIDECAR_MOVED = ('%s is now an entry of the "sidecars" block: '
|
|
793
|
+
'"sidecars": {"%s": {"enabled": true, "endpoint": "..."}}. The old top-level key is read for '
|
|
794
|
+
'this release and refused in the next.')
|
|
795
|
+
_SIDECAR_NAME = re.compile(r'^[a-z][a-z0-9_-]{0,31}$')
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
@dataclasses.dataclass(frozen=True)
|
|
799
|
+
class SidecarsConfig(ParamBlock):
|
|
800
|
+
"""The services this node fronts, by name. Each is declared on the
|
|
801
|
+
descriptor only after its manifest's self-tests pass (node/sidecars.py).
|
|
802
|
+
|
|
803
|
+
"sidecars": {
|
|
804
|
+
"grasp": {"enabled": true, "endpoint": "http://127.0.0.1:9000"},
|
|
805
|
+
"myhost": {"enabled": true, "endpoint": "http://127.0.0.1:9300",
|
|
806
|
+
"manifest": "@/etc/fleetmesh/myhost.sidecar.json"}
|
|
807
|
+
}
|
|
808
|
+
"""
|
|
809
|
+
key = 'sidecars'
|
|
810
|
+
entries: Mapping[str, SidecarEntry] = dataclasses.field(default_factory=dict)
|
|
811
|
+
DERIVED: bool = False
|
|
812
|
+
PARAMS = (
|
|
813
|
+
Parameter(key='entries', name='Sidecar Entries', description='Configured sidecar services', type=dict, default={}, tags=('sidecars', 'services')),
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
@classmethod
|
|
817
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
818
|
+
if not isinstance(raw, Mapping):
|
|
819
|
+
p.add('sidecars must be an object of {name: {enabled, endpoint[, manifest]}}'); return
|
|
820
|
+
for name, e in raw.items():
|
|
821
|
+
if not isinstance(name, str) or not _SIDECAR_NAME.match(name):
|
|
822
|
+
p.add('sidecars: %r is not a sidecar name (short lowercase identifier)' % (name,)); continue
|
|
823
|
+
if not isinstance(e, Mapping):
|
|
824
|
+
p.add('sidecars.%s must be an object' % name); continue
|
|
825
|
+
opt(e, 'enabled', lambda v: isinstance(v, bool), p, 'sidecars.%s.enabled must be a boolean' % name)
|
|
826
|
+
opt(e, 'endpoint', is_http_url, p, 'sidecars.%s.endpoint must be an http(s) URL' % name)
|
|
827
|
+
if e.get('enabled') and not e.get('endpoint'):
|
|
828
|
+
p.add('sidecars.%s.enabled needs sidecars.%s.endpoint' % (name, name))
|
|
829
|
+
m = e.get('manifest')
|
|
830
|
+
if m is not None and not (isinstance(m, Mapping) or (isinstance(m, str) and m.startswith('@'))):
|
|
831
|
+
p.add('sidecars.%s.manifest must be an inline object or "@/path/to/manifest.json"' % name)
|
|
832
|
+
extra = set(e) - {'enabled', 'endpoint', 'manifest'}
|
|
833
|
+
if extra:
|
|
834
|
+
p.add('sidecars.%s has unknown keys: %s' % (name, ', '.join(sorted(extra))))
|
|
835
|
+
|
|
836
|
+
@classmethod
|
|
837
|
+
def from_dict(cls, raw: Any) -> 'SidecarsConfig':
|
|
838
|
+
out = {}
|
|
839
|
+
for name, e in (raw or {}).items():
|
|
840
|
+
e = e or {}
|
|
841
|
+
out[name] = SidecarEntry(enabled=bool(e.get('enabled', False)), endpoint=e.get('endpoint'),
|
|
842
|
+
manifest=e.get('manifest'))
|
|
843
|
+
return cls(entries=out)
|
|
844
|
+
|
|
845
|
+
def to_dict(self) -> Mapping[str, Any]:
|
|
846
|
+
d: dict[str, Mapping[str, Any]] = {}
|
|
847
|
+
for name, e in self.entries.items():
|
|
848
|
+
row: MutableMapping[str, Any] = {'enabled': e.enabled}
|
|
849
|
+
if e.endpoint is not None:
|
|
850
|
+
row['endpoint'] = e.endpoint
|
|
851
|
+
if e.manifest is not None:
|
|
852
|
+
row['manifest'] = e.manifest
|
|
853
|
+
d[name] = row
|
|
854
|
+
return d
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def sidecars_of(cfg: Mapping[str, Any]) -> dict[str, Mapping[str, Any]]:
|
|
858
|
+
"""The raw `sidecars` entries, with the moved top-level keys folded in
|
|
859
|
+
for this release. One place reads both shapes; everything else sees
|
|
860
|
+
the new one."""
|
|
861
|
+
out: dict[str, Mapping[str, Any]] = dict(cfg.get('sidecars') or {})
|
|
862
|
+
for k in SIDECAR_KEYS_MOVED:
|
|
863
|
+
if k in cfg and k not in out and isinstance(cfg[k], Mapping):
|
|
864
|
+
out[k] = cfg[k]
|
|
865
|
+
return out
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
# --- updates: check-and-report ------------------------------------------------
|
|
869
|
+
|
|
870
|
+
# The project's release signer: npub1dxlsl0y...4xs7uy, the identity that signs
|
|
871
|
+
# everything fleetmesh publishes on nostr (OpenBao fleetmesh/publisher; release.sh
|
|
872
|
+
# stamps it into every manifest it signs). The committed manifests once carried
|
|
873
|
+
# the FIXTURE key from fixtures/npack/ instead, and a node that trusted that
|
|
874
|
+
# would have refused every genuine release - test_updates pins this to the
|
|
875
|
+
# packaged manifests so the three cannot drift apart again.
|
|
876
|
+
#
|
|
877
|
+
# This is the fallback, not the default. The default `updates.release_key` is
|
|
878
|
+
# "the key that made me": the publisher stamped into the tree by release.sh
|
|
879
|
+
# (node/PUBLISHER) or, in a checkout, the one in packages/fleetmesh-node's
|
|
880
|
+
# manifest - see default_release_key(). A fork's build therefore listens to the
|
|
881
|
+
# fork's signer without anyone editing a config. The operator can always write
|
|
882
|
+
# another key; it is a plain config value and state() shows which is in effect.
|
|
883
|
+
PROJECT_RELEASE_KEY: str = '69bf0fbc8211488fc321bee5bfa07bae184a881b0b16c3fb6e13a4b1dda28124'
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def default_release_key() -> str:
|
|
887
|
+
from ..release import running
|
|
888
|
+
return running().publisher or PROJECT_RELEASE_KEY
|
|
889
|
+
DEFAULT_CHECK_EVERY_SECONDS: int = 21600
|
|
890
|
+
UPDATE_POLICIES: tuple[str, ...] = ('pinned',) # 'track:patch' / 'track:minor' arrive with the apply tier
|
|
891
|
+
TRACK_NOT_YET: str = ("updates.policy: %r is not available yet; this release checks and reports. "
|
|
892
|
+
"Use 'pinned'")
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
@dataclasses.dataclass(frozen=True)
|
|
896
|
+
class UpdatesConfig(ParamBlock):
|
|
897
|
+
"""Check-and-report against an operator-pinned release key. See docs/updates.md.
|
|
898
|
+
|
|
899
|
+
The first compartment converted to `Parameter` (node/config/parameter.py).
|
|
900
|
+
Each setting is declared once in `PARAMS`; `check` and `from_dict` are
|
|
901
|
+
derived from it, and the four sentences an operator can be told are the
|
|
902
|
+
four `must` clauses below. Nothing here states a default twice -
|
|
903
|
+
`check_every` used to state `DEFAULT_CHECK_EVERY_SECONDS` in the field and
|
|
904
|
+
again in `from_dict`.
|
|
905
|
+
"""
|
|
906
|
+
key = 'updates'
|
|
907
|
+
SWITCHABLE = True
|
|
908
|
+
|
|
909
|
+
release_key: str = dataclasses.field(default_factory=default_release_key)
|
|
910
|
+
channel: tuple[str, ...] = ()
|
|
911
|
+
check_every: int = DEFAULT_CHECK_EVERY_SECONDS
|
|
912
|
+
policy: Mapping[str, str] = dataclasses.field(default_factory=lambda: {'*': 'pinned'})
|
|
913
|
+
pinned: Optional[str] = None
|
|
914
|
+
|
|
915
|
+
PARAMS = (
|
|
916
|
+
Parameter[str](
|
|
917
|
+
key='release_key', name='Release signing key',
|
|
918
|
+
description='Who may sign a release this node will mention. Defaults to the key '
|
|
919
|
+
'that signed the release this tree came from.',
|
|
920
|
+
type=str, default=default_release_key, must='be a 64-hex pubkey',
|
|
921
|
+
verify_regex=r'[0-9a-fA-F]{64}', coerce=lambda v: str(v).lower(),
|
|
922
|
+
tags=('updates', 'trust'), env='FLEETMESH_RELEASE_KEY',
|
|
923
|
+
# The key that decides which releases this node will mention, so
|
|
924
|
+
# the host and nobody else. No ability is named, which means no
|
|
925
|
+
# delegation can ever carry it; and no manifest, because a fleet
|
|
926
|
+
# declaration reaching the release-signing key is a supply chain
|
|
927
|
+
# with one more link in it than anyone intended.
|
|
928
|
+
sources=(par.ARG, par.ENV, par.FILE)),
|
|
929
|
+
Parameter[tuple](
|
|
930
|
+
key='channel', name='Release channel relays',
|
|
931
|
+
description='Relays the kind 9900 release events live on. Empty means the '
|
|
932
|
+
"node's own relays.",
|
|
933
|
+
type=tuple, item=str, default=(), must='be a list of ws(s) relay URLs',
|
|
934
|
+
verify_regex=r'wss?://.+', coerce=lambda v: tuple(str(u) for u in v),
|
|
935
|
+
tags=('updates', 'relays'), sources=(par.ARG, par.ENV, par.FILE, par.MANIFEST)),
|
|
936
|
+
Parameter[int](
|
|
937
|
+
key='check_every', name='Check interval',
|
|
938
|
+
description='Seconds between checks. 0 means only on `meshnode updates.check`.',
|
|
939
|
+
type=int, default=DEFAULT_CHECK_EVERY_SECONDS, minimum=0,
|
|
940
|
+
must='be a non-negative integer of seconds',
|
|
941
|
+
tags=('updates', 'cadence'), live=True,
|
|
942
|
+
# The only setting in this block that is safe to hand out: it
|
|
943
|
+
# paces a check and cannot change what the node trusts. Named as
|
|
944
|
+
# a UCAN ability rather than a role, and declared only - nothing
|
|
945
|
+
# authenticates a principal yet (see ACCESS_DECLARED_ONLY).
|
|
946
|
+
requires='config/updates.cadence'),
|
|
947
|
+
Parameter[dict](
|
|
948
|
+
key='policy', name='Per-compartment update policy',
|
|
949
|
+
description="Policy per compartment or '*'. Only 'pinned' exists in this tier; a "
|
|
950
|
+
'track is refused by name so an operator who writes one learns it does '
|
|
951
|
+
'nothing instead of believing it does.',
|
|
952
|
+
type=dict, default=lambda: {'*': 'pinned'}, choices=UPDATE_POLICIES,
|
|
953
|
+
must='be an object of compartment -> policy', tags=('updates', 'policy'),
|
|
954
|
+
coerce=dict, validate=lambda label, raw, p: _check_update_policy(label, raw, p),
|
|
955
|
+
sources=(par.ARG, par.ENV, par.FILE)),
|
|
956
|
+
Parameter[Optional[str]](
|
|
957
|
+
key='pinned', name='Pinned release',
|
|
958
|
+
description='What the operator says they run. Unset means Release.running().',
|
|
959
|
+
type=str, default=None, must='be a semver string (e.g. 0.1.0)',
|
|
960
|
+
validate=lambda label, raw, p: _check_pinned(label, raw, p),
|
|
961
|
+
tags=('updates', 'release'), sources=(par.ARG, par.ENV, par.FILE)),
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _check_update_policy(label: str, raw: Any, p: Problems) -> None:
|
|
966
|
+
"""`updates.policy`: a mapping whose values are an enum, plus one sentence
|
|
967
|
+
for a policy that does not exist yet.
|
|
968
|
+
|
|
969
|
+
A parameter's own fields cannot say this - the enum applies to the values
|
|
970
|
+
of a mapping, and `track:` has its own message - so the parameter carries
|
|
971
|
+
this instead, and is still the single place the setting is declared.
|
|
972
|
+
"""
|
|
973
|
+
pol = raw.get('policy')
|
|
974
|
+
if pol is None:
|
|
975
|
+
return
|
|
976
|
+
if not isinstance(pol, Mapping) or not all(isinstance(k, str) for k in pol):
|
|
977
|
+
p.add('%s.policy must be an object of compartment -> policy' % label)
|
|
978
|
+
return
|
|
979
|
+
for k, v in pol.items():
|
|
980
|
+
if isinstance(v, str) and v.startswith('track:'):
|
|
981
|
+
p.add(TRACK_NOT_YET % v)
|
|
982
|
+
elif v not in UPDATE_POLICIES:
|
|
983
|
+
p.add('%s.policy.%s must be one of %s' % (label, k, ', '.join(UPDATE_POLICIES)))
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _check_pinned(label: str, raw: Any, p: Problems) -> None:
|
|
987
|
+
"""`updates.pinned`: a semver, parsed by the release module rather than
|
|
988
|
+
matched by a pattern, so one definition of a version decides."""
|
|
989
|
+
pinned = raw.get('pinned')
|
|
990
|
+
if pinned is None:
|
|
991
|
+
return
|
|
992
|
+
from ..release import parse_semver
|
|
993
|
+
if not isinstance(pinned, str) or parse_semver(pinned) is None:
|
|
994
|
+
p.add('%s.pinned must be a semver string (e.g. 0.1.0)' % label)
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
# --- fetch pricing -------------------------------------------------------------
|
|
998
|
+
|
|
999
|
+
@dataclasses.dataclass(frozen=True)
|
|
1000
|
+
class CashuRail(Block):
|
|
1001
|
+
mints: tuple[str, ...] = ()
|
|
1002
|
+
unit: str = 'sat'
|
|
1003
|
+
sweep_above_sat: int = 0
|
|
1004
|
+
sweep_to: Optional[str] = None
|
|
1005
|
+
|
|
1006
|
+
@classmethod
|
|
1007
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
1008
|
+
if not isinstance(raw, Mapping):
|
|
1009
|
+
p.add('fetch_pricing.rails.cashu must be an object'); return
|
|
1010
|
+
opt(raw, 'mints', lambda v: isinstance(v, list) and all(is_http_url(m) for m in v), p,
|
|
1011
|
+
'fetch_pricing.rails.cashu.mints must be a list of http(s) URLs')
|
|
1012
|
+
opt(raw, 'unit', lambda v: isinstance(v, str), p, 'fetch_pricing.rails.cashu.unit must be a string')
|
|
1013
|
+
opt(raw, 'sweep_above_sat', non_negative_int, p, 'fetch_pricing.rails.cashu.sweep_above_sat must be a non-negative integer')
|
|
1014
|
+
opt(raw, 'sweep_to', lambda v: v is None or isinstance(v, str), p, 'fetch_pricing.rails.cashu.sweep_to must be a string or null')
|
|
1015
|
+
|
|
1016
|
+
@classmethod
|
|
1017
|
+
def from_dict(cls, raw: Any) -> 'CashuRail':
|
|
1018
|
+
return cls(mints=tuple(raw.get('mints') or ()), unit=str(raw.get('unit') or 'sat'),
|
|
1019
|
+
sweep_above_sat=int(raw.get('sweep_above_sat') or 0), sweep_to=raw.get('sweep_to'))
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
PRICING_RULE_KEYS: tuple[str, ...] = ('min_bytes', 'max_bytes', 'content_type', 'uploader', 'msat_per_mb', 'msat')
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
@dataclasses.dataclass(frozen=True)
|
|
1026
|
+
class PricingRule(Block):
|
|
1027
|
+
"""One match-and-price rule; first match wins. See node/fetch_pricing.py."""
|
|
1028
|
+
min_bytes: Optional[int] = None
|
|
1029
|
+
max_bytes: Optional[int] = None
|
|
1030
|
+
content_type: Optional[str] = None
|
|
1031
|
+
uploader: Optional[str] = None
|
|
1032
|
+
msat_per_mb: Optional[int] = None
|
|
1033
|
+
msat: int = 0
|
|
1034
|
+
|
|
1035
|
+
@staticmethod
|
|
1036
|
+
def check_one(i: int, e: Any, p: Problems) -> None:
|
|
1037
|
+
if not isinstance(e, Mapping):
|
|
1038
|
+
p.add('fetch_pricing.rules[%d] must be an object' % i); return
|
|
1039
|
+
for k in ('min_bytes', 'max_bytes', 'msat_per_mb', 'msat'):
|
|
1040
|
+
if k in e and not non_negative_int(e[k]):
|
|
1041
|
+
p.add('fetch_pricing.rules[%d].%s must be a non-negative integer' % (i, k))
|
|
1042
|
+
for k in ('content_type', 'uploader'):
|
|
1043
|
+
if k in e and not isinstance(e[k], str):
|
|
1044
|
+
p.add('fetch_pricing.rules[%d].%s must be a string' % (i, k))
|
|
1045
|
+
unknown = sorted(set(e) - set(PRICING_RULE_KEYS))
|
|
1046
|
+
if unknown:
|
|
1047
|
+
p.add('fetch_pricing.rules[%d] has keys no rule reads: %s' % (i, ', '.join(unknown)))
|
|
1048
|
+
|
|
1049
|
+
@classmethod
|
|
1050
|
+
def from_dict(cls, raw: Any) -> 'PricingRule':
|
|
1051
|
+
return cls(min_bytes=raw.get('min_bytes'), max_bytes=raw.get('max_bytes'), content_type=raw.get('content_type'),
|
|
1052
|
+
uploader=raw.get('uploader'), msat_per_mb=raw.get('msat_per_mb'), msat=int(raw.get('msat', 0)))
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
@dataclasses.dataclass(frozen=True)
|
|
1056
|
+
class FetchPricingConfig(ParamBlock):
|
|
1057
|
+
"""What a fetch costs. Everything defaults to zero or off."""
|
|
1058
|
+
key = 'fetch_pricing'
|
|
1059
|
+
default_msat: int = 0
|
|
1060
|
+
free_for_partners: bool = True
|
|
1061
|
+
free_bytes_per_day: Optional[int] = None
|
|
1062
|
+
max_account_fee_msat: int = 0
|
|
1063
|
+
grasp_session_msat: int = 0
|
|
1064
|
+
sidecar_session_msat: Mapping[str, int] = dataclasses.field(default_factory=dict)
|
|
1065
|
+
rules: tuple[PricingRule, ...] = ()
|
|
1066
|
+
cashu: CashuRail = CashuRail()
|
|
1067
|
+
DERIVED: bool = False
|
|
1068
|
+
PARAMS = (
|
|
1069
|
+
Parameter(key='default_msat', name='Default Price (msat)', description='Default fetch price in millisatoshis', type=int, default=0, tags=('fetch', 'pricing')),
|
|
1070
|
+
Parameter(key='free_for_partners', name='Free for Partners', description='Waive fetch fee for peering partners', type=bool, default=True, tags=('fetch', 'pricing')),
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
@classmethod
|
|
1074
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
1075
|
+
if not isinstance(raw, Mapping):
|
|
1076
|
+
p.add('fetch_pricing must be an object'); return
|
|
1077
|
+
opt(raw, 'default_msat', non_negative_int, p, 'fetch_pricing.default_msat must be a non-negative integer')
|
|
1078
|
+
opt(raw, 'free_for_partners', lambda v: isinstance(v, bool), p, 'fetch_pricing.free_for_partners must be a boolean')
|
|
1079
|
+
opt(raw, 'free_bytes_per_day', lambda v: v is None or non_negative_int(v), p,
|
|
1080
|
+
'fetch_pricing.free_bytes_per_day must be a non-negative integer or null')
|
|
1081
|
+
opt(raw, 'max_account_fee_msat', non_negative_int, p, 'fetch_pricing.max_account_fee_msat must be a non-negative integer')
|
|
1082
|
+
opt(raw, 'grasp_session_msat', non_negative_int, p, 'fetch_pricing.grasp_session_msat must be a non-negative integer')
|
|
1083
|
+
ssm = raw.get('sidecar_session_msat', {})
|
|
1084
|
+
if not isinstance(ssm, Mapping) or not all(isinstance(k, str) and _SIDECAR_NAME.match(k) and non_negative_int(v)
|
|
1085
|
+
for k, v in ssm.items()):
|
|
1086
|
+
p.add('fetch_pricing.sidecar_session_msat must be {sidecar name: non-negative msat}')
|
|
1087
|
+
rules = raw.get('rules', [])
|
|
1088
|
+
if not isinstance(rules, list):
|
|
1089
|
+
p.add('fetch_pricing.rules must be a list')
|
|
1090
|
+
else:
|
|
1091
|
+
for i, e in enumerate(rules):
|
|
1092
|
+
PricingRule.check_one(i, e, p)
|
|
1093
|
+
rails = raw.get('rails', {})
|
|
1094
|
+
if not isinstance(rails, Mapping):
|
|
1095
|
+
p.add('fetch_pricing.rails must be an object')
|
|
1096
|
+
elif 'cashu' in rails:
|
|
1097
|
+
CashuRail.check(rails['cashu'], p)
|
|
1098
|
+
|
|
1099
|
+
@classmethod
|
|
1100
|
+
def from_dict(cls, raw: Any) -> 'FetchPricingConfig':
|
|
1101
|
+
rails = raw.get('rails') or {}
|
|
1102
|
+
return cls(default_msat=int(raw.get('default_msat') or 0), free_for_partners=bool(raw.get('free_for_partners', True)),
|
|
1103
|
+
free_bytes_per_day=raw.get('free_bytes_per_day'), max_account_fee_msat=int(raw.get('max_account_fee_msat') or 0),
|
|
1104
|
+
grasp_session_msat=int(raw.get('grasp_session_msat') or 0),
|
|
1105
|
+
sidecar_session_msat={k: int(v) for k, v in (raw.get('sidecar_session_msat') or {}).items()},
|
|
1106
|
+
rules=tuple(PricingRule.from_dict(r) for r in raw.get('rules') or []),
|
|
1107
|
+
cashu=CashuRail.from_dict(rails.get('cashu') or {}))
|
|
1108
|
+
|
|
1109
|
+
def to_dict(self) -> Mapping[str, Any]:
|
|
1110
|
+
"""The file shape keeps `rails: {cashu: ...}`, so the default stays byte-identical."""
|
|
1111
|
+
d = dict(super().to_dict())
|
|
1112
|
+
d['rails'] = {'cashu': d.pop('cashu')}
|
|
1113
|
+
return d
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
# --- protocol suites ------------------------------------------------------
|
|
1117
|
+
|
|
1118
|
+
from .protocols import (
|
|
1119
|
+
ActivityPubProtocolConfig,
|
|
1120
|
+
ATProtoProtocolConfig,
|
|
1121
|
+
HostMultiplexerConfig,
|
|
1122
|
+
NostrProtocolConfig,
|
|
1123
|
+
P2PandaProtocolConfig,
|
|
1124
|
+
ProtocolSuitesConfig,
|
|
1125
|
+
RhioProtocolConfig,
|
|
1126
|
+
SSBProtocolConfig,
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
COMPARTMENT_BLOCKS: tuple[type[Block], ...] = (
|
|
1131
|
+
BridgeConfig, AnnounceConfig, LanConfig, P2PConfig, GossipConfig, CapacityConfig, ComputeConfig, DVMConfig,
|
|
1132
|
+
SettlementConfig, MediationConfig, TransportConfig, BlobWireConfig, BlobReplicationConfig,
|
|
1133
|
+
StorageConfig, BlobsConfig, SidecarsConfig, FetchPricingConfig, UpdatesConfig,
|
|
1134
|
+
PluginsConfig,
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
__all__ = [c.__name__ for c in COMPARTMENT_BLOCKS] + [
|
|
1138
|
+
'BridgeIngress', 'BridgeEgress', 'STRFRY_PLUGIN_REF', 'STRFRY_MOVED', 'SidecarEntry', 'SIDECAR_KEYS_MOVED', 'SIDECAR_MOVED', 'sidecars_of', 'CashuRail', 'PricingRule',
|
|
1139
|
+
'HostMultiplexerConfig', 'NostrProtocolConfig', 'ATProtoProtocolConfig', 'ActivityPubProtocolConfig', 'SSBProtocolConfig',
|
|
1140
|
+
'P2PandaProtocolConfig', 'RhioProtocolConfig', 'ProtocolSuitesConfig',
|
|
1141
|
+
'COMPARTMENT_BLOCKS', 'PRICING_RULE_KEYS', 'BLOB_WIRE_CAPS', 'switch_or_object', 'DEFAULT_INGRESS_KINDS', 'DEFAULT_EGRESS_KINDS',
|
|
1142
|
+
'DEFAULT_INGRESS_FILTERS', 'DEFAULT_RATE_LIMIT_PER_MIN', 'DEFAULT_MAX_FUTURE_SECONDS',
|
|
1143
|
+
'DEFAULT_MAX_EVENT_BYTES', 'DEFAULT_DVM_KINDS', 'PROJECT_RELEASE_KEY', 'default_release_key', 'UPDATE_POLICIES', 'TRACK_NOT_YET']
|
|
1144
|
+
|