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.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,461 @@
1
+ """Sidecars: fronting off-the-shelf projects the node does not reimplement.
2
+
3
+ This compartment does not speak git smart-http, the OCI distribution
4
+ protocol or Magpie's media API itself - ngit-grasp, an ncontainer-compatible
5
+ gateway and magpie already do, and reimplementing any of them (Option B,
6
+ considered and rejected each time) buys nothing a sidecar does not already
7
+ give for less code and less to keep current with upstream. What this
8
+ compartment owns is the bridge:
9
+
10
+ * which capabilities this node declares, and the endpoint tags a leaf
11
+ reads to find the sidecar - all opt-in, all carried on the kind 11801
12
+ descriptor the same way mint/price already are (see
13
+ PaidFetch.descriptor_tags).
14
+ * whether the sidecar actually speaks its protocol. Every sidecar ships
15
+ its own self-tests (a git `info/refs`, an OCI `/v2/`, a magpie files
16
+ route): they run when the compartment starts, on the usual cadence after
17
+ that, and on demand through the `sidecar_test` verb. A capability is
18
+ declared ONLY while the tests pass - a config flag alone declares
19
+ nothing - and when the verified set changes the compartment rebuilds the
20
+ descriptor there and then, so a kind 11801 never advertises a protocol
21
+ nothing behind this node can answer. A failing sidecar is `trouble()`
22
+ the way every other mechanism here is.
23
+ * nothing about payment: PaidFetch.quote_session (GRASP, coarse per
24
+ clone/fetch session) and PaidFetch.quote (ncontainer and magpie, per blob -
25
+ a resolved OCI digest or a Magpie file sha256 is priced exactly like any
26
+ other Blossom fetch) are the payment surface, called from the reverse
27
+ proxy in front of each sidecar, not from here.
28
+
29
+ What a sidecar IS lives in a manifest (node/sidecar_manifest.py), not here:
30
+ the node ships three in node/planes/services/sidecars/manifests/ (grasp, ncontainer, magpie);
31
+ an operator adds a fourth with `sidecars.<name>.manifest`; a plugin may
32
+ declare more. This compartment reads the `sidecars` config block, resolves
33
+ each entry to a manifest, runs the manifest's checks and declares what
34
+ passes. Adding a sidecar is a JSON file - not a change to this one.
35
+
36
+ A node with no `sidecars` block behaves exactly as before: no capability
37
+ tag, no endpoint tag, nothing to test, nothing in `trouble()`.
38
+ """
39
+ from __future__ import annotations
40
+
41
+ import urllib.parse
42
+ from typing import Any, Callable, Mapping, MutableMapping, NamedTuple, Optional
43
+
44
+ from node.transport import netfetch
45
+ from . import manifest as manifest_mod
46
+ from node import compartment as compartment_mod
47
+ from node.compartment import stores as st
48
+ from node.channels import Channel, INFO, NOTE
49
+ from node.metrics import COUNTER, Metric
50
+ from node.compartment import Command, Every, Need
51
+ from node.config.parameter import Parameter
52
+ from fleetmesh_core.wire import validate
53
+ from node.config import SidecarEntry, sidecars_of, SidecarsConfig
54
+ from .manifest import Check, ManifestError, Sidecar # noqa: F401 - the compartment's own vocabulary
55
+
56
+ # The origin whose capability identifiers the schema digest governs: only a
57
+ # shipped manifest may declare under it. Derived from the schema, never
58
+ # written down here (node/ names no host; see test_node's unplug scan).
59
+ RESERVED_ORIGIN = validate.FLEETMESH_ORIGIN.rstrip('/')
60
+
61
+ # The manifests the node ships, in descriptor order. Operator and plugin
62
+ # manifests are appended behind them, in config order.
63
+ BUILTINS: tuple[Sidecar, ...] = manifest_mod.builtins()
64
+ _BUILTIN_CAPS = {s.name: s.capability for s in BUILTINS}
65
+ CAP_GRASP = _BUILTIN_CAPS['grasp']
66
+ CAP_NCONTAINER = _BUILTIN_CAPS['ncontainer']
67
+ CAP_MAGPIE = _BUILTIN_CAPS['magpie']
68
+ SIDECARS = BUILTINS # the historical name; the live set is Sidecars.registry()
69
+ _BUILTIN_BY_NAME: dict[str, Sidecar] = {s.name: s for s in BUILTINS}
70
+
71
+
72
+ from .types import SidecarStatusReport
73
+
74
+
75
+ def resolve_manifest(name: str, entry: Mapping[str, Any],
76
+ extra: Mapping[str, Sidecar] = {}) -> Sidecar:
77
+ """The manifest a config entry stands for: inline or `@file` when given,
78
+ else a built-in or plugin-declared manifest of that name. A manifest's
79
+ `name` must match the entry key it is configured under - the key is the
80
+ systemd unit and the Caddy route, the manifest the protocol."""
81
+ raw = entry.get('manifest')
82
+ if raw is None:
83
+ s = _BUILTIN_BY_NAME.get(name) or extra.get(name)
84
+ if s is None:
85
+ raise ManifestError('sidecars.%s: not a built-in (%s) and no manifest given' % (
86
+ name, ', '.join(_BUILTIN_BY_NAME)))
87
+ return s
88
+ if isinstance(raw, str):
89
+ if not raw.startswith('@'):
90
+ raise ManifestError('sidecars.%s.manifest must be "@/path/to/manifest.json" or an object' % name)
91
+ s = manifest_mod.load_file(raw[1:], origin=RESERVED_ORIGIN)
92
+ else:
93
+ s = manifest_mod.parse(raw, origin=RESERVED_ORIGIN)
94
+ if s.name != name:
95
+ raise ManifestError('sidecars.%s: the manifest calls itself %r; the key and the name must agree' % (name, s.name))
96
+ return s
97
+
98
+
99
+ # How often a tick re-runs the checks - matching the cadence other
100
+ # compartments use for anything that costs a network round trip. The first
101
+ # run is at start(), not a cadence later.
102
+ _CHECK_EVERY_SECONDS = 300
103
+ _PROBE_TIMEOUT = 3.0
104
+
105
+
106
+ class ProbeResult(NamedTuple):
107
+ """What one check said. `status` is the HTTP status when there was one."""
108
+ name: str
109
+ ok: bool
110
+ status: Optional[int]
111
+ why: str
112
+
113
+
114
+ Fetcher = Callable[[str, str], tuple[int, Mapping[str, str]]]
115
+
116
+
117
+ def _http(url: str, method: str, timeout: float = _PROBE_TIMEOUT) -> tuple[int, Mapping[str, str]]:
118
+ """One request to the sidecar: (status, headers). Only http/https are
119
+ ever dialled - the URL is operator config, but urlopen would otherwise
120
+ honour file:// and ftp:// too - and it goes through netfetch so redirects
121
+ stay inside the same policy. allow_private=True because sidecars are
122
+ local by design. Raises on anything that is not an HTTP answer."""
123
+ parts = urllib.parse.urlsplit(url)
124
+ if parts.scheme not in ('http', 'https'):
125
+ raise ValueError('not an http(s) URL')
126
+ try:
127
+ _body, headers = netfetch.fetch(url, max_bytes=4096, timeout=timeout, allow_private=True, method=method)
128
+ return 200, headers
129
+ except netfetch.FetchHTTPError as e:
130
+ return e.code, e.headers or {}
131
+
132
+
133
+ def _reachable(url: str, timeout: float = _PROBE_TIMEOUT) -> bool:
134
+ """HEAD the sidecar. Reachable means it answered HTTP at all: a 4xx is a
135
+ live process that disliked HEAD, not an absent one."""
136
+ try:
137
+ status, _ = _http(url, 'HEAD', timeout)
138
+ return status < 500
139
+ except Exception:
140
+ return False
141
+
142
+
143
+ def run_check(fetch: Fetcher, endpoint: str, check: Check) -> ProbeResult:
144
+ """One check against one endpoint, never raising."""
145
+ url = endpoint.rstrip('/') + check.path
146
+ try:
147
+ status, headers = fetch(url, check.method)
148
+ except Exception as e:
149
+ return ProbeResult(check.name, False, None, '%s: %s' % (type(e).__name__, str(e)[:80] or 'no answer'))
150
+ if status not in check.expect:
151
+ return ProbeResult(check.name, False, status, 'HTTP %d, wanted one of %s' % (status, sorted(check.expect)))
152
+ if status == 200 and check.content_type:
153
+ ctype = next((v for k, v in headers.items() if k.lower() == 'content-type'), '')
154
+ if not str(ctype).lower().startswith(check.content_type):
155
+ return ProbeResult(check.name, False, status, 'content-type %r is not %s' % (ctype, check.content_type))
156
+ return ProbeResult(check.name, True, status, '')
157
+
158
+
159
+ class Sidecars(compartment_mod.Compartment):
160
+ """Declares one capability per sidecar whose self-tests pass, and keeps
161
+ testing the sidecars behind them. See module docstring for what it does
162
+ not do."""
163
+
164
+ name = 'sidecars'
165
+ reads = ('sidecars',) + tuple(s.name for s in BUILTINS) # the old top-level keys, one release more
166
+ live = ('sidecars',) + tuple(s.name for s in BUILTINS)
167
+ config_cls = SidecarsConfig
168
+ STORES = (
169
+ st.Store(key='probes', description='recent sidecar probe results',
170
+ kind=st.RING, shape=dict, bound=256),
171
+ )
172
+ CHANNELS = (
173
+ Channel('sidecar_verified', 'Sidecar self-tests passed and capability declared', level=INFO, say='sidecar {name} verified and declared', tags=('sidecars', 'probe')),
174
+ Channel('sidecar_failed', 'Sidecar self-test failed and capability retracted', level=NOTE, say='sidecar {name} self-test failed: {why}', tags=('sidecars', 'probe')),
175
+ )
176
+ METRICS = (
177
+ Metric(name='sidecars_verified_total', description='Times sidecars verified.', kind=COUNTER, counts='sidecar_verified', tags=('sidecars', 'probe')),
178
+ Metric(name='sidecars_failed_total', description='Times sidecar probes failed.', kind=COUNTER, counts='sidecar_failed', tags=('sidecars', 'probe')),
179
+ )
180
+ NEEDS = (
181
+ Need('cfg', 'cfg', getter=True),
182
+ Need('descriptor', 'descriptor'),
183
+ Need('redescribe', 'redescribe'),
184
+ )
185
+ EVERY = (Every(seconds=_CHECK_EVERY_SECONDS, run='_check_endpoints'),)
186
+ COMMANDS = (
187
+ Command(
188
+ key='sidecars',
189
+ name='Configured Sidecars',
190
+ description='each configured sidecar, its endpoint, whether it is verified and its last self-test',
191
+ tags=('sidecars', 'status'),
192
+ permission='sidecars/sidecars',
193
+ run='list_sidecars',
194
+ reply='dict',
195
+ ),
196
+ Command(
197
+ key='sidecar_test',
198
+ name='Sidecar Self-Test',
199
+ description='run the self-tests of one sidecar (or all) now and redeclare if the result changed',
200
+ tags=('sidecars', 'test'),
201
+ parameters=(
202
+ Parameter(key='name', name='name', description='test: only this sidecar (default all enabled)', type=str, default=None, required=False),
203
+ ),
204
+ permission='sidecars/sidecar_test',
205
+ run='run_sidecar_test',
206
+ reply='{name: {verified, checks: [{name, ok, status, why}], at}}',
207
+ ),
208
+ )
209
+
210
+ def __init__(self, cfg: Callable[[], Mapping[str, Any]], clock: Optional[Callable[[], int]] = None,
211
+ checker: Optional[Callable[[str], bool]] = None, node: Any = None,
212
+ fetch: Fetcher = _http,
213
+ descriptor: Optional[Callable[[], Optional[Mapping[str, Any]]]] = None,
214
+ redescribe: Optional[Callable[[], Any]] = None) -> None:
215
+ super().__init__(node, clock)
216
+ self._cfg = cfg
217
+ # `checker(url) -> bool` is the older, coarser injection: when given it
218
+ # stands in for every check of a sidecar. `fetch` is the real seam.
219
+ self._checker = checker
220
+ self._fetch = fetch
221
+ self._descriptor = descriptor or (lambda: None)
222
+ self._redescribe = redescribe
223
+ self._last_checked: dict[str, int] = {} # not a store: local configured sidecars probe timestamps
224
+ self._results: dict[str, list[ProbeResult]] = {} # not a store: local configured sidecars probe results
225
+ self._verified: dict[str, bool] = {} # not a store: local configured sidecars verified flags
226
+ self._declared: dict[str, Sidecar] = {} # not a store: plugin-declared manifests, by name
227
+ self._manifest_errors: dict[str, str] = {} # not a store: manifest errors for configured sidecars
228
+ self.redeclared = 0
229
+ self._closed = False
230
+
231
+ # -- config -----------------------------------------------------------
232
+ def _entries(self) -> dict[str, SidecarEntry]:
233
+ """The `sidecars` block as it is now (old top-level keys folded in);
234
+ every sidecar reads live."""
235
+ out = {}
236
+ for name, e in sidecars_of(self._cfg()).items():
237
+ e = e or {}
238
+ out[name] = SidecarEntry(enabled=bool(e.get('enabled', False)), endpoint=e.get('endpoint'),
239
+ manifest=e.get('manifest'))
240
+ return out
241
+
242
+ def block(self, name: str) -> SidecarEntry:
243
+ return self._entries().get(name, SidecarEntry())
244
+
245
+ def enabled(self, name: str) -> bool:
246
+ return self.block(name).on
247
+
248
+ def registry(self) -> list[Sidecar]:
249
+ """Every sidecar this node knows of, in descriptor order: the
250
+ built-ins, then plugin-declared ones, then any the operator named
251
+ only by manifest. A configured entry whose manifest does not resolve
252
+ is remembered as a manifest problem (trouble()) and skipped - a
253
+ bad manifest declares nothing, same as a failing check."""
254
+ self._manifest_errors = {} # not a store: reset on registry evaluation
255
+ out: list[Sidecar] = list(BUILTINS)
256
+ seen = {s.name for s in out}
257
+ for name, s in self._declared.items():
258
+ if name not in seen:
259
+ out.append(s); seen.add(name)
260
+ for name, e in self._entries().items():
261
+ if e.manifest is None and name in seen:
262
+ continue
263
+ try:
264
+ s = resolve_manifest(name, {'manifest': e.manifest}, self._declared)
265
+ except (ManifestError, OSError) as err:
266
+ self._manifest_errors[name] = str(err)
267
+ continue
268
+ if name in seen: # operator manifest overrides a plugin's, never a built-in
269
+ if name in _BUILTIN_BY_NAME:
270
+ self._manifest_errors[name] = 'sidecars.%s: %s is a built-in; its manifest is not overridable' % (name, name)
271
+ continue
272
+ out = [s if x.name == name else x for x in out]
273
+ else:
274
+ out.append(s); seen.add(name)
275
+ return out
276
+
277
+ def manifest(self, name: str) -> Sidecar:
278
+ for s in self.registry():
279
+ if s.name == name:
280
+ return s
281
+ raise KeyError('no sidecar named %r (have: %s)' % (name, ', '.join(x.name for x in self.registry())))
282
+
283
+ def declare(self, sidecar: Sidecar) -> None:
284
+ """A plugin (or test) adds a manifest the node did not ship. Refused
285
+ under the reserved origin, like any other third-party manifest."""
286
+ manifest_mod.parse(sidecar.to_dict(), origin=RESERVED_ORIGIN)
287
+ self._declared[sidecar.name] = sidecar
288
+
289
+ # Readable names for the individual sidecars; enabled(name) is the
290
+ # generic form and what the table-driven paths use.
291
+ def grasp_enabled(self) -> bool:
292
+ return self.enabled('grasp')
293
+
294
+ def ncontainer_enabled(self) -> bool:
295
+ return self.enabled('ncontainer')
296
+
297
+ def magpie_enabled(self) -> bool:
298
+ return self.enabled('magpie')
299
+
300
+ def _endpoints(self) -> dict[str, str]:
301
+ """Enabled entries whose manifest resolved. One with a manifest
302
+ problem is not an endpoint: it is neither tested nor declared."""
303
+ known = {s.name for s in self.registry()} - set(self._manifest_errors)
304
+ return {n: str(e.endpoint) for n, e in self._entries().items() if e.on and n in known}
305
+
306
+ def verified(self, name: str) -> bool:
307
+ """Enabled now AND its last self-test passed."""
308
+ return self.enabled(name) and self._verified.get(name, False)
309
+
310
+ # -- what the descriptor says ------------------------------------------
311
+ def capability_protocols(self) -> list[str]:
312
+ """Extra `c` protocol identifiers this node's descriptor should carry,
313
+ alongside the required ones `descriptor.build` always adds. Only the
314
+ sidecars whose self-tests pass: enabling one declares nothing until
315
+ it answers."""
316
+ return [s.capability for s in self.registry() if self.verified(s.name)]
317
+
318
+ def descriptor_tags(self) -> list[list[str]]:
319
+ """<name>-endpoint, only for a verified sidecar - see
320
+ schema/kinds.json's note on kind 11801."""
321
+ eps = self._endpoints()
322
+ return [[s.tag, eps[s.name]] for s in self.registry() if s.name in eps and self.verified(s.name)]
323
+
324
+ def _declared_on_descriptor(self) -> set[str]:
325
+ desc = self._descriptor()
326
+ if not desc:
327
+ return set()
328
+ caps = {t[1] for t in desc.get('tags', []) if len(t) > 1 and t[0] == 'c'}
329
+ return {s.name for s in self.registry() if s.capability in caps}
330
+
331
+ # -- the self-tests ------------------------------------------------------
332
+ def test(self, name: str, now: Optional[int] = None) -> list[ProbeResult]:
333
+ """Run one sidecar's checks now. Records the verdict; does not redeclare."""
334
+ s = self.manifest(name)
335
+ at = now if now is not None else self.clock()
336
+ url = self._endpoints().get(name)
337
+ if url is None:
338
+ results = [ProbeResult('enabled', False, None, 'not enabled')]
339
+ elif self._checker is not None:
340
+ up = self._checker(url)
341
+ results = [ProbeResult(c.name, up, 200 if up else None, '' if up else 'no answer') for c in s.checks]
342
+ else:
343
+ results = [run_check(self._fetch, url, c) for c in s.checks]
344
+ self._last_checked[name] = at
345
+ self._results[name] = results
346
+ self._verified[name] = url is not None and all(r.ok for r in results)
347
+ for r in results:
348
+ if not r.ok:
349
+ self.refusals.refuse('sidecar', 'self-test %s failed' % r.name, subject=name,
350
+ detail={'endpoint': url or '', 'status': r.status, 'why': r.why}, at=at)
351
+ return results
352
+
353
+ def test_all(self, now: Optional[int] = None, only: Optional[str] = None) -> dict[str, list[ProbeResult]]:
354
+ names = [only] if only else list(self._endpoints())
355
+ return {n: self.test(n, now) for n in names}
356
+
357
+ def redeclare_if_changed(self) -> bool:
358
+ """Rebuild the descriptor when what should be declared differs from
359
+ what the current one says. True when it was rebuilt."""
360
+ want = {s.name for s in self.registry() if self.verified(s.name)}
361
+ if want == self._declared_on_descriptor():
362
+ return False
363
+ if self._redescribe is None:
364
+ return False
365
+ self._redescribe()
366
+ self.redeclared += 1
367
+ return True
368
+
369
+ # -- lifecycle -----------------------------------------------------------
370
+ def start(self) -> None:
371
+ """Test every enabled sidecar before anything is declared, and bring
372
+ the descriptor into agreement at once rather than a cadence later."""
373
+ if self._endpoints():
374
+ self.test_all(self.clock())
375
+ self._every_last['_check_endpoints'] = self.clock()
376
+ self.redeclare_if_changed()
377
+
378
+ def _check_endpoints(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
379
+ now = now if now is not None else self.clock()
380
+ out: list[tuple[Any, ...]] = []
381
+ changed = False
382
+ for name in self._endpoints():
383
+ was = self._verified.get(name)
384
+ self.test(name, now)
385
+ is_ok = self._verified[name]
386
+ if was is not None and was != is_ok:
387
+ out.append(('sidecar', name, 'verified' if is_ok else 'failing'))
388
+ changed = changed or (was != is_ok)
389
+ if changed and self.redeclare_if_changed():
390
+ out.append(('sidecar', 'descriptor', 'redeclared'))
391
+ return out
392
+
393
+ _tick_runs_every = True
394
+
395
+ def tick(self, now: int) -> list[tuple[Any, ...]]:
396
+ out: list[tuple[Any, ...]] = []
397
+ if any(name not in self._last_checked for name in self._endpoints()):
398
+ self._every_last['_check_endpoints'] = now
399
+ out += self._check_endpoints(now)
400
+ else:
401
+ out += self.run_every(now)
402
+ changed = False
403
+ # A sidecar removed from the config stops being verified without a probe.
404
+ for name in [n for n in self._verified if n not in self._endpoints()]:
405
+ del self._verified[name]
406
+ changed = True
407
+ if changed and self.redeclare_if_changed():
408
+ out.append(('sidecar', 'descriptor', 'redeclared'))
409
+ return out
410
+
411
+ def _report(self, name: str) -> SidecarStatusReport:
412
+ return {'verified': self.verified(name), 'last_checked': self._last_checked.get(name),
413
+ 'checks': [r._asdict() for r in self._results.get(name, [])]}
414
+
415
+ def state(self) -> Mapping[str, Any]:
416
+ if self._closed:
417
+ return {'stopped': True}
418
+ out: MutableMapping[str, Any] = {}
419
+ entries = self._entries()
420
+ for s in self.registry():
421
+ if s.name in entries:
422
+ out[s.name] = {'enabled': self.enabled(s.name), 'endpoint': self.block(s.name).endpoint,
423
+ 'capability': s.capability, 'builtin': s.builtin,
424
+ 'reachable': self._verified.get(s.name), **self._report(s.name)}
425
+ else:
426
+ out[s.name] = {'enabled': False, 'capability': s.capability, 'builtin': s.builtin}
427
+ for name, why in self._manifest_errors.items():
428
+ out[name] = {'enabled': entries[name].enabled, 'manifest_error': why}
429
+ out['redeclared'] = self.redeclared
430
+ return out
431
+
432
+ def trouble(self) -> list[str]:
433
+ out = []
434
+ eps = self._endpoints()
435
+ self.registry()
436
+ for name, why in self._manifest_errors.items():
437
+ out.append('%s sidecar has no usable manifest: %s' % (name, why))
438
+ for name, url in eps.items():
439
+ if self._verified.get(name) is False:
440
+ failed = [r for r in self._results.get(name, []) if not r.ok]
441
+ out.append('%s sidecar is configured at %s but not verified: %s' % (
442
+ name, url, '; '.join('%s (%s)' % (r.name, r.why) for r in failed) or 'untested'))
443
+ return out
444
+
445
+ def list_sidecars(self) -> Mapping[str, Any]:
446
+ return {n: {'endpoint': url, **self._report(n)} for n, url in self._endpoints().items()}
447
+
448
+ def run_sidecar_test(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
449
+ only = args.get('name')
450
+ if only is not None:
451
+ self.manifest(only) # KeyError names the ones there are
452
+ if only is not None and not self.enabled(only):
453
+ raise KeyError('%s is not enabled' % only)
454
+ now = self.clock()
455
+ results = self.test_all(now, only)
456
+ redeclared = self.redeclare_if_changed()
457
+ return {n: {'verified': all(r.ok for r in rs), 'checks': [r._asdict() for r in rs], 'at': now}
458
+ for n, rs in results.items()} | {'redeclared': redeclared}
459
+
460
+ def stop(self) -> Any:
461
+ self._closed = True