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,285 @@
1
+ """Publishing a finding as a kind 30802 report, the relay-score projection
2
+ that rides beside it, withdrawal, and the tick that drives all of it.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Mapping, Optional
7
+ from fleetmesh_core.accounting import window # noqa: E402
8
+ from fleetmesh_core.crypto import giftwrap # noqa: E402
9
+ from fleetmesh_core.peering import complaint, projection, reports # noqa: E402
10
+ from fleetmesh_core.storage import store # noqa: E402
11
+ from fleetmesh_core.wire.interfaces import NostrEvent # noqa: E402
12
+ from node.storage import grants as grants_mod # noqa: E402
13
+ from node.protocols.nostr import client as relay # noqa: E402
14
+ from .constants import FINDING_WINDOWS, PUBLISH_TRIES, REPORT_TTL
15
+ from .finding import Finding
16
+ from .shape import ReporterShape
17
+
18
+ from fleetmesh_core.identity.did import did_nostr as _did
19
+
20
+
21
+ class PublishMixin:
22
+ def publish(self: ReporterShape, p: Finding, now: Optional[int] = None) -> Optional[NostrEvent]:
23
+ now_val = now if now is not None else self.clock()
24
+ worst = p.worst
25
+ observed = (worst[0], worst[1]['measured'], worst[1]['limit']) if worst else None
26
+ claimed = (worst[0], worst[1]['claimed']) if worst and worst[1].get('claimed') is not None else None
27
+ notice = (p.thid, p.notice_at) if p.thid else None
28
+ if not p.evidence:
29
+ # Nothing the subject signed exists for this window. Active silence is
30
+ # the case that can never produce one: the subject said nothing, so
31
+ # there is nothing of its own to quote. Do not invent a citation.
32
+ reason = ['the subject signed nothing this window that could be cited']
33
+ with self._lock:
34
+ p.unpublishable = reason
35
+ p.tries = PUBLISH_TRIES # nothing about a closed window will change this
36
+ self._note_unpublishable(p, reason)
37
+ self._persist(p)
38
+ return None
39
+ # The grant this report cites travels with it. It is addressable under
40
+ # a blinded tag, so the one that governed a past window has already
41
+ # been replaced at that coordinate and no relay serves it; a report
42
+ # that did not carry its own became inadmissible to every reader
43
+ # within about an hour of being published.
44
+ envelope = self.grants.envelope_event(p.peer, grants_mod.ISSUED, p.grant)
45
+ ev = reports.build(self.ks.node_pk, p.peer, p.severity, p.grant, (p.epoch, p.epoch),
46
+ p.evidence, now_val, REPORT_TTL, observed=observed, claimed=claimed, notice=notice,
47
+ content='; '.join(str(w) for w in p.why)[:400] or p.source,
48
+ grant_envelope=envelope)
49
+ at = self.forward_clock('report:' + p.peer, now_val) if self.forward_clock else now_val
50
+ signed = store.make_signed_event(self.ks.node_sk, at, 30802, ev['tags'], ev['content'])
51
+ # Do not publish what a reader will discard. A report whose evidence
52
+ # does not resolve to something the subject signed is inadmissible, and
53
+ # publishing it is pure cost: readers count it as unsupported, which
54
+ # raises this node's own false_report_rate and lowers what its word is
55
+ # worth. The finding stays local and the grant stays zeroed.
56
+ okay, reasons = reports.admissible(signed, self.ks.node_pk, self._resolver(), giftwrap.verify_event)
57
+ if not okay:
58
+ with self._lock:
59
+ p.unpublishable = reasons
60
+ p.tries = p.tries + 1
61
+ self._note_unpublishable(p, reasons)
62
+ self._persist(p)
63
+ return None
64
+ relay.publish_all(self.relays, signed, self.timeout)
65
+ self.ledger.reported_against(p.peer)
66
+ with self._lock:
67
+ p.report = signed
68
+ self.published.append(signed)
69
+ self._persist(p)
70
+ self._project(signed, now_val)
71
+ return signed
72
+
73
+ def _project(self: ReporterShape, report: NostrEvent, now: int) -> list[NostrEvent]:
74
+ """The relay score a client can read, derived from the report just published.
75
+
76
+ A NIP-85 kind 30385 per ws or wss endpoint the subject's descriptor
77
+ declares; see fleetmesh_core/projection.py for the shape and the reason it is
78
+ adverse only. A subject that declares no relay endpoint gets none:
79
+ there is nothing a client could connect to. Addressable under d = the
80
+ relay URL, so it is stamped forward like the report it follows.
81
+ """
82
+ if not self.project:
83
+ return []
84
+ subject = [t[1] for t in report['tags'] if t[0] == 'd'][0]
85
+ desc = self.transport.peers.descriptor(subject)
86
+ endpoints = projection.relay_endpoints(desc) if desc is not None else []
87
+ out: list[NostrEvent] = []
88
+ for ev in projection.project(report, endpoints, now):
89
+ d = [t[1] for t in ev['tags'] if t[0] == 'd'][0]
90
+ at = self.forward_clock('projection:' + d, now) if self.forward_clock else now
91
+ signed = store.make_signed_event(self.ks.node_sk, at, projection.KIND, ev['tags'], ev['content'])
92
+ relay.publish_all(self.relays, signed, self.timeout)
93
+ out.append(signed)
94
+ with self._lock:
95
+ for signed_ev in out:
96
+ self.projected.append(signed_ev)
97
+ try:
98
+ lbl_ev = projection.label(report, now)
99
+ at_lbl = self.forward_clock('label:' + subject, now) if self.forward_clock else now
100
+ signed_lbl = store.make_signed_event(self.ks.node_sk, at_lbl, projection.LABEL_KIND, lbl_ev['tags'], lbl_ev['content'])
101
+ relay.publish_all(self.relays, signed_lbl, self.timeout)
102
+ with self._lock:
103
+ self.labeled.append(signed_lbl)
104
+ except Exception:
105
+ pass
106
+ return out
107
+
108
+ def _note_unpublishable(self: ReporterShape, p: Finding, reasons: list[str]) -> None:
109
+ with self._lock:
110
+ self.unpublishable.append((p.peer, p.epoch, p.severity, reasons))
111
+
112
+ def _withdraw(self: ReporterShape, p: Finding, reason: str) -> None:
113
+ body = complaint.withdraw(self.clock(), reason)
114
+ self.wire_client.send(p.peer, complaint.message('withdraw', body, self.ks.did, _did(p.peer), thid=p.thid), stored=True)
115
+ p.withdrawn = True
116
+ self._persist(p)
117
+
118
+ def retract(self: ReporterShape, p: Finding, reason: str) -> Optional[str]:
119
+ """Withdraw a finding AND take back what it did. Returns the tier restored.
120
+
121
+ A withdrawal used to say the accusation was wrong and leave its whole
122
+ effect standing: the S2 stayed in the ledger, the demotion it earned
123
+ stayed in force, and the streak stayed broken. Clearing 297 of them off
124
+ a live mesh still left four peerings at probation that a person had to
125
+ seed back by hand, which is the whole of the argument that the command
126
+ was only half a command.
127
+
128
+ Not every withdrawal may do this. The ladder is one-way by design, so
129
+ the line is WHY the finding is going, not that it is:
130
+
131
+ - the verdict was wrong - a person says so, or a receipt arrived late
132
+ and the window was repriced - and the tier it produced goes with it.
133
+ Nothing is earned; the node returns to where it was before it made
134
+ a mistake it now admits.
135
+ - the peer misbehaved and then fixed it. That is the ladder working,
136
+ and giving the tier back for it would be a two-way ladder bought
137
+ with a fault, which antifragility.html rules out by name.
138
+
139
+ It is refused outright when this node still stands behind something
140
+ adverse and newer, because then the tier in force is not this finding's
141
+ doing and is not this finding's to lift.
142
+ """
143
+ self._withdraw(p, reason)
144
+ return self._restore(p)
145
+
146
+ def _restore(self: ReporterShape, p: Finding) -> Optional[str]:
147
+ """Take the window back out of the history, and the tier back off it."""
148
+ peer, epoch = p.peer, int(p.epoch)
149
+ was = self.ledger.forget_window(peer, epoch)
150
+ if self.accounts is not None:
151
+ self.accounts.put_evaluated(peer, epoch, None)
152
+ if was is None or was == 'S0':
153
+ return None
154
+ if any(e > epoch and sev != 'S0' for e, sev in self.ledger.windows.get(peer) or []):
155
+ return None
156
+ governing = self.grants.governing(peer, grants_mod.ISSUED, epoch)
157
+ before = governing['payload'].get('tier') if governing else None
158
+ held = self.ledger.tier.get(peer)
159
+ if not before or not window.outranks(before, held or 'probation'):
160
+ return None # never a demotion, and never past where it stood
161
+ self.ledger.tier[peer] = before
162
+ if self.regrant is not None:
163
+ self.regrant(peer, before, self.clock())
164
+ self.log.append(('restored', peer, epoch, held, before))
165
+ return before
166
+
167
+ def withdraw(self: ReporterShape, peer: str, epoch: int, why: Optional[str] = None) -> Mapping[str, Any] | list[Mapping[str, Any]]:
168
+ """Withdraw one finding because a person said to.
169
+
170
+ Every other path to a withdrawal is the node deciding: a remedy landed
171
+ and the next window was clean, or the window was repriced. This one is
172
+ the operator, and it exists because the node cannot always find out on
173
+ its own that a finding was wrong. The two S2s that made it necessary
174
+ were about receipts that arrived after their window closed, and by the
175
+ time that was understood the windows had been priced under a build that
176
+ could not read them again.
177
+
178
+ A published report is not deleted. It carries its own expiration and
179
+ stands until it lapses; what a withdrawal does is tell the subject, on
180
+ the thread the notice opened, so the peer holding the accusation hears
181
+ that it is retracted from the node that made it.
182
+ """
183
+ with self._lock:
184
+ hit = [q for q in self.pending
185
+ if q.peer == peer and q.epoch == int(epoch) and not q.withdrawn]
186
+ if not hit:
187
+ raise KeyError('no finding held about %s for window %s that is not already withdrawn'
188
+ % (peer[:16], epoch))
189
+ out: list[Mapping[str, Any]] = []
190
+ for q in hit:
191
+ published = q.report
192
+ reason = why or 'withdrawn by the operator'
193
+ restored = self.retract(q, reason)
194
+ out.append({'peer': q.peer, 'window': q.epoch, 'was_published': published is not None,
195
+ 'report': published['id'] if published is not None else None,
196
+ 'restored': restored, 'why': reason})
197
+ return out[0] if len(out) == 1 else out
198
+
199
+ def tick(self: ReporterShape, now: Optional[int] = None) -> list[tuple[Any, ...]]:
200
+ """Publish what may be published; withdraw what a fix and a clean window earned.
201
+ Pending entries that are done - published or withdrawn - are dropped once
202
+ the report they made has itself expired; the list is not a history."""
203
+ with self._lock:
204
+ at = now if now is not None else self.clock()
205
+ horizon = at - REPORT_TTL
206
+ # A finding that became a report, or was withdrawn, waits for the
207
+ # report it made to expire. One that became NEITHER used to wait
208
+ # forever: the condition required a report or a withdrawal, and an
209
+ # unpublishable finding - active silence, where the subject signed
210
+ # nothing that could be cited - has neither and never will. So the
211
+ # list was not a history, as the line above says, except for
212
+ # exactly the entries that can never leave it. Four nodes carried
213
+ # 253 between them, all of them from one outage, none of them ever
214
+ # to be published, and every future silent window would have added
215
+ # another.
216
+ #
217
+ # Dropping one loses nothing that was doing any work. The remedy it
218
+ # earned was applied to the grant when the window closed, and
219
+ # standing/1.0 carries the severity in the ledger; the finding is
220
+ # only the paperwork for a report that cannot be written. It goes
221
+ # when the accounting under it goes, which is the point past which
222
+ # this node could not substantiate it to anyone, itself included.
223
+ done = []
224
+ for p in self.pending:
225
+ if p.done:
226
+ if p.opened_at < horizon:
227
+ done.append(p)
228
+ elif p.opened_at < at - FINDING_WINDOWS * (p.window_seconds or 3600):
229
+ done.append(p)
230
+ self.pending = [p for p in self.pending if p not in done] # not a store: the queue accounts keeps on disk, for persist=DURABLE
231
+ for p in done:
232
+ if self.accounts is not None:
233
+ self.accounts.drop_finding(p.fid)
234
+ now_val = now if now is not None else self.clock()
235
+ did: list[tuple[Any, ...]] = []
236
+ with self._lock:
237
+ # A finding that could not be published is retried, because a
238
+ # grant that did not resolve may be a relay that was down. One
239
+ # that has failed PUBLISH_TRIES times is not: it stays on the
240
+ # record, unpublished, and stops being attempted. Findings
241
+ # survive restarts now, so a permanent refusal that kept being
242
+ # retried logged the same line every half minute for as long as
243
+ # the node ran, and the list of them grew without bound.
244
+ todo = [p for p in self.pending if not p.done
245
+ and p.tries < PUBLISH_TRIES]
246
+ for p in todo:
247
+ # `_notify` puts a complaint on the wire, so a subject whose
248
+ # descriptor no longer resolves raises here - and used to take
249
+ # every finding behind it in `todo` with it. Same fault as the
250
+ # budget's peer loop and the scheduler's before that: one
251
+ # unreachable peer is not a reason to stop working for the rest.
252
+ try:
253
+ did += self._tick_finding(p, now_val)
254
+ except Exception as e:
255
+ self.refusals.refuse('finding', 'the subject raised', subject=p.peer,
256
+ detail={'epoch': p.epoch,
257
+ 'error': '%s: %s' % (type(e).__name__, e)}, at=now_val)
258
+ did.append(('finding failed', p.peer, p.epoch, '%s: %s' % (type(e).__name__, e)))
259
+ return did
260
+
261
+ def _tick_finding(self: ReporterShape, p: Finding, now_val: int) -> list[tuple[Any, ...]]:
262
+ """One finding's share of the tick: notice it, withdraw it, or publish it."""
263
+ if p.notice_at is None and p.severity in complaint.C['notice_required_for']:
264
+ # Opened and never noticed: the node stopped between the two.
265
+ # The notice is what the rule requires, so send it now and
266
+ # let the window run from here.
267
+ self._notify(p)
268
+ return [('noticed', p.peer, p.epoch)]
269
+ if not complaint.may_publish(p.severity, now_val, p.notice_at, p.window_seconds, reachable=True):
270
+ return []
271
+ remedy = p.remedy or {}
272
+ # fleetmesh_core.peering.complaint.resolve is the reference's reading of a remedy: every
273
+ # remedy_action but `contested` makes the finding withdraw-eligible.
274
+ # A local list of fix names restated the same rule and would drift
275
+ # from constants.remedy_actions on the next addition.
276
+ if remedy and complaint.resolve(remedy) == 'withdraw-eligible' and self.ledger.clean_windows(p.peer) >= 1:
277
+ self._withdraw(p, 'remedied: %s, and the next window was clean' % remedy['action'])
278
+ return [('withdrawn', p.peer, p.epoch)]
279
+ ev = self.publish(p, now_val)
280
+ if ev is not None:
281
+ return [('published', p.peer, p.epoch, ev['id'])]
282
+ if p.tries >= PUBLISH_TRIES:
283
+ return [('unpublishable', p.peer, p.epoch, p.severity,
284
+ '; '.join(p.unpublishable or [])[:120])]
285
+ return []
@@ -0,0 +1,193 @@
1
+ """Reading reports about a node, pricing a reporter's word, standing/1.0
2
+ asks, and inbound dispatch for complaint/1.0 and standing/1.0 messages.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import secrets
7
+ from typing import Any, Callable, Mapping, Optional
8
+ from fleetmesh_core.accounting import standing, window # noqa: E402
9
+ from fleetmesh_core.crypto import giftwrap # noqa: E402
10
+ from fleetmesh_core.peering import complaint, reports # noqa: E402
11
+ from fleetmesh_core.wire.interfaces import NostrEvent # noqa: E402
12
+ from fleetmesh_core.wire import validate # noqa: E402
13
+ from node.protocols.nostr import client as relay # noqa: E402
14
+ from .shape import ReporterShape
15
+
16
+ from fleetmesh_core.identity.did import did_nostr as _did
17
+
18
+
19
+ class ReadingMixin:
20
+ # ---- the complaint, subject side ------------------------------------------
21
+ def answer_notice(self: ReporterShape, peer: str, msg: Mapping[str, Any], budget: Optional[Any] = None) -> Mapping[str, Any]:
22
+ """What this node says when accused. If our own meter agrees with their
23
+ observation we were over and say we will throttle; if it disagrees we
24
+ contest with our own count. Both are honest; neither erases the window."""
25
+ body = msg['body']
26
+ thid = msg.get('thid') or msg['id']
27
+ self.notices_in[thid] = body # the oldest thread forgotten at the store's bound
28
+ ours = None
29
+ if budget is not None:
30
+ m = budget.usage_of_me.get(peer, {}).get(body.get('window'))
31
+ ours = m.snapshot() if m else {}
32
+ metric = body.get('metric')
33
+ if ours is not None and body.get('claimed') is not None and ours.get(metric, 0) != body.get('observed'):
34
+ remedy = complaint.remedy('contested', 'our meter says %s' % ours.get(metric, 0),
35
+ counter_evidence={'counters': ours, 'window': body.get('window')})
36
+ elif (body.get('observed') or 0) > (body.get('limit') or 0):
37
+ remedy = complaint.remedy('budget_throttled', 'over the limit on %s; throttling' % metric)
38
+ else:
39
+ remedy = complaint.remedy('clock_adjusted', 'no overrun on our side; checking the clock')
40
+ self.wire_client.send(peer, complaint.message('remedy', dict(remedy), self.ks.did, _did(peer), thid=thid), stored=True)
41
+ return dict(remedy)
42
+
43
+ # ---- reading ----------------------------------------------------------------
44
+ def _resolver(self: ReporterShape) -> Callable[[str], Optional[NostrEvent]]:
45
+ cache: dict[str, Optional[NostrEvent]] = {}
46
+
47
+ def resolve(eid: str) -> Optional[NostrEvent]:
48
+ if eid not in cache:
49
+ found = relay.query_all(self.relays, [{'ids': [eid]}], self.timeout)
50
+ cache[eid] = found[0] if found else None
51
+ return cache[eid]
52
+ return resolve
53
+
54
+ def reports_about(self: ReporterShape, target_pk: str) -> list[Mapping[str, Any]]:
55
+ """Every kind 30802 about a node, with admissibility decided here.
56
+
57
+ Reading a report is also the moment this node can price its reporter.
58
+ A report that is inadmissible is unsupported; one that is admissible
59
+ but whose own cited numbers do not produce the outcome it claims is
60
+ refuted. Both are contradicted, both are decidable here alone, and
61
+ both are what the third term of w(r) counts.
62
+ """
63
+ found = relay.query_all(self.relays, [{'kinds': [30802], '#d': [target_pk]}], self.timeout)
64
+ resolve = self._resolver()
65
+ out: list[Mapping[str, Any]] = []
66
+ for ev in found:
67
+ if not giftwrap.verify_event(ev):
68
+ continue
69
+ okay, reasons = reports.admissible(ev, ev['pubkey'], resolve, giftwrap.verify_event)
70
+ contradicted, why = (True, 'unsupported: %s' % '; '.join(reasons[:2])) if not okay else self._refuted(ev)
71
+ self.ledger.record_report(ev['pubkey'], ev['id'], contradicted)
72
+ if self.accounts is not None:
73
+ self.accounts.put_report_seen(ev['pubkey'], ev['id'], contradicted)
74
+ for cb in self.on_report_read:
75
+ cb(ev['pubkey'], target_pk, ev['id'], okay, contradicted)
76
+ out.append({
77
+ 'event': ev,
78
+ 'reporter': ev['pubkey'],
79
+ 'admissible': okay,
80
+ 'reasons': reasons,
81
+ 'contradicted': contradicted,
82
+ 'why': why,
83
+ 'severity': next((t[1] for t in ev['tags'] if t[0] == 'outcome'), None),
84
+ })
85
+ return out
86
+
87
+ @staticmethod
88
+ def _refuted(ev: NostrEvent | Mapping[str, Any]) -> tuple[bool, str]:
89
+ """(refuted, why) for an admissible report, from its own tags.
90
+
91
+ A report states what it measured, what the subject claimed and what
92
+ the limit was, and the classifier is deterministic, so a reader needs
93
+ neither the grant nor anyone's agreement to check that the outcome
94
+ follows. A report that states no numbers is not refutable: it is a
95
+ finding this reader cannot check, not one it may hold against the
96
+ reporter.
97
+ """
98
+ tags = {t[0]: t for t in ev.get('tags', []) if t}
99
+ observed, claimed = tags.get('observed'), tags.get('claimed')
100
+ if not observed or len(observed) < 4:
101
+ return False, 'no observation to check'
102
+ try:
103
+ metric, measured, limit = observed[1], int(observed[2]), int(observed[3])
104
+ said = int(claimed[2]) if claimed and len(claimed) > 2 else None
105
+ except (TypeError, ValueError):
106
+ return False, 'observation is not numeric'
107
+ got, want = reports.refuted(ev, {metric: limit}, {metric: measured},
108
+ {metric: said} if said is not None else {},
109
+ window.evaluate_window)
110
+ return got, ('refuted: its own numbers give %s' % want) if got else 'the finding follows from its evidence'
111
+
112
+ def weight_of(self: ReporterShape, reporter_pk: str) -> float:
113
+ """What this node's own ledger says a reporter's word is worth.
114
+
115
+ The whole of w(r): the tier this node granted the reporter, how long
116
+ its clean streak is, and how often its reports have turned out to be
117
+ unsupported or refuted. That third term had never been passed, so a
118
+ reporter that filed nothing but refutable findings weighed exactly as
119
+ much as an honest one, and filing a false report cost nothing - which
120
+ is the price the attack surface says a reporter pays.
121
+ """
122
+ if not self.ledger.knows(reporter_pk):
123
+ return 0.0
124
+ contradicted, evaluated = self.ledger.report_record(reporter_pk)
125
+ return window.reporter_weight(self.ledger.tier[reporter_pk], self.ledger.clean_windows(reporter_pk),
126
+ contradicted, evaluated)
127
+
128
+ def view(self: ReporterShape, target_pk: str, own_severity: str = 'S0') -> tuple[str, list[Mapping[str, Any]]]:
129
+ rs: list[Mapping[str, Any]] = [{'severity': r['severity'], 'weight': self.weight_of(r['reporter'])}
130
+ for r in self.reports_about(target_pk) if r['admissible'] and r['severity']]
131
+ return window.view(own_severity, rs), rs
132
+
133
+ # ---- standing/1.0 -------------------------------------------------------------
134
+ def ask_standing(self: ReporterShape, peer_pk: str, target_pk: str, timeout: float = 15.0) -> tuple[Optional[str], Optional[Mapping[str, Any]]]:
135
+ msg = standing.message('ask', dict(standing.ask(_did(target_pk))), self.ks.did, _did(peer_pk),
136
+ mid='standing-ask-%s' % secrets.token_hex(4))
137
+ reply = self.wire_client.request(peer_pk, msg, timeout=timeout)
138
+ if reply is None:
139
+ return None, None
140
+ return reply.message['type'].rsplit('/', 1)[1], reply.message['body']
141
+
142
+ # ---- dispatch -----------------------------------------------------------------
143
+ def handle_notice(self: ReporterShape, inbound: Any, sender: str, budget: Optional[Any] = None) -> Optional[tuple[Any, ...]]:
144
+ msg = getattr(inbound, 'message', inbound)
145
+ why = validate.check_body('complaint', 'notice', msg.get('body'))
146
+ if why is not None:
147
+ self.refusals.refuse('notice', why, subject=sender)
148
+ return ('ignored', sender, 'malformed notice: ' + why)
149
+ if budget is None:
150
+ budget = getattr(self, 'budget', None)
151
+ return ('remedied', sender, self.answer_notice(sender, msg, budget)['action'])
152
+
153
+ def handle_remedy(self: ReporterShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
154
+ msg = getattr(inbound, 'message', inbound)
155
+ why = validate.check_body('complaint', 'remedy', msg.get('body'))
156
+ if why is not None:
157
+ self.refusals.refuse('remedy', why, subject=sender)
158
+ return ('ignored', sender, 'malformed remedy: ' + why)
159
+ thid = msg.get('thid') or msg.get('id')
160
+ self.remedies_in[thid] = msg['body']
161
+ with self._lock:
162
+ hit = [p for p in self.pending if p.thid == thid]
163
+ for p in hit:
164
+ p.remedy = msg['body']
165
+ for p in hit:
166
+ self._persist(p)
167
+ return ('remedy', sender, complaint.resolve(msg['body']))
168
+
169
+ def handle_withdraw(self: ReporterShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
170
+ msg = getattr(inbound, 'message', inbound)
171
+ why = validate.check_body('complaint', 'withdraw', msg.get('body'))
172
+ if why is not None:
173
+ self.refusals.refuse('withdraw', why, subject=sender)
174
+ return ('ignored', sender, 'malformed withdraw: ' + why)
175
+ thid = msg.get('thid') or msg.get('id')
176
+ self.log.append(('withdrawn by', sender, msg['body']))
177
+ return ('withdrawn', sender, thid)
178
+
179
+ def handle_ask(self: ReporterShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
180
+ msg = getattr(inbound, 'message', inbound)
181
+ why = validate.check_body('standing', 'ask', msg.get('body'))
182
+ if why is not None:
183
+ self.refusals.refuse('ask', why, subject=sender)
184
+ return ('ignored', sender, 'malformed ask: ' + why)
185
+ answer, body = self.ledger.answer(msg['body'], willing=True)
186
+ self.wire_client.send(sender, standing.message(answer, body, self.ks.did, _did(sender), thid=msg['id']))
187
+ return ('answered', sender, answer)
188
+
189
+ def handle_unknown(self: ReporterShape, inbound: Any, sender: str) -> tuple[Any, ...]:
190
+ msg = getattr(inbound, 'message', inbound)
191
+ t = str(msg.get('type', '')) if isinstance(msg, dict) else ''
192
+ kind = t.rsplit('/', 1)[1] if '/' in t else t
193
+ return ('ignored', sender, kind)
@@ -0,0 +1,86 @@
1
+ """The shape of a Reporter as its mixins see it.
2
+
3
+ The one class is split across findings.py, publish.py, reading.py and core.py
4
+ by concern, the same way node/budget/ is split: each mixin method annotates
5
+ `self` as this Protocol so the checker knows what is on it without the mixins
6
+ inheriting from one another or falling back to Any. It is a declaration only;
7
+ Reporter satisfies it structurally and nothing here runs.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+ from typing import Any, Callable, Mapping, Optional, Protocol
13
+
14
+ from fleetmesh_core.accounting import standing
15
+ from fleetmesh_core.wire.interfaces import ComplaintNoticeBody, ComplaintRemedyBody, NostrEvent
16
+ from node.compartment import stores as st
17
+ from node.storage import grants as grants_mod
18
+ from node.compartment import Refusals
19
+ from .finding import Finding, LimitDetail
20
+
21
+
22
+ class ReporterShape(Protocol):
23
+ # class attributes on Reporter and Compartment
24
+ TROUBLE_WINDOW: int
25
+ # set in Compartment.__init__
26
+ refusals: Refusals
27
+ wire_client: Any
28
+ node: Any
29
+ # set in Reporter.__init__, in that order
30
+ regrant: Optional[Callable[[str, str, int], Any]]
31
+ ks: Any
32
+ accounts: Optional[Any]
33
+ project: bool
34
+ projected: st.Ring[NostrEvent]
35
+ labeled: st.Ring[NostrEvent]
36
+ transport: Any
37
+ grants: grants_mod.GrantStore
38
+ relays: list[str]
39
+ ledger: standing.Ledger
40
+ clock: Callable[[], int]
41
+ timeout: float
42
+ _lock: threading.RLock
43
+ pending: list[Finding]
44
+ published: st.Ring[NostrEvent]
45
+ unpublishable: st.Ring[tuple[str, int, str, list[str]]]
46
+ notices_in: st.Table[str, ComplaintNoticeBody]
47
+ remedies_in: st.Table[str, ComplaintRemedyBody]
48
+ log: st.Ring[tuple[Any, ...]]
49
+ forward_clock: Optional[Callable[[str, int], int]]
50
+ budget: Optional[Any]
51
+ on_report_read: list[Callable[[str, str, str, bool, bool], Any]]
52
+
53
+ # core.py
54
+ def _persist(self, p: Finding) -> None: ...
55
+ @property
56
+ def rejected(self) -> list[tuple[Optional[str], str, str]]: ...
57
+
58
+ # findings.py
59
+ def _open(self, peer: str, epoch: int, severity: str, evidence: list[Any],
60
+ worst: Optional[tuple[str, LimitDetail]], source: str,
61
+ raw: Any) -> Optional[Finding]: ...
62
+ def _notify(self, p: Finding) -> None: ...
63
+
64
+ # publish.py
65
+ def publish(self, p: Finding, now: Optional[int] = None) -> Optional[NostrEvent]: ...
66
+ def _project(self, report: NostrEvent, now: int) -> list[NostrEvent]: ...
67
+ def _note_unpublishable(self, p: Finding, reasons: list[str]) -> None: ...
68
+ def _withdraw(self, p: Finding, reason: str) -> None: ...
69
+ def withdraw(self, peer: str, epoch: int, why: Optional[str] = None) -> Mapping[str, Any] | list[Mapping[str, Any]]: ...
70
+ def retract(self, p: Finding, reason: str) -> Optional[str]: ...
71
+ def _restore(self, p: Finding) -> Optional[str]: ...
72
+ def _tick_finding(self, p: Finding, now_val: int) -> list[tuple[Any, ...]]: ...
73
+
74
+ # reading.py
75
+ def _resolver(self) -> Callable[[str], Optional[NostrEvent]]: ...
76
+ def reports_about(self, target_pk: str) -> list[Mapping[str, Any]]: ...
77
+ def _refuted(self, ev: Any) -> tuple[bool, str]: ...
78
+ def weight_of(self, reporter_pk: str) -> float: ...
79
+ def view(self, target_pk: str, own_severity: str = 'S0') -> tuple[str, list[Mapping[str, Any]]]: ...
80
+ def ask_standing(self, peer_pk: str, target_pk: str, timeout: float = 15.0) -> tuple[Optional[str], Optional[Mapping[str, Any]]]: ...
81
+ def answer_notice(self, peer: str, msg: Any, budget: Optional[Any] = None) -> Mapping[str, Any]: ...
82
+ def handle_notice(self, inbound: Any, sender: str, budget: Optional[Any] = None) -> Optional[tuple[Any, ...]]: ...
83
+ def handle_remedy(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
84
+ def handle_withdraw(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
85
+ def handle_ask(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
86
+ def handle_unknown(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
@@ -0,0 +1,50 @@
1
+ """Types and records for the Reporter compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional, TypedDict
5
+
6
+ from fleetmesh_core.wire.interfaces import ComplaintRemedyBody, NostrEvent
7
+
8
+
9
+ class LimitDetail(TypedDict, total=False):
10
+ """One per-limit entry of fleetmesh_core.accounting.window.evaluate's `detail`: what was
11
+ measured against what limit, and what the subject's receipt claimed."""
12
+ measured: float
13
+ claimed: Optional[float]
14
+ limit: float
15
+ overrun: float
16
+ divergence: float
17
+ severity: str
18
+
19
+
20
+ class FindingDict(TypedDict, total=False):
21
+ """The JSON serialization shape of a Finding."""
22
+ subject: str
23
+ severity: str
24
+ evidence: list[tuple[Any, ...]]
25
+ worst: Optional[tuple[str, LimitDetail]]
26
+ source: str
27
+ grant: str
28
+ window_seconds: int
29
+ opened_at: int
30
+ notice_at: Optional[int]
31
+ thid: Optional[str]
32
+ remedy: Optional[ComplaintRemedyBody]
33
+ report: Optional[NostrEvent]
34
+ withdrawn: bool
35
+ why: list[Any]
36
+ measured: dict[str, float]
37
+ state: Optional[str]
38
+ limits: dict[str, float]
39
+ evidence_partial: bool
40
+ tries: int
41
+ unpublishable: Optional[list[str]]
42
+
43
+
44
+ class ReporterStateReport(TypedDict):
45
+ pending: int
46
+ published: int
47
+ withdrawn: int
48
+ noticed: int
49
+ unpublishable: int
50
+ malformed: int
@@ -0,0 +1,9 @@
1
+ """What this node has survived, and the reserves it sets.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+ """
5
+ from .compartment import StrainLedger
6
+ from .types import FlagTreeReport, StrainStateReport
7
+
8
+ __all__ = ['StrainLedger', 'FlagTreeReport', 'StrainStateReport']
9
+