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,458 @@
1
+ """COMMANDS: what a compartment can be asked to do, declared where it is.
2
+
3
+ Split out of node/compartment.py when it became a package. node/commands
4
+ re-exports these for callers that think in commands rather than compartments.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import dataclasses
9
+ import inspect
10
+ from typing import Any, Callable, Mapping, Optional, Sequence, TypedDict, Union
11
+
12
+ from ..config.parameter import Parameter, ParameterRecord
13
+ from ..interfaces import CommandSpec
14
+ from .problem import (DuplicateProblem, Problem, Problems, ReferenceProblem,
15
+ TypeMismatchProblem)
16
+
17
+ ParameterValue = Any
18
+
19
+
20
+ class CommandRecord(TypedDict, total=False):
21
+ """A command as `meshnode commands`, control socket, and docs read it."""
22
+ key: str
23
+ name: str
24
+ description: str
25
+ tags: list[str]
26
+ parameters: list[ParameterRecord]
27
+ permission: str
28
+ permission_enforced: bool
29
+ process: list[str]
30
+ process_enforced: bool
31
+ reply: str
32
+ reads: list[str]
33
+ writes: list[str]
34
+ compartment: str
35
+
36
+
37
+ class CommandError(ValueError):
38
+ """The caller asked wrongly: an argument missing, unknown, or not what
39
+ the command takes. A refusal the caller reads, never a fault in the
40
+ node - the control socket answers it as one (`ControlError`) and keeps
41
+ it out of the error ring that counts bugs."""
42
+
43
+
44
+ @dataclasses.dataclass(frozen=True)
45
+ class Command:
46
+ """One operation a compartment exposes, declared where the compartment is.
47
+
48
+ Declared once, in the compartment's COMMANDS, and answered by the base:
49
+ `Compartment.command` finds the declaration, checks the arguments against
50
+ it, and calls what `run` names. A compartment that declares COMMANDS does
51
+ not write its own `command()` - the if-chain that used to sit beside a
52
+ hand-kept `commands` dict was the second copy of every declaration.
53
+
54
+ Fields:
55
+ key: short name of the command on the compartment (e.g. 'show', 'test')
56
+ name: human-readable title (e.g. 'Show announce status')
57
+ description: full prose explanation of what it does
58
+ tags: categorization tags (e.g. ('announce', 'status'))
59
+ parameters: the arguments it takes, as Parameters. Nothing else is
60
+ accepted: an argument it does not declare is refused by name, so a
61
+ misspelt optional one is an error rather than silently its default.
62
+ permission: UCAN ability required (e.g. 'announce/show') - declared,
63
+ not yet enforced (see `describe`)
64
+ process: sequence of required processes / sidecars (e.g. ('sidecar',))
65
+ run: what answers it. The name of a method on the compartment, taking
66
+ either nothing or the checked arguments as one dict; or a callable
67
+ `(compartment, args)`. Checked when the compartment class is defined
68
+ (`problems`), so a name that does not resolve never reaches a node.
69
+ reply: description of return structure
70
+ reads / writes: the stores it reads and writes - this compartment's by
71
+ key, another's as `name.key` (one it requires or uses). Checked when
72
+ the compartment class is defined; the census reads them to say what
73
+ data each command touches.
74
+ """
75
+ key: str
76
+ name: str
77
+ description: str
78
+ tags: tuple[str, ...] = ()
79
+ parameters: tuple[Parameter[Any], ...] = ()
80
+ permission: str = ''
81
+ process: tuple[str, ...] = ()
82
+ run: Optional[Union[Callable[..., Any], str]] = None
83
+ reply: str = ''
84
+ reads: tuple[str, ...] = ()
85
+ writes: tuple[str, ...] = ()
86
+
87
+ def __init__(
88
+ self,
89
+ key: str,
90
+ name: str,
91
+ description: str,
92
+ tags: Sequence[str] = (),
93
+ parameters: Sequence[Parameter[Any]] = (),
94
+ permission: str = '',
95
+ process: Sequence[str] | str = (),
96
+ run: Optional[Union[Callable[..., Any], str]] = None,
97
+ reply: str = '',
98
+ *,
99
+ reads: Sequence[str] = (),
100
+ writes: Sequence[str] = (),
101
+ ) -> None:
102
+ object.__setattr__(self, 'key', key)
103
+ object.__setattr__(self, 'name', name)
104
+ object.__setattr__(self, 'description', description)
105
+ object.__setattr__(self, 'tags', tuple(tags))
106
+ object.__setattr__(self, 'parameters', tuple(parameters))
107
+ object.__setattr__(self, 'permission', permission)
108
+ procs = (process,) if isinstance(process, str) else tuple(process)
109
+ object.__setattr__(self, 'process', tuple(p for p in procs if p))
110
+ object.__setattr__(self, 'run', run)
111
+ object.__setattr__(self, 'reply', reply)
112
+ object.__setattr__(self, 'reads', tuple(reads))
113
+ object.__setattr__(self, 'writes', tuple(writes))
114
+
115
+ def resource(self, subsystem: str, node_did: str = '') -> str:
116
+ """The UCAN resource this command is, as `with` would name it.
117
+
118
+ Matches `Parameter.resource()` convention:
119
+ `command/<subsystem>.<key>`, prefixed with node_did if present.
120
+ """
121
+ path = 'command/%s.%s' % (subsystem, self.key)
122
+ return '%s#%s' % (node_did, path) if node_did else path
123
+
124
+ # ---- checked when the compartment class is defined ----------------------
125
+ def problems(self, cls: type) -> list[str]:
126
+ """Everything wrong with this declaration on compartment `cls`, as
127
+ sentences - collected by `Compartment.declaration_problems`, so a
128
+ command that cannot be answered fails at import, not on first use."""
129
+ out = Problems()
130
+ seen: set[str] = set()
131
+ # The annotations promise Parameters and a str-or-callable run; this
132
+ # is for the declaration that broke the promise, so look at what it
133
+ # actually holds.
134
+ params: tuple[object, ...] = tuple(self.parameters)
135
+ run: object = self.run
136
+ for p in params:
137
+ if not isinstance(p, Parameter):
138
+ out.append(TypeMismatchProblem('COMMANDS', 'node.config.Parameter', type(p).__name__,
139
+ message='command %s: parameters are node.config.Parameter, not %s' % (self.key, type(p).__name__)))
140
+ continue
141
+ if p.key in seen:
142
+ out.append(DuplicateProblem('COMMANDS', p.key, message='command %s declares parameter %r twice' % (self.key, p.key)))
143
+ seen.add(p.key)
144
+ if run is None:
145
+ out.append(Problem('command %s has no run: name the method that answers it' % self.key, surface='COMMANDS', code='missing_run', target=self.key))
146
+ elif isinstance(run, str):
147
+ fn = getattr(cls, run, None)
148
+ if fn is None or not callable(fn):
149
+ out.append(ReferenceProblem('COMMANDS', f"command {self.key}", run,
150
+ message='command %s runs %r, which %s has no method called' % (self.key, run, cls.__name__)))
151
+ elif _takes(fn, bound=False) is None:
152
+ out.append(Problem('command %s runs %s.%s, which must take nothing or one argument (the args)' % (self.key, cls.__name__, run),
153
+ surface='COMMANDS', code='invalid_signature', target=self.key))
154
+ elif not callable(run):
155
+ out.append(TypeMismatchProblem('COMMANDS', 'method name or a callable', type(run).__name__,
156
+ message='command %s: run is a method name or a callable, not %s' % (self.key, type(run).__name__)))
157
+ else:
158
+ try:
159
+ inspect.signature(run).bind(None, None)
160
+ except TypeError:
161
+ out.append(Problem('command %s: a callable run takes (compartment, args)' % self.key,
162
+ surface='COMMANDS', code='invalid_signature', target=self.key))
163
+ except ValueError:
164
+ pass # a builtin with no signature to read; called as declared
165
+ return out
166
+
167
+
168
+ # ---- answered -----------------------------------------------------------
169
+ def validate_args(self, args: Optional[Mapping[str, Any]] = None) -> Mapping[str, ParameterValue]:
170
+ """The arguments, checked against what this command declares.
171
+
172
+ Every declared one is present in what comes back - the caller's
173
+ value coerced and checked, or the default. A required one missing,
174
+ a value that is not what the parameter states, or an argument the
175
+ command does not take at all is a CommandError naming it.
176
+ """
177
+ raw = dict(args or {})
178
+ declared = {p.key for p in self.parameters}
179
+ unknown = sorted(k for k in raw if k not in declared)
180
+ if unknown:
181
+ raise CommandError('%s takes no argument %s; it takes %s'
182
+ % (self.key, ', '.join(map(repr, unknown)),
183
+ ', '.join(sorted(declared)) or 'none'))
184
+ resolved: dict[str, ParameterValue] = {}
185
+ for p in self.parameters:
186
+ v = raw.get(p.key)
187
+ if v is None:
188
+ if p.required:
189
+ raise CommandError('%s needs %s' % (self.key, p.key))
190
+ resolved[p.key] = p.value()
191
+ continue
192
+ try:
193
+ v = _coerced(p, v)
194
+ except (TypeError, ValueError) as e:
195
+ raise CommandError('%s: %s %s' % (self.key, p.key, _must(p, e))) from e
196
+ if p.validate is None and not p.ok(v):
197
+ raise CommandError('%s: %s %s, not %r' % (self.key, p.key, _must(p), str(v)[:32]))
198
+ resolved[p.key] = v
199
+ return resolved
200
+
201
+ def execute(self, target: Any, args: Optional[Mapping[str, Any]] = None) -> Any:
202
+ """Answer this command on `target` (the compartment instance)."""
203
+ resolved = self.validate_args(args)
204
+ # If target has a check_command_dependencies hook, verify them
205
+ if hasattr(target, 'check_command_dependencies'):
206
+ target.check_command_dependencies(self)
207
+ if isinstance(self.run, str):
208
+ fn = getattr(target, self.run)
209
+ return fn(resolved) if _takes(fn, bound=True) else fn()
210
+ if callable(self.run):
211
+ return self.run(target, resolved)
212
+ raise TypeError('command %s has no run' % self.key)
213
+
214
+ def to_spec(self, subsystem: str = '') -> CommandSpec:
215
+ """Convert to legacy CommandSpec dictionary for backward compatibility."""
216
+ args_spec: dict[str, str] = {}
217
+ params: tuple[object, ...] = tuple(self.parameters)
218
+ for p in params:
219
+ # Derived at class creation, before the lint: a non-Parameter is
220
+ # left for `problems()` to name rather than crashing here.
221
+ if not isinstance(p, Parameter):
222
+ continue
223
+ if getattr(p, 'required', False):
224
+ args_spec[p.key] = 'required'
225
+ elif getattr(p, 'choices', None):
226
+ args_spec[p.key] = ' | '.join(str(c) for c in p.choices)
227
+ else:
228
+ args_spec[p.key] = 'optional'
229
+ spec: CommandSpec = {
230
+ 'args': args_spec,
231
+ 'does': self.description,
232
+ 'reply': self.reply,
233
+ }
234
+ if subsystem:
235
+ spec['subsystem'] = subsystem
236
+ return spec
237
+
238
+ def describe(self) -> CommandRecord:
239
+ out: CommandRecord = {
240
+ 'key': self.key,
241
+ 'name': self.name,
242
+ 'description': self.description,
243
+ 'tags': list(self.tags),
244
+ 'permission': self.permission,
245
+ 'process': list(self.process),
246
+ }
247
+ # A declared ability must never read as an enforced one. `execute`
248
+ # validates arguments and asks the target to check dependencies; it
249
+ # does not check `permission`, and nothing authenticates a principal
250
+ # yet - the control socket is chmod 0o600 with no peer credential and
251
+ # no token. This is the same footing, and the same reason, as
252
+ # `Parameter.requires` and `config.change.ACCESS_DECLARED_ONLY`, and
253
+ # `requires_enforced: False` on a parameter row. Stated on the row
254
+ # because a row is what an operator reads.
255
+ if self.permission:
256
+ out['permission_enforced'] = False
257
+ # The dependency check is advisory in the same way: `execute` calls
258
+ # `check_command_dependencies` only when the target happens to have
259
+ # one, so a compartment without the hook declares its sidecars and
260
+ # nothing verifies them.
261
+ if self.process:
262
+ out['process_enforced'] = False
263
+ if self.reply:
264
+ out['reply'] = self.reply
265
+ if self.reads:
266
+ out['reads'] = list(self.reads)
267
+ if self.writes:
268
+ out['writes'] = list(self.writes)
269
+ if self.parameters:
270
+ out['parameters'] = [
271
+ {
272
+ 'key': p.key,
273
+ 'name': getattr(p, 'name', p.key),
274
+ 'description': getattr(p, 'description', ''),
275
+ 'type': getattr(getattr(p, 'type', object), '__name__', str(getattr(p, 'type', ''))),
276
+ 'default': getattr(p, 'default', None),
277
+ 'required': bool(getattr(p, 'required', False)),
278
+ 'live': bool(getattr(p, 'live', False)),
279
+ 'tags': list(getattr(p, 'tags', [])),
280
+ }
281
+ for p in self.parameters
282
+ ]
283
+ return out
284
+
285
+ def bind(self, target: Any) -> BoundCommand:
286
+ """Bind this command to a compartment instance."""
287
+ return BoundCommand(self, target)
288
+
289
+
290
+ @dataclasses.dataclass(frozen=True)
291
+ class Alias(Command):
292
+ """A command alias that forwards execution to a compartment command.
293
+
294
+ Declared in NODE_COMMANDS or a compartment's COMMANDS: exposes an alias
295
+ key (e.g. 'strain') that forwards to a target command (e.g. 'strain.show')
296
+ via the node's compartment registry.
297
+ """
298
+ target: str = ''
299
+
300
+ def __init__(
301
+ self,
302
+ key: str,
303
+ target: str,
304
+ description: str = '',
305
+ reply: str = '',
306
+ parameters: Sequence[Parameter[Any]] = (),
307
+ name: str = '',
308
+ tags: Sequence[str] = (),
309
+ permission: str = '',
310
+ process: Sequence[str] | str = (),
311
+ ) -> None:
312
+ super().__init__(
313
+ key=key,
314
+ name=name or key,
315
+ description=description,
316
+ tags=tags,
317
+ parameters=parameters,
318
+ permission=permission,
319
+ process=process,
320
+ run=None,
321
+ reply=reply,
322
+ )
323
+ object.__setattr__(self, 'target', target)
324
+
325
+ def problems(self, cls: type) -> list[str]:
326
+ out = Problems()
327
+ seen: set[str] = set()
328
+ params: tuple[object, ...] = tuple(self.parameters)
329
+ for p in params:
330
+ if not isinstance(p, Parameter):
331
+ out.append(TypeMismatchProblem('COMMANDS', 'node.config.Parameter', type(p).__name__,
332
+ message='alias %s: parameters are node.config.Parameter, not %s' % (self.key, type(p).__name__)))
333
+ continue
334
+ if p.key in seen:
335
+ out.append(DuplicateProblem('COMMANDS', p.key, message='alias %s declares parameter %r twice' % (self.key, p.key)))
336
+ seen.add(p.key)
337
+ if not self.target or not isinstance(self.target, str):
338
+ out.append(Problem('alias %s has no target: name the command it forwards to' % self.key,
339
+ surface='COMMANDS', code='missing_target', target=self.key))
340
+ return out
341
+
342
+
343
+ def execute(self, target: Any, args: Optional[Mapping[str, Any]] = None) -> Any:
344
+ resolved = self.validate_args(args)
345
+ subsystems = getattr(target, 'subsystems', None)
346
+ if subsystems is None and hasattr(target, 'node'):
347
+ subsystems = getattr(target.node, 'subsystems', None)
348
+ if subsystems is not None:
349
+ return subsystems.dispatch(self.target, resolved)
350
+ raise RuntimeError('cannot dispatch alias %s to %s: target has no subsystems' % (self.key, self.target))
351
+
352
+
353
+ @dataclasses.dataclass(frozen=True)
354
+ class BoundCommand:
355
+ """A Command bound to a specific compartment instance."""
356
+ command: Command
357
+ target: Any
358
+
359
+ @property
360
+ def key(self) -> str:
361
+ return self.command.key
362
+
363
+ @property
364
+ def name(self) -> str:
365
+ return self.command.name
366
+
367
+ @property
368
+ def description(self) -> str:
369
+ return self.command.description
370
+
371
+ @property
372
+ def tags(self) -> tuple[str, ...]:
373
+ return self.command.tags
374
+
375
+ @property
376
+ def parameters(self) -> tuple[Parameter[Any], ...]:
377
+ return self.command.parameters
378
+
379
+ @property
380
+ def permission(self) -> str:
381
+ return self.command.permission
382
+
383
+
384
+ @property
385
+ def process(self) -> tuple[str, ...]:
386
+ return self.command.process
387
+
388
+
389
+ def __call__(self, args: Optional[Mapping[str, Any]] = None, **kw: Any) -> Any:
390
+ merged: dict[str, ParameterValue] = dict(args or {})
391
+ merged.update(kw)
392
+ return self.command.execute(self.target, merged)
393
+
394
+
395
+
396
+ def _takes(fn: Callable[..., Any], bound: bool) -> Optional[bool]:
397
+ """Whether a run method takes the args: True, False, or None when it
398
+ takes something else. `bound` says whether `self` is already applied.
399
+
400
+ The args go only to a REQUIRED positional parameter. A method whose
401
+ positionals all have defaults - `sweep(self, now=None)` - is called bare:
402
+ its optional parameter is its own, and handing it the args dict would
403
+ set `now = {}`."""
404
+ try:
405
+ params = list(inspect.signature(fn).parameters.values())
406
+ except (TypeError, ValueError):
407
+ return None
408
+ if not bound:
409
+ params = params[1:] # self
410
+ positional = [p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)]
411
+ required = [p for p in positional if p.default is p.empty]
412
+ if len(required) > 1 or any(p.kind == p.KEYWORD_ONLY and p.default is p.empty for p in params):
413
+ return None
414
+ return len(required) == 1
415
+
416
+
417
+ def _coerced(p: Parameter[Any], v: Any) -> Any:
418
+ """What the caller sent, as the parameter's type. A command line sends
419
+ every value as a string, so an integer or a number given as one is
420
+ taken - and only those: a string is never read as a bool or a list."""
421
+ if p.coerce is not None:
422
+ return p.coerce(v)
423
+ if isinstance(v, bool):
424
+ return v
425
+ if p.type is int and isinstance(v, (str, float)):
426
+ if isinstance(v, float) and not v.is_integer():
427
+ raise ValueError('not a whole number')
428
+ return int(v)
429
+ if p.type is float and isinstance(v, (str, int)):
430
+ return float(v)
431
+ if p.type is tuple and isinstance(v, list):
432
+ return tuple(v)
433
+ return v
434
+
435
+
436
+ def _must(p: Parameter[Any], e: Optional[BaseException] = None) -> str:
437
+ if p.must:
438
+ return 'must ' + p.must
439
+ what = getattr(p.type, '__name__', str(p.type))
440
+ return 'must be %s%s' % ('one of %s' % ', '.join(map(str, p.choices)) if p.choices else 'a ' + what,
441
+ ' (%s)' % e if e is not None else '')
442
+
443
+
444
+ def declared(obj: Any) -> tuple[Command, ...]:
445
+ """The commands a compartment declares, or () for one not yet converted."""
446
+ return tuple(getattr(obj, 'COMMANDS', ()) or ())
447
+
448
+
449
+ def every_command(compartments: Sequence[Any]) -> list[CommandRecord]:
450
+ """Every declared command across compartments."""
451
+ out: list[CommandRecord] = []
452
+ for c in compartments:
453
+ c_name = getattr(c, 'name', '') or getattr(c, 'key', '') or getattr(c, '__name__', str(c))
454
+ for cmd in declared(c):
455
+ row = cmd.describe()
456
+ row['compartment'] = c_name
457
+ out.append(row)
458
+ return out
@@ -0,0 +1,163 @@
1
+ """The shared surface a compartment is built from.
2
+
3
+ Every compartment used to be constructed by hand in node/app/core.py from
4
+ whichever of the node's stores and services it happened to need, in one of
5
+ six different signature styles, with `node=self` passed alongside so the
6
+ compartment could reach anything the signature forgot. `NodeContext` is the
7
+ one typed object that replaces that: the stores, the transport, the face, the
8
+ clock, the config, and the handful of node-level callables a compartment
9
+ still has to reach. A compartment's `from_context(ctx)` says, in one place,
10
+ what it takes from the node, and a third party writing a compartment against
11
+ `subsystem/1` has a stable thing to write against.
12
+
13
+ `node` stays on the context as a labelled escape hatch, not a hidden one:
14
+ `scheduler` calls `node.propose`/`node.pull` and `trust` calls
15
+ `node.regrant`, which are NodeApp methods and not stores. Anything reaching
16
+ for `ctx.node` beyond those is a dependency that should be named here instead.
17
+
18
+ Standard library only.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import threading
24
+ from dataclasses import dataclass, field
25
+ from typing import Any, Callable, Mapping, NamedTuple, Optional, Sequence, TypeVar
26
+
27
+ from ..channels import NodeEventBus
28
+ from ..interfaces import NostrEvent
29
+ from ..config.base import Block
30
+ from ..config.node import NodeConfigDict
31
+
32
+
33
+ class NodeStores(NamedTuple):
34
+ """The four durable stores a node keeps, as one argument."""
35
+ events: Any # eventstore.NodeStore
36
+ grants: Any # grants.GrantStore
37
+ accounts: Any # accounts.AccountStore
38
+ blobs: Any # blobstore.BlobStore
39
+
40
+
41
+ def _no_descriptor() -> Optional[NostrEvent]:
42
+ return None
43
+
44
+
45
+ B = TypeVar('B', bound=Block)
46
+
47
+
48
+ @dataclass
49
+ class NodeContext:
50
+ """Explicit, typed context handed to every compartment at assembly.
51
+
52
+ `cfg` is the live config dict the node mutates on reload; a compartment
53
+ that must see changes reads through `cfg_of(key)` on each tick rather than
54
+ copying a section at construction. `descriptor` is a getter because the
55
+ descriptor is mined or loaded in start() and does not exist at the time
56
+ the context is made.
57
+ """
58
+ dir: str
59
+ ks: Any # keystore.Keystore
60
+ cfg: NodeConfigDict
61
+ clock: Callable[[], int]
62
+ events: NodeEventBus
63
+ stores: NodeStores
64
+ relays: list[str]
65
+ transport: Optional[Any] = None # transport.TransportRouter
66
+ router: Optional[Any] = None # router.ProtocolRouter
67
+ face: Optional[Any] = None # relayserver.RelayServer
68
+ host: Optional[Any] = None # transport.host.ProtocolHostRouter
69
+ peers: Optional[Any] = None # peers.Peers
70
+ policy: Optional[Any] = None # fleetmesh_core.peering.Policy
71
+ ledger: Optional[Any] = None # fleetmesh_core.accounting.standing.Ledger
72
+ timeout: float = 15.0
73
+ stop: threading.Event = field(default_factory=threading.Event)
74
+ descriptor: Callable[[], Optional[NostrEvent]] = _no_descriptor
75
+ digest: Optional[str] = None # schema digest this node speaks
76
+ node: Optional[Any] = None # the NodeApp; see module docstring
77
+ says: Optional[Any] = None # node log / channels router
78
+ # Write `cfg` back to the node directory. A compartment that changes
79
+ # what this node claims (coverage, a price) calls this rather than
80
+ # importing the app's config writer - the node owns its directory and
81
+ # the file is what the next tick reads. No-op outside a running node.
82
+ persist_cfg: Callable[[], None] = lambda: None
83
+ # Rebuild and republish the descriptor from what the compartments say
84
+ # now. For a compartment whose declaration can change after start (a
85
+ # sidecar whose self-tests begin or stop passing). No-op outside a
86
+ # running node, where there is no descriptor to correct.
87
+ redescribe: Callable[[], Any] = lambda: None
88
+ # Node-internal methods reached by compartments:
89
+ regrant: Callable[..., Any] = lambda *a, **kw: None
90
+ propose: Callable[..., Any] = lambda *a, **kw: ('declined', None)
91
+ pull: Callable[..., Any] = lambda *a, **kw: {}
92
+ amend: Callable[..., Any] = lambda *a, **kw: (None, None)
93
+ build_descriptor: Callable[[], Any] = lambda: None
94
+
95
+ @property
96
+ def _amend(self) -> Callable[..., Any]:
97
+ return self.amend
98
+
99
+ # ---- derived -------------------------------------------------------------
100
+ @property
101
+ def pubkey(self) -> str:
102
+ return getattr(self.ks, 'node_pk', '')
103
+
104
+ @property
105
+ def did(self) -> str:
106
+ return getattr(self.ks, 'did', '')
107
+
108
+ @property
109
+ def node_class(self) -> str:
110
+ return str(self.cfg.get('class', 'edge'))
111
+
112
+ def cfg_of(self, key: str, default: Any = None) -> Any:
113
+ """A top-level config key as it is *now*, for compartments that read
114
+ live. The same contract `Compartment.config()` keeps."""
115
+ return self.cfg.get(key, default)
116
+
117
+ def cfg_getter(self) -> Callable[[], Mapping[str, Any]]:
118
+ """The `lambda: self.cfg` several compartments take, named."""
119
+ return lambda: self.cfg
120
+
121
+ def block(self, block_cls: type[B]) -> B:
122
+ """A compartment's config section as its typed block, *now*.
123
+
124
+ `block_cls.key` names the section; an absent section is the default
125
+ block. The file was checked at the door (load_config) and on every
126
+ reload, so from_dict here cannot meet a shape it refuses - a block
127
+ that reads live calls this each tick and gets the current values.
128
+ """
129
+ raw = self.cfg.get(block_cls.key)
130
+ return block_cls.from_dict({} if raw is None else raw)
131
+
132
+ def block_getter(self, block_cls: type[B]) -> Callable[[], B]:
133
+ """`lambda: ctx.block(cls)`, for a compartment that reads live."""
134
+ return lambda: self.block(block_cls)
135
+
136
+ def path(self, *parts: str) -> str:
137
+ return os.path.join(self.dir, *parts)
138
+
139
+ def optional(self, name: str) -> Optional[Any]:
140
+ """A compartment named in the caller's `uses`, if the node built it;
141
+ None when it is off. Resolved once at construction: the manifest
142
+ orders `uses` before the user, so a present one is already built.
143
+ Tests that assemble without a node fall back to the attribute set on
144
+ `node` by assemble(), which is the same object either way."""
145
+ reg = getattr(self, '_registry', None)
146
+ if reg is not None:
147
+ res = reg.get(name)
148
+ if res is not None:
149
+ return res
150
+ registry = getattr(self.node, 'subsystems', None) if self.node is not None else None
151
+ if registry is not None:
152
+ res = registry.get(name)
153
+ if res is not None:
154
+ return res
155
+ return getattr(self.node, name, None) if self.node is not None else None
156
+
157
+ # ---- the previous keyword-style constructor, kept for callers ------------
158
+ @classmethod
159
+ def build(cls, directory: str, ks: Any, cfg: NodeConfigDict, clock: Callable[[], int],
160
+ events: NodeEventBus, stores: NodeStores, relays: Sequence[str],
161
+ transport: Optional[Any] = None, router: Optional[Any] = None, **more: Any) -> NodeContext:
162
+ return cls(dir=directory, ks=ks, cfg=cfg, clock=clock, events=events, stores=stores,
163
+ relays=list(relays), transport=transport, router=router, **more)