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,310 @@
|
|
|
1
|
+
"""The compartment. Optional; `"updates": true` in config.json turns it on."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Mapping, Optional, Sequence
|
|
5
|
+
|
|
6
|
+
from node import channels as ch_mod
|
|
7
|
+
from node import metrics as mx_mod
|
|
8
|
+
from node import release as release_mod
|
|
9
|
+
from node.compartment import stores as st
|
|
10
|
+
from node.protocols.nostr import client as relay_mod
|
|
11
|
+
from node.config import UpdatesConfig
|
|
12
|
+
from node.config.parameter import Parameter
|
|
13
|
+
from node.interfaces import NostrEvent
|
|
14
|
+
from node.compartment import Command, Compartment, Every, Need
|
|
15
|
+
from fleetmesh_core.peering.release import RELEASE_KIND, REVOKE_KIND, ReleaseInfo, parse_revocation, select
|
|
16
|
+
from .store import UpdateStore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from .types import UpdateStateReport
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Fetcher = Callable[[Sequence[str], Sequence[Mapping[str, Any]]], list[NostrEvent]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _relay_fetch(urls: Sequence[str], filters: Sequence[Mapping[str, Any]]) -> list[NostrEvent]:
|
|
26
|
+
return relay_mod.query_all(urls, filters, timeout=10.0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
RELEASE_AVAILABLE: str = 'release.available'
|
|
30
|
+
RELEASE_REVOKED: str = 'release.revoked'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Updates(Compartment):
|
|
34
|
+
"""Check-and-report: the newest release this node could run, said once.
|
|
35
|
+
|
|
36
|
+
`trouble()` speaks when a newer release is within what the operator's
|
|
37
|
+
policy would apply - which in this tier is never, so it speaks about any
|
|
38
|
+
newer compatible patch or minor and stays quiet about a major: an
|
|
39
|
+
operator who chose `pinned` is told what they could move to without a
|
|
40
|
+
protocol or API change, not shouted at about a jump they would have to
|
|
41
|
+
opt into. `updates.ack <version>` silences one version; a newer one
|
|
42
|
+
speaks again.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
name = 'updates'
|
|
46
|
+
optional = True
|
|
47
|
+
enabled_by = 'updates'
|
|
48
|
+
reads = ('updates', 'relays')
|
|
49
|
+
live = ('updates',)
|
|
50
|
+
config_cls = UpdatesConfig
|
|
51
|
+
SCHEMA = UpdateStore.SCHEMA
|
|
52
|
+
STORE_CLS = UpdateStore
|
|
53
|
+
NEEDS = (
|
|
54
|
+
Need('db_path', path='updates.sqlite'),
|
|
55
|
+
Need('cfg', block=UpdatesConfig, getter=True),
|
|
56
|
+
Need('relays', 'relays'),
|
|
57
|
+
)
|
|
58
|
+
STORES = (
|
|
59
|
+
st.Store(key='releases', description='recent release announcements seen',
|
|
60
|
+
kind=st.RING, shape=dict, bound=256),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# What this compartment measures. A counter naming a channel is fed by
|
|
64
|
+
# that channel being raised, so the line and the count come from one
|
|
65
|
+
# declaration and cannot disagree.
|
|
66
|
+
METRICS = (
|
|
67
|
+
mx_mod.Metric(
|
|
68
|
+
name='release_available',
|
|
69
|
+
description='Times a newer compatible release was announced by this node.',
|
|
70
|
+
kind=mx_mod.COUNTER, counts=RELEASE_AVAILABLE,
|
|
71
|
+
tags=('updates', 'release')),
|
|
72
|
+
mx_mod.Metric(
|
|
73
|
+
name='release_revoked',
|
|
74
|
+
description='Times a release this node could run was found revoked.',
|
|
75
|
+
kind=mx_mod.COUNTER, counts=RELEASE_REVOKED,
|
|
76
|
+
tags=('updates', 'release')),
|
|
77
|
+
mx_mod.Metric(
|
|
78
|
+
name='updates_check',
|
|
79
|
+
description='How long a release check took, end to end.',
|
|
80
|
+
kind=mx_mod.HISTOGRAM, unit='seconds', buckets=mx_mod.LATENCY_BUCKETS,
|
|
81
|
+
tags=('updates', 'latency')),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Channel names as constants, not strings at the raise site. A mistyped
|
|
85
|
+
# string is an undeclared channel found at runtime by a counter; a
|
|
86
|
+
# mistyped constant is a NameError at import. eventbus.py's TOPIC_* names
|
|
87
|
+
# already work this way.
|
|
88
|
+
RELEASE_AVAILABLE: str = 'release.available'
|
|
89
|
+
RELEASE_REVOKED: str = 'release.revoked'
|
|
90
|
+
|
|
91
|
+
# What this compartment says. Before this the same event left as an
|
|
92
|
+
# unnamed positional tuple - ('release', 'available', version, kind) -
|
|
93
|
+
# which control.py joined with spaces for the journal and nothing
|
|
94
|
+
# structured ever saw. The tuple still goes back from tick(); the channel
|
|
95
|
+
# is what gives it a name, a level and one rendering per audience.
|
|
96
|
+
CHANNELS = (
|
|
97
|
+
ch_mod.Channel(
|
|
98
|
+
name=RELEASE_AVAILABLE,
|
|
99
|
+
description='A release newer than the one this node runs is published and '
|
|
100
|
+
'compatible with the operator policy. Said once per version; '
|
|
101
|
+
'`updates.ack <version>` silences one.',
|
|
102
|
+
level=ch_mod.NOTE,
|
|
103
|
+
wire=lambda version, kind, running: {
|
|
104
|
+
'version': version, 'kind': kind, 'running': running},
|
|
105
|
+
say='release {version} available ({kind}) - running {running}',
|
|
106
|
+
tags=('updates', 'release')),
|
|
107
|
+
ch_mod.Channel(
|
|
108
|
+
name=RELEASE_REVOKED,
|
|
109
|
+
description='A release this node could run was revoked by the signing key. '
|
|
110
|
+
'Trouble rather than a note: a revoked release is one nobody '
|
|
111
|
+
'should move to.',
|
|
112
|
+
level=ch_mod.TROUBLE,
|
|
113
|
+
wire=lambda version, reason: {'version': version, 'reason': reason},
|
|
114
|
+
say='release {version} revoked: {reason}',
|
|
115
|
+
tags=('updates', 'release', 'trouble')),
|
|
116
|
+
)
|
|
117
|
+
COMMANDS = (
|
|
118
|
+
Command(
|
|
119
|
+
key='check',
|
|
120
|
+
name='Check Updates',
|
|
121
|
+
description='ask the channel now; the state after',
|
|
122
|
+
tags=('updates', 'check'),
|
|
123
|
+
permission='updates/check',
|
|
124
|
+
run='check',
|
|
125
|
+
reply='dict',
|
|
126
|
+
),
|
|
127
|
+
Command(
|
|
128
|
+
key='show',
|
|
129
|
+
name='Updates Status',
|
|
130
|
+
description='the newest compatible release as parsed, and every one seen',
|
|
131
|
+
tags=('updates', 'status'),
|
|
132
|
+
permission='updates/show',
|
|
133
|
+
run='show_cmd',
|
|
134
|
+
reply='dict',
|
|
135
|
+
),
|
|
136
|
+
Command(
|
|
137
|
+
key='ack',
|
|
138
|
+
name='Acknowledge Update',
|
|
139
|
+
description='stop mentioning this version in trouble',
|
|
140
|
+
tags=('updates', 'ack'),
|
|
141
|
+
parameters=(
|
|
142
|
+
Parameter(key='version', name='version', description='semver version to acknowledge', type=str, default='', required=True),
|
|
143
|
+
),
|
|
144
|
+
permission='updates/ack',
|
|
145
|
+
run='ack_cmd',
|
|
146
|
+
reply='dict',
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
EVERY = (
|
|
150
|
+
Every(
|
|
151
|
+
# 0 is off (Every.is_off): checked only on command.
|
|
152
|
+
seconds=lambda self: float(max(0, self.settings().check_every)),
|
|
153
|
+
run='periodic_check',
|
|
154
|
+
name='check_updates',
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
db_path: str = ':memory:',
|
|
161
|
+
cfg: Optional[Callable[[], UpdatesConfig]] = None,
|
|
162
|
+
relays: Optional[Sequence[str]] = None,
|
|
163
|
+
clock: Optional[Callable[[], int]] = None,
|
|
164
|
+
fetch: Fetcher = _relay_fetch,
|
|
165
|
+
running: Optional[release_mod.Release] = None,
|
|
166
|
+
node: Any = None,
|
|
167
|
+
db: Optional[Any] = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
if db is not None:
|
|
170
|
+
if not isinstance(db, UpdateStore):
|
|
171
|
+
# Said here, by name, rather than as an AttributeError on the
|
|
172
|
+
# first store method - which is how B13 went unnoticed.
|
|
173
|
+
raise TypeError('updates needs an UpdateStore as db, not %s' % type(db).__name__)
|
|
174
|
+
store: Any = db
|
|
175
|
+
elif db_path:
|
|
176
|
+
store = UpdateStore(db_path)
|
|
177
|
+
else:
|
|
178
|
+
store = UpdateStore(':memory:')
|
|
179
|
+
super().__init__(node, clock, db=store)
|
|
180
|
+
self.store: Any = store
|
|
181
|
+
self.db = store
|
|
182
|
+
self._cfg: Callable[[], UpdatesConfig] = cfg or (lambda: UpdatesConfig())
|
|
183
|
+
self._relays: list[str] = list(relays or []) # not a store: the configured relays
|
|
184
|
+
self._fetch = fetch
|
|
185
|
+
self.running: release_mod.Release = running or release_mod.running()
|
|
186
|
+
self.newest: Optional[ReleaseInfo] = None # newest compatible, newer than running
|
|
187
|
+
self.incompatible: Optional[ReleaseInfo] = None # newest that speaks another protocol
|
|
188
|
+
self._closed = False
|
|
189
|
+
last = self.store.last_check()
|
|
190
|
+
if last is not None:
|
|
191
|
+
self._every_last['check_updates'] = last
|
|
192
|
+
|
|
193
|
+
# ---- configuration ----------------------------------------------------------------
|
|
194
|
+
def settings(self) -> UpdatesConfig:
|
|
195
|
+
return self._cfg()
|
|
196
|
+
|
|
197
|
+
def channel(self) -> list[str]:
|
|
198
|
+
return list(self.settings().channel) or self._relays
|
|
199
|
+
|
|
200
|
+
def version(self) -> str:
|
|
201
|
+
"""What this node runs, by the operator's word if they gave one."""
|
|
202
|
+
return self.settings().pinned or self.running.version
|
|
203
|
+
|
|
204
|
+
def policy_for(self, compartment: str = '*') -> str:
|
|
205
|
+
pol = self.settings().policy
|
|
206
|
+
return str(pol.get(compartment) or pol.get('*') or 'pinned')
|
|
207
|
+
|
|
208
|
+
# ---- the check ----------------------------------------------------------------------
|
|
209
|
+
def check(self, now: Optional[int] = None) -> UpdateStateReport:
|
|
210
|
+
now = self.clock() if now is None else now
|
|
211
|
+
s = self.settings()
|
|
212
|
+
urls = self.channel()
|
|
213
|
+
# kind + author only: relays index single-letter tags (NIP-01), so a
|
|
214
|
+
# `#name` filter matches nothing anywhere. parse_release_event keeps
|
|
215
|
+
# the name check; releases of other packages are skipped, not counted.
|
|
216
|
+
filters = [{'kinds': [RELEASE_KIND, REVOKE_KIND], 'authors': [s.release_key], 'limit': 100}]
|
|
217
|
+
reachable: dict[str, bool] = {}
|
|
218
|
+
events: list[NostrEvent] = []
|
|
219
|
+
for url in urls:
|
|
220
|
+
try:
|
|
221
|
+
events.extend(self._fetch([url], filters))
|
|
222
|
+
reachable[url] = True
|
|
223
|
+
except Exception as e: # a relay down is state, not a crash
|
|
224
|
+
reachable[url] = False
|
|
225
|
+
self.refusals.refuse('channel', 'unreachable', subject=url, detail={'error': str(e)[:120]}, at=now)
|
|
226
|
+
# the pure half - revocations first whichever order relays hand
|
|
227
|
+
# events back, then the newest compatible / incompatible newer -
|
|
228
|
+
# is fleetmesh_core.peering.release.select, the rule the conformance vectors pin
|
|
229
|
+
sel = select(events, s.release_key, self.version(), self.running.protocol, self.running.name,
|
|
230
|
+
already_revoked=self.store.revoked())
|
|
231
|
+
for ev in events:
|
|
232
|
+
rv = parse_revocation(ev, s.release_key)
|
|
233
|
+
if rv is not None:
|
|
234
|
+
self.store.revoke(rv[0], rv[1], now)
|
|
235
|
+
for eid, why in sel.refused:
|
|
236
|
+
self.refusals.refuse('release', why, subject=eid[:16], at=now)
|
|
237
|
+
revoked = set(sel.revoked)
|
|
238
|
+
# a revocation can arrive after the release was already the newest;
|
|
239
|
+
# a previous check's pick stays unless this check found a newer one
|
|
240
|
+
newest = None if self.newest and self.newest.event_id in revoked else self.newest
|
|
241
|
+
incompatible = None if self.incompatible and self.incompatible.event_id in revoked else self.incompatible
|
|
242
|
+
if sel.newest_compatible and (newest is None or release_mod.compare(sel.newest_compatible.version, newest.version) > 0):
|
|
243
|
+
newest = sel.newest_compatible
|
|
244
|
+
if sel.incompatible_newer and (incompatible is None
|
|
245
|
+
or release_mod.compare(sel.incompatible_newer.version, incompatible.version) > 0):
|
|
246
|
+
incompatible = sel.incompatible_newer
|
|
247
|
+
self.newest, self.incompatible = newest, incompatible
|
|
248
|
+
if newest is not None:
|
|
249
|
+
self.store.saw(newest.version, newest.event_id, newest.sha256, now)
|
|
250
|
+
self.store.checked(now, reachable)
|
|
251
|
+
return self.state()
|
|
252
|
+
|
|
253
|
+
def periodic_check(self, now: int) -> list[tuple[Any, ...]]:
|
|
254
|
+
if self.settings().check_every <= 0:
|
|
255
|
+
return []
|
|
256
|
+
before = self.newest.version if self.newest else None
|
|
257
|
+
self.check(now)
|
|
258
|
+
after = self.newest.version if self.newest else None
|
|
259
|
+
return [('release', 'available', after, release_mod.classify(self.version(), after))] if after and after != before else []
|
|
260
|
+
|
|
261
|
+
# ---- the compartment contract ---------------------------------------------------------------
|
|
262
|
+
def state(self) -> UpdateStateReport:
|
|
263
|
+
if self._closed:
|
|
264
|
+
return {'stopped': True}
|
|
265
|
+
n = self.newest
|
|
266
|
+
cfg = self.settings() # parsed once, not three times (audit E7)
|
|
267
|
+
return {
|
|
268
|
+
'running': dict(self.running.as_dict(), pinned=cfg.pinned),
|
|
269
|
+
'release_key': cfg.release_key,
|
|
270
|
+
'policy': dict(cfg.policy),
|
|
271
|
+
'channel': self.channel(),
|
|
272
|
+
'reachable': self.store.reachable(),
|
|
273
|
+
'last_check': self.store.last_check(),
|
|
274
|
+
'newest_compatible': dict(n.as_dict(), **{'class': release_mod.classify(self.version(), n.version),
|
|
275
|
+
'acked': self.store.acked(n.version)}) if n else None,
|
|
276
|
+
'incompatible_newer': self.incompatible.as_dict() if self.incompatible else None,
|
|
277
|
+
'revoked': self.store.revoked(),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
def trouble(self) -> list[str]:
|
|
281
|
+
out: list[str] = []
|
|
282
|
+
n = self.newest
|
|
283
|
+
if n is not None and not self.store.acked(n.version):
|
|
284
|
+
cls = release_mod.classify(self.version(), n.version)
|
|
285
|
+
if cls in ('patch', 'minor'):
|
|
286
|
+
out.append('%s %s available (%s); you run %s; policy %s - `meshnode updates.show` for the release'
|
|
287
|
+
% (n.name, n.version, cls, self.version(), self.policy_for()))
|
|
288
|
+
if self.running.source == 'checkout' and n is not None and not self.store.acked(n.version):
|
|
289
|
+
out.append('this node runs from a checkout; updates are reported, never applied here')
|
|
290
|
+
reach = self.store.reachable()
|
|
291
|
+
if reach and not any(reach.values()):
|
|
292
|
+
out.append('update channel unreachable: %s' % ', '.join(sorted(reach)))
|
|
293
|
+
return out
|
|
294
|
+
|
|
295
|
+
def show_cmd(self) -> Mapping[str, Any]:
|
|
296
|
+
return {'newest_compatible': self.newest.as_dict() if self.newest else None,
|
|
297
|
+
'incompatible_newer': self.incompatible.as_dict() if self.incompatible else None,
|
|
298
|
+
'seen': self.store.seen(), 'revoked': self.store.revoked()}
|
|
299
|
+
|
|
300
|
+
def ack_cmd(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
301
|
+
v = str(args.get('version', ''))
|
|
302
|
+
if release_mod.parse_semver(v) is None:
|
|
303
|
+
raise ValueError('updates.ack: version must be a semver')
|
|
304
|
+
return {'version': v, 'acked': self.store.ack(v)}
|
|
305
|
+
|
|
306
|
+
def stop(self) -> Any:
|
|
307
|
+
self._closed = True
|
|
308
|
+
self.store.close()
|
|
309
|
+
super().stop()
|
|
310
|
+
return None
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""What the update check has seen and what the operator has acknowledged.
|
|
2
|
+
|
|
3
|
+
Durable so a restart does not re-announce a release the operator already
|
|
4
|
+
read, and so `last_check` survives to be reported honestly.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Mapping, Optional
|
|
10
|
+
|
|
11
|
+
from node.compartment.schema import Column, Table
|
|
12
|
+
from node.storage import SqliteStore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UpdateStore(SqliteStore):
|
|
16
|
+
SCHEMA = (
|
|
17
|
+
Table('kv', (
|
|
18
|
+
Column('k', 'TEXT', primary_key=True),
|
|
19
|
+
Column('v', 'TEXT', not_null=True),
|
|
20
|
+
)),
|
|
21
|
+
Table('seen', (
|
|
22
|
+
Column('version', 'TEXT', primary_key=True),
|
|
23
|
+
Column('event_id', 'TEXT', not_null=True),
|
|
24
|
+
Column('sha256', 'TEXT', not_null=True),
|
|
25
|
+
Column('first_seen', 'INTEGER', not_null=True),
|
|
26
|
+
Column('acked', 'INTEGER', not_null=True, default=0),
|
|
27
|
+
)),
|
|
28
|
+
Table('revoked', (
|
|
29
|
+
Column('event_id', 'TEXT', primary_key=True),
|
|
30
|
+
Column('reason', 'TEXT', not_null=True),
|
|
31
|
+
Column('seen', 'INTEGER', not_null=True),
|
|
32
|
+
)),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _get(self, k: str) -> Optional[Any]:
|
|
36
|
+
row = self._one('SELECT v FROM kv WHERE k=?', (k,))
|
|
37
|
+
return json.loads(row[0]) if row else None
|
|
38
|
+
|
|
39
|
+
def _set(self, k: str, v: Any) -> None:
|
|
40
|
+
with self.transaction() as cur:
|
|
41
|
+
cur.execute('INSERT OR REPLACE INTO kv (k, v) VALUES (?, ?)', (k, json.dumps(v)))
|
|
42
|
+
|
|
43
|
+
def last_check(self) -> Optional[int]:
|
|
44
|
+
v = self._get('last_check')
|
|
45
|
+
return int(v) if v is not None else None
|
|
46
|
+
|
|
47
|
+
def checked(self, now: int, reachable: dict[str, bool]) -> None:
|
|
48
|
+
self._set('last_check', now)
|
|
49
|
+
self._set('reachable', reachable)
|
|
50
|
+
|
|
51
|
+
def reachable(self) -> dict[str, bool]:
|
|
52
|
+
return dict(self._get('reachable') or {})
|
|
53
|
+
|
|
54
|
+
def saw(self, version: str, event_id: str, sha256: str, now: int) -> bool:
|
|
55
|
+
"""Record a release; True if this is the first time."""
|
|
56
|
+
with self.transaction() as cur:
|
|
57
|
+
cur.execute('INSERT OR IGNORE INTO seen (version, event_id, sha256, first_seen) VALUES (?,?,?,?)',
|
|
58
|
+
(version, event_id, sha256, now))
|
|
59
|
+
return cur.rowcount == 1
|
|
60
|
+
|
|
61
|
+
def ack(self, version: str) -> bool:
|
|
62
|
+
with self.transaction() as cur:
|
|
63
|
+
cur.execute('UPDATE seen SET acked=1 WHERE version=?', (version,))
|
|
64
|
+
return cur.rowcount == 1
|
|
65
|
+
|
|
66
|
+
def acked(self, version: str) -> bool:
|
|
67
|
+
row = self._one('SELECT acked FROM seen WHERE version=?', (version,))
|
|
68
|
+
return bool(row and row[0])
|
|
69
|
+
|
|
70
|
+
def seen(self) -> list[Mapping[str, Any]]:
|
|
71
|
+
rows = self._all('SELECT version, event_id, sha256, first_seen, acked FROM seen ORDER BY first_seen')
|
|
72
|
+
return [dict(version=r[0], event_id=r[1], sha256=r[2], first_seen=r[3], acked=bool(r[4])) for r in rows]
|
|
73
|
+
|
|
74
|
+
def revoke(self, event_id: str, reason: str, now: int) -> None:
|
|
75
|
+
with self.transaction() as cur:
|
|
76
|
+
cur.execute('INSERT OR IGNORE INTO revoked (event_id, reason, seen) VALUES (?,?,?)', (event_id, reason, now))
|
|
77
|
+
|
|
78
|
+
def revoked(self) -> dict[str, str]:
|
|
79
|
+
return {r[0]: r[1] for r in self._all('SELECT event_id, reason FROM revoked')}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Types and records for the Updates compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
from fleetmesh_core.peering.release import ReleaseDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UpdateStateReport(TypedDict, total=False):
|
|
10
|
+
"""The report Updates.state() gives of itself."""
|
|
11
|
+
stopped: bool
|
|
12
|
+
running: Mapping[str, Any]
|
|
13
|
+
release_key: str
|
|
14
|
+
policy: Mapping[str, str]
|
|
15
|
+
channel: list[str]
|
|
16
|
+
reachable: Mapping[str, bool]
|
|
17
|
+
last_check: Optional[int]
|
|
18
|
+
newest_compatible: Optional[Mapping[str, Any]]
|
|
19
|
+
incompatible_newer: Optional[ReleaseDict]
|
|
20
|
+
revoked: list[str]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Peering plane: trust, strain, peering, budget, capacity, reporter."""
|
|
2
|
+
from . import budget, capacity, peering, reporter, strain, trust
|
|
3
|
+
from .budget.core import Budget, BudgetConfig
|
|
4
|
+
from .capacity.compartment import Capacity
|
|
5
|
+
from .peering.core import Peering, PolicyConfig
|
|
6
|
+
from .reporter.core import Reporter, ReporterConfig
|
|
7
|
+
from .strain.compartment import StrainConfig, StrainLedger
|
|
8
|
+
from .trust.compartment import Trust, TrustConfig
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'Budget',
|
|
12
|
+
'BudgetConfig',
|
|
13
|
+
'Capacity',
|
|
14
|
+
'Peering',
|
|
15
|
+
'PolicyConfig',
|
|
16
|
+
'Reporter',
|
|
17
|
+
'ReporterConfig',
|
|
18
|
+
'StrainConfig',
|
|
19
|
+
'StrainLedger',
|
|
20
|
+
'Trust',
|
|
21
|
+
'TrustConfig',
|
|
22
|
+
'budget',
|
|
23
|
+
'capacity',
|
|
24
|
+
'peering',
|
|
25
|
+
'reporter',
|
|
26
|
+
'strain',
|
|
27
|
+
'trust',
|
|
28
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""budget/1.0 over the wire, and the window it feeds.
|
|
2
|
+
|
|
3
|
+
The grant is a promise about what a subject may do to an issuer. Both sides
|
|
4
|
+
count it. The issuer meters what the peer actually did (bytes it sent,
|
|
5
|
+
events it delivered, requests it made); the subject meters the same
|
|
6
|
+
quantities from its own side and states them, cumulatively, in a chained
|
|
7
|
+
receipt on the cadence the grant names. The issuer answers every receipt
|
|
8
|
+
with an ack carrying the head it holds. When a window closes and the grace
|
|
9
|
+
has passed, the issuer evaluates measured against claimed with fleetmesh_core/window.py - under
|
|
10
|
+
the grant that governed that window - and applies the ladder's remedy with
|
|
11
|
+
the reference's AIMD. The grant it decides on waits for the next window
|
|
12
|
+
boundary; there it is written, published as a new kind 30801 under the same
|
|
13
|
+
blinded d tag, and the subject is told with peering/1.0/amend. The standing
|
|
14
|
+
ledger records the window.
|
|
15
|
+
|
|
16
|
+
The wait is not a nicety. Published the moment the grace period ended, an
|
|
17
|
+
amend landed six minutes into the next window, and a receipt is a statement
|
|
18
|
+
about one window under one grant: the issuer rejected every receipt still
|
|
19
|
+
naming the grant it had just replaced, the subject restarted its chain at
|
|
20
|
+
seq 0 on the one it had just taken up, and every window after the first
|
|
21
|
+
closed on a claim from its first ten minutes. Both nodes filed S2 against an
|
|
22
|
+
honest peer every hour, and one published a kind 30802 about it. A chain is
|
|
23
|
+
per window and survives the grant changing under it; a receipt names the
|
|
24
|
+
grant the subject held when it wrote it, and the issuer accepts the grant
|
|
25
|
+
that governed the window, the one before it, and the one current now.
|
|
26
|
+
|
|
27
|
+
Every number here is in the issuer's frame, which is the grant's: events_in
|
|
28
|
+
is events into the issuer, bytes_out is bytes the issuer sent. That is the
|
|
29
|
+
frame the specification's tier ceilings are written in and the frame
|
|
30
|
+
fleetmesh_core/badpeer.py measures in.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
from .meter import ( # noqa: E402
|
|
34
|
+
AMEND, C, KEEP_FINDINGS, METERED, PIURI, SYNC, TOLERANCE, ZERO, Meter, _observe, _ranges, metered_bytes,
|
|
35
|
+
)
|
|
36
|
+
from .core import Budget # noqa: E402
|
|
37
|
+
from .chain import OutChain # noqa: E402
|
|
38
|
+
from .evaluation import Evaluation, Repricing # noqa: E402
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
'AMEND', 'Budget', 'C', 'Evaluation', 'KEEP_FINDINGS', 'METERED', 'Meter', 'OutChain', 'PIURI', 'Repricing', 'SYNC', 'TOLERANCE', 'ZERO',
|
|
42
|
+
'_observe', '_ranges', 'metered_bytes',
|
|
43
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""The outbound receipt chain this node keeps per (peer, window).
|
|
2
|
+
|
|
3
|
+
One per window this node has sent a receipt in. `seq` and `prev` are what
|
|
4
|
+
the next receipt will carry; `heads` is every head this node produced, in
|
|
5
|
+
sequence order, so an ack naming one says how far behind the receiver is;
|
|
6
|
+
`final` marks the chain closed by the window's final receipt. Written to
|
|
7
|
+
the AccountStore before the receipt leaves (node/budget/receipts.py says
|
|
8
|
+
why), and read back whole on restart.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import asdict, dataclass, field
|
|
14
|
+
from typing import Any, Mapping, Optional, cast
|
|
15
|
+
|
|
16
|
+
from .meter import ZERO
|
|
17
|
+
|
|
18
|
+
__all__ = ['OutChain', 'OutChainDict']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from .types import OutChainDict
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class OutChain:
|
|
26
|
+
grant: str # envelope id of the grant governing the window
|
|
27
|
+
seq: int = 0
|
|
28
|
+
prev: str = ZERO # the head the first receipt chains from
|
|
29
|
+
sent_at: Optional[int] = None
|
|
30
|
+
last: Optional[dict[str, int]] = None # the counters the latest receipt claimed
|
|
31
|
+
heads: list[str] = field(default_factory=list)
|
|
32
|
+
final: bool = False
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> OutChainDict:
|
|
35
|
+
return cast(OutChainDict, asdict(self))
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls, body: Mapping[str, Any]) -> OutChain:
|
|
39
|
+
"""A chain row as the AccountStore stored it. Rows written before
|
|
40
|
+
`heads` and `final` were kept read as empty and open."""
|
|
41
|
+
return cls(grant=body.get('grant') or '', seq=int(body.get('seq', 0)), prev=body.get('prev') or ZERO,
|
|
42
|
+
sent_at=body.get('sent_at'), last=body.get('last'),
|
|
43
|
+
heads=list(body.get('heads') or []), final=bool(body.get('final', False)))
|