skcomms 0.2.0__tar.gz → 0.2.2__tar.gz

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 (356) hide show
  1. {skcomms-0.2.0 → skcomms-0.2.2}/PKG-INFO +1 -1
  2. {skcomms-0.2.0 → skcomms-0.2.2}/pyproject.toml +1 -1
  3. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/server.py +56 -2
  4. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/ack.py +85 -18
  5. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/api.py +469 -77
  6. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/capauth_validator.py +215 -42
  7. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/cli.py +748 -0
  8. skcomms-0.2.2/src/skcomms/cluster.py +219 -0
  9. skcomms-0.2.2/src/skcomms/config.py +415 -0
  10. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_group_gate.py +27 -14
  11. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_pipeline.py +52 -18
  12. skcomms-0.2.2/src/skcomms/core.py +1471 -0
  13. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/crypto.py +11 -1
  14. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/discovery.py +24 -7
  15. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/federation.py +85 -1
  16. skcomms-0.2.2/src/skcomms/home.py +293 -0
  17. skcomms-0.2.2/src/skcomms/housekeeping.py +260 -0
  18. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/integration.py +41 -12
  19. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/key_exchange.py +31 -11
  20. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/mailbox.py +287 -41
  21. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/nostr_discovery.py +165 -5
  22. skcomms-0.2.2/src/skcomms/observability.py +398 -0
  23. skcomms-0.2.2/src/skcomms/operator_probe.py +246 -0
  24. skcomms-0.2.2/src/skcomms/outbox.py +1134 -0
  25. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/outbox_migrate.py +154 -0
  26. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pairing.py +4 -0
  27. skcomms-0.2.2/src/skcomms/pairing_mirror.py +103 -0
  28. skcomms-0.2.2/src/skcomms/paths.py +436 -0
  29. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/peers.py +35 -0
  30. skcomms-0.2.2/src/skcomms/public_pairing.py +286 -0
  31. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/queue.py +23 -6
  32. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/registry.py +10 -4
  33. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/router.py +475 -184
  34. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/signaling.py +37 -7
  35. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/signing.py +11 -0
  36. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/skfed_resolve.py +28 -86
  37. skcomms-0.2.2/src/skcomms/skworld_manifest.py +82 -0
  38. skcomms-0.2.2/src/skcomms/ssrf.py +398 -0
  39. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/store_forward.py +32 -11
  40. skcomms-0.2.2/src/skcomms/syncthing_rail.py +920 -0
  41. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/tofu.py +59 -0
  42. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transport.py +32 -1
  43. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/broker_server.py +9 -4
  44. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/file.py +318 -15
  45. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/http_s2s.py +189 -15
  46. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/syncthing.py +104 -1
  47. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/webrtc.py +78 -6
  48. skcomms-0.2.2/src/skcomms/trustbackup.py +551 -0
  49. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/PKG-INFO +1 -1
  50. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/SOURCES.txt +51 -14
  51. skcomms-0.2.2/tests/test_access_nonce_durable.py +216 -0
  52. skcomms-0.2.2/tests/test_ack_bind.py +380 -0
  53. skcomms-0.2.2/tests/test_api_cors.py +105 -0
  54. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_federation_inbox.py +31 -9
  55. skcomms-0.2.2/tests/test_api_health.py +97 -0
  56. skcomms-0.2.2/tests/test_api_send_signed_wire.py +247 -0
  57. skcomms-0.2.2/tests/test_broker_server.py +92 -0
  58. skcomms-0.2.2/tests/test_capauth_failclosed.py +249 -0
  59. skcomms-0.2.2/tests/test_capauth_resolution.py +58 -0
  60. skcomms-0.2.2/tests/test_capauth_revoked_expired.py +180 -0
  61. skcomms-0.2.2/tests/test_cluster_schema.py +165 -0
  62. skcomms-0.2.2/tests/test_consent_ban_gate_fail_closed.py +212 -0
  63. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_group_gate.py +111 -14
  64. skcomms-0.2.2/tests/test_core_inbound_beacon_skip.py +81 -0
  65. skcomms-0.2.2/tests/test_core_send_signed.py +251 -0
  66. skcomms-0.2.2/tests/test_core_stop.py +47 -0
  67. skcomms-0.2.2/tests/test_deadletter_ops.py +481 -0
  68. skcomms-0.2.2/tests/test_delivery_semantics.py +268 -0
  69. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_home_scaffold.py +62 -0
  70. skcomms-0.2.2/tests/test_housekeeping.py +718 -0
  71. skcomms-0.2.2/tests/test_http_s2s_star_recipient.py +39 -0
  72. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_http_s2s_transport.py +149 -9
  73. skcomms-0.2.2/tests/test_identity_trust_backup.py +639 -0
  74. skcomms-0.2.2/tests/test_key_exchange_transport_scoping.py +132 -0
  75. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_mailbox.py +24 -17
  76. skcomms-0.2.2/tests/test_mailbox_encrypt_at_rest.py +516 -0
  77. skcomms-0.2.2/tests/test_mailbox_recipient_key.py +40 -0
  78. skcomms-0.2.2/tests/test_no_subapp_dependency.py +114 -0
  79. skcomms-0.2.2/tests/test_nonce_durable.py +340 -0
  80. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_nostr_discovery.py +188 -0
  81. skcomms-0.2.2/tests/test_observability.py +492 -0
  82. skcomms-0.2.2/tests/test_operator_cli.py +193 -0
  83. skcomms-0.2.2/tests/test_outbox_home_alignment.py +104 -0
  84. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_outbox_migrate.py +126 -11
  85. skcomms-0.2.2/tests/test_pairing_mirror.py +285 -0
  86. skcomms-0.2.2/tests/test_paths_scoping.py +574 -0
  87. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqroute_padding.py +2 -3
  88. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqroute_transport.py +2 -3
  89. skcomms-0.2.2/tests/test_prune_inbox.py +96 -0
  90. skcomms-0.2.2/tests/test_public_pairing.py +245 -0
  91. skcomms-0.2.2/tests/test_queue_bounds.py +831 -0
  92. skcomms-0.2.2/tests/test_receiver_wire_ttl_expiry.py +50 -0
  93. skcomms-0.2.2/tests/test_router_failure_log_dedup.py +174 -0
  94. skcomms-0.2.2/tests/test_router_perm_backoff.py +131 -0
  95. skcomms-0.2.2/tests/test_router_perm_backoff_fixes.py +156 -0
  96. skcomms-0.2.2/tests/test_router_quarantine.py +122 -0
  97. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_router_rail_ordering.py +28 -23
  98. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_router_route_anon.py +2 -1
  99. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_router_route_signed.py +3 -2
  100. skcomms-0.2.2/tests/test_router_star_broadcast_routing.py +139 -0
  101. skcomms-0.2.2/tests/test_router_unsigned_excludes_https_s2s.py +155 -0
  102. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_ssrf.py +20 -9
  103. skcomms-0.2.2/tests/test_skworld_manifest.py +114 -0
  104. skcomms-0.2.2/tests/test_ssrf_guard.py +431 -0
  105. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_store_forward.py +92 -0
  106. skcomms-0.2.2/tests/test_syncthing_rail.py +415 -0
  107. skcomms-0.2.2/tests/test_webrtc_bridge_lifecycle.py +404 -0
  108. skcomms-0.2.2/tests/test_webrtc_signaling_log_dedup.py +54 -0
  109. skcomms-0.2.0/src/skcomms/cluster.py +0 -67
  110. skcomms-0.2.0/src/skcomms/config.py +0 -230
  111. skcomms-0.2.0/src/skcomms/core.py +0 -947
  112. skcomms-0.2.0/src/skcomms/cot.py +0 -299
  113. skcomms-0.2.0/src/skcomms/cot_agent.py +0 -173
  114. skcomms-0.2.0/src/skcomms/cot_client.py +0 -142
  115. skcomms-0.2.0/src/skcomms/cot_pki.py +0 -622
  116. skcomms-0.2.0/src/skcomms/cot_server.py +0 -603
  117. skcomms-0.2.0/src/skcomms/cot_service.py +0 -191
  118. skcomms-0.2.0/src/skcomms/geo.py +0 -477
  119. skcomms-0.2.0/src/skcomms/home.py +0 -152
  120. skcomms-0.2.0/src/skcomms/outbox.py +0 -663
  121. skcomms-0.2.0/tests/test_ack_bind.py +0 -89
  122. skcomms-0.2.0/tests/test_broker_server.py +0 -36
  123. skcomms-0.2.0/tests/test_capauth_failclosed.py +0 -98
  124. skcomms-0.2.0/tests/test_cot_agent_nameagnostic.py +0 -18
  125. skcomms-0.2.0/tests/test_cot_beacon_outbox.py +0 -204
  126. skcomms-0.2.0/tests/test_cot_codec.py +0 -146
  127. skcomms-0.2.0/tests/test_cot_pki.py +0 -150
  128. skcomms-0.2.0/tests/test_cot_server.py +0 -216
  129. skcomms-0.2.0/tests/test_geo.py +0 -247
  130. skcomms-0.2.0/tests/test_mailbox_encrypt_at_rest.py +0 -173
  131. {skcomms-0.2.0 → skcomms-0.2.2}/LICENSE +0 -0
  132. {skcomms-0.2.0 → skcomms-0.2.2}/README.md +0 -0
  133. {skcomms-0.2.0 → skcomms-0.2.2}/setup.cfg +0 -0
  134. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/__init__.py +0 -0
  135. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/__init__.py +0 -0
  136. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/audit.py +0 -0
  137. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/config.py +0 -0
  138. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/exec.py +0 -0
  139. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/files.py +0 -0
  140. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/grants.py +0 -0
  141. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/knowledge.py +0 -0
  142. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/registry.py +0 -0
  143. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/routing.py +0 -0
  144. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/wiring.py +0 -0
  145. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/__init__.py +0 -0
  146. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/base.py +0 -0
  147. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/discord.py +0 -0
  148. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/factory.py +0 -0
  149. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/fake.py +0 -0
  150. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/matrix.py +0 -0
  151. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/models.py +0 -0
  152. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/registry.py +0 -0
  153. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/slack.py +0 -0
  154. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/telegram.py +0 -0
  155. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/anon_queue.py +0 -0
  156. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/anon_transport.py +0 -0
  157. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/capabilities.py +0 -0
  158. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/compression.py +0 -0
  159. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent.py +0 -0
  160. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_banfeeds.py +0 -0
  161. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_captcha.py +0 -0
  162. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_greylist.py +0 -0
  163. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_groups.py +0 -0
  164. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_moderation.py +0 -0
  165. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_policy.py +0 -0
  166. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_requests.py +0 -0
  167. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_runtime.py +0 -0
  168. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_tiering.py +0 -0
  169. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_tokens.py +0 -0
  170. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/crypto_suites.py +0 -0
  171. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/did_router.py +0 -0
  172. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/envelope.py +0 -0
  173. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/__init__.py +0 -0
  174. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/codebook.py +0 -0
  175. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/codec.py +0 -0
  176. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/emergent.py +0 -0
  177. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/gloss.py +0 -0
  178. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/handshake.py +0 -0
  179. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/macros.py +0 -0
  180. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/message.py +0 -0
  181. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/session.py +0 -0
  182. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/grants.py +0 -0
  183. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/heartbeat.py +0 -0
  184. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/household_router.py +0 -0
  185. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/identity.py +0 -0
  186. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/marketplace.py +0 -0
  187. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/mcp_server.py +0 -0
  188. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/metrics.py +0 -0
  189. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/models.py +0 -0
  190. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/node_registry.py +0 -0
  191. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/padding.py +0 -0
  192. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pq_provider.py +0 -0
  193. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqdm.py +0 -0
  194. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqkem.py +0 -0
  195. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqkem_backend.py +0 -0
  196. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqroute.py +0 -0
  197. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqroute_transport.py +0 -0
  198. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqsig.py +0 -0
  199. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/profile_router.py +0 -0
  200. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pubsub.py +0 -0
  201. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pubsub_transport.py +0 -0
  202. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/ratelimit.py +0 -0
  203. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/realm.py +0 -0
  204. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/skfed_announce.py +0 -0
  205. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/skfed_directory.py +0 -0
  206. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/skfed_readdr.py +0 -0
  207. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/souls_router.py +0 -0
  208. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/__init__.py +0 -0
  209. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/audio_track.py +0 -0
  210. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/__init__.py +0 -0
  211. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/gatt.py +0 -0
  212. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/identity.py +0 -0
  213. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/node.py +0 -0
  214. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/noise.py +0 -0
  215. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/protocol.py +0 -0
  216. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/radio.py +0 -0
  217. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/relay.py +0 -0
  218. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/__init__.py +0 -0
  219. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/addressing.py +0 -0
  220. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/framing.py +0 -0
  221. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/interface.py +0 -0
  222. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/meshtastic_iface.py +0 -0
  223. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/store.py +0 -0
  224. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/transport.py +0 -0
  225. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/nostr.py +0 -0
  226. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/p2p_connector.py +0 -0
  227. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/p2p_manager.py +0 -0
  228. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/p2p_session.py +0 -0
  229. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/signaling_base.py +0 -0
  230. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/signaling_broker.py +0 -0
  231. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/signaling_mailbox.py +0 -0
  232. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/tailscale.py +0 -0
  233. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/video_track.py +0 -0
  234. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/webrtc_media.py +0 -0
  235. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/websocket.py +0 -0
  236. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/dependency_links.txt +0 -0
  237. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/entry_points.txt +0 -0
  238. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/requires.txt +0 -0
  239. {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/top_level.txt +0 -0
  240. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_exec.py +0 -0
  241. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_files.py +0 -0
  242. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_rbac.py +0 -0
  243. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_routing.py +0 -0
  244. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_server.py +0 -0
  245. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_wiring.py +0 -0
  246. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_adapter_factory.py +0 -0
  247. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_adapter_fake.py +0 -0
  248. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_anon_queue.py +0 -0
  249. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_anon_transport.py +0 -0
  250. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_access_token.py +0 -0
  251. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_consent.py +0 -0
  252. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_consent_token_gate.py +0 -0
  253. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_skfed_directory.py +0 -0
  254. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_backfill_file_locations.py +0 -0
  255. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_fragment.py +0 -0
  256. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_gatt.py +0 -0
  257. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_identity.py +0 -0
  258. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_mesh.py +0 -0
  259. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_noise.py +0 -0
  260. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_pairing_bundle.py +0 -0
  261. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_protocol.py +0 -0
  262. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_radio.py +0 -0
  263. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_relay.py +0 -0
  264. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_capabilities.py +0 -0
  265. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_capauth_key_reconcile.py +0 -0
  266. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_channel_adapter.py +0 -0
  267. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_config.py +0 -0
  268. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent.py +0 -0
  269. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_banfeeds.py +0 -0
  270. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_captcha.py +0 -0
  271. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_cli.py +0 -0
  272. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_cli_mgmt.py +0 -0
  273. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_greylist.py +0 -0
  274. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_groups.py +0 -0
  275. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_moderation.py +0 -0
  276. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_pipeline.py +0 -0
  277. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_policy.py +0 -0
  278. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_requests.py +0 -0
  279. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_runtime.py +0 -0
  280. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_tiering.py +0 -0
  281. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_tokens.py +0 -0
  282. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_core_send_federated.py +0 -0
  283. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_crypto_failclosed.py +0 -0
  284. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_crypto_suites_q0.py +0 -0
  285. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_daemon_adapter_lifecycle.py +0 -0
  286. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_discord_adapter.py +0 -0
  287. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_encrypt_fail_closed.py +0 -0
  288. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_envelope_consent_token.py +0 -0
  289. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_envelope_v1.py +0 -0
  290. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_federation.py +0 -0
  291. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_federation_integration.py +0 -0
  292. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_codebook.py +0 -0
  293. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_codec_l0.py +0 -0
  294. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_codec_l1.py +0 -0
  295. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_codec_l2.py +0 -0
  296. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_emergent.py +0 -0
  297. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_emergent_negotiator.py +0 -0
  298. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_emergent_protocol.py +0 -0
  299. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_gloss.py +0 -0
  300. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_handshake.py +0 -0
  301. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_handshake_lexicon.py +0 -0
  302. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_macros.py +0 -0
  303. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_macros_render.py +0 -0
  304. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_message.py +0 -0
  305. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_session.py +0 -0
  306. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_session_macros.py +0 -0
  307. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_to_human.py +0 -0
  308. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_grants.py +0 -0
  309. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_hybrid_envelope_sig.py +0 -0
  310. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_identity_env_override.py +0 -0
  311. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_identity_key_binding.py +0 -0
  312. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_identity_realm.py +0 -0
  313. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_integration.py +0 -0
  314. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_knowledge.py +0 -0
  315. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_addressing.py +0 -0
  316. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_framing.py +0 -0
  317. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_interface.py +0 -0
  318. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_meshtastic_iface.py +0 -0
  319. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_store.py +0 -0
  320. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_transport.py +0 -0
  321. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_matrix_adapter.py +0 -0
  322. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_node_registry.py +0 -0
  323. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_p2p_connector.py +0 -0
  324. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_p2p_manager.py +0 -0
  325. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_p2p_media.py +0 -0
  326. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_p2p_session.py +0 -0
  327. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_padding.py +0 -0
  328. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pairing.py +0 -0
  329. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pairing_cli.py +0 -0
  330. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_peer_inbox_traversal.py +0 -0
  331. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_peers.py +0 -0
  332. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqc_cutover.py +0 -0
  333. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqc_shim.py +0 -0
  334. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqdm.py +0 -0
  335. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqkem.py +0 -0
  336. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqroute.py +0 -0
  337. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqsig.py +0 -0
  338. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_registry.py +0 -0
  339. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_registry_cli.py +0 -0
  340. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_sign_fail_closed.py +0 -0
  341. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_signaling_broker.py +0 -0
  342. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_signaling_mailbox.py +0 -0
  343. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_signaling_select.py +0 -0
  344. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_announce.py +0 -0
  345. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_cli.py +0 -0
  346. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_directory.py +0 -0
  347. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_operator_pin.py +0 -0
  348. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_readdr.py +0 -0
  349. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_resolve.py +0 -0
  350. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_slack_adapter.py +0 -0
  351. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_smoke.py +0 -0
  352. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_syncthing_recipient_guard.py +0 -0
  353. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_telegram_adapter.py +0 -0
  354. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_tofu.py +0 -0
  355. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_typed_message_content.py +0 -0
  356. {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_webrtc_ice_logging.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skcomms
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Sovereign communications for AI agents — multi-channel transport + FQID realm addressing
5
5
  Author-email: smilinTux <chefboyrdave2.1@gmail.com>
6
6
  License: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skcomms"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Sovereign communications for AI agents — multi-channel transport + FQID realm addressing"
9
9
  readme = "README.md"
10
10
  license = {text = "GPL-3.0-or-later"}
@@ -27,9 +27,11 @@ from __future__ import annotations
27
27
 
28
28
  import json
29
29
  import logging
30
+ import os
30
31
  import socket
31
32
  from dataclasses import dataclass
32
- from typing import Any, Optional
33
+ from pathlib import Path
34
+ from typing import Any, Optional, Union
33
35
 
34
36
  from .. import federation as fed
35
37
  from ..discovery import PeerStore
@@ -42,6 +44,49 @@ from .registry import AccessRegistry, DEFAULT_REGISTRY, RegisteredTool, Scope
42
44
  logger = logging.getLogger("skcomms.access.server")
43
45
 
44
46
 
47
+ def _access_nonce_db_path() -> Path:
48
+ """Resolve the sk-access durable replay-cache path (NODE-LOCAL, never synced).
49
+
50
+ Precedence: ``SKCOMMS_ACCESS_NONCE_DB`` (explicit file path) wins, then
51
+ the ``SKCOMMS_NONCE_CACHE_DIR`` directory override, then the XDG state
52
+ default (``$XDG_STATE_HOME/skcomms/`` or ``~/.local/state/skcomms/``):
53
+ see :func:`skcomms.home.resolve_nonce_db`. It is a SEPARATE file from
54
+ the federation inbox cache (``nonce_cache.db``) so the two surfaces keep
55
+ independent replay history.
56
+
57
+ The store used to live at ``skcomms_home()/state/access_nonce_cache.db``,
58
+ inside the Syncthing-shared tree; a replay cache must never sync between
59
+ nodes, so it now defaults outside any synced dir. A healthy legacy DB is
60
+ migrated once; otherwise a fresh cache starts (safe: replay exposure is
61
+ bounded by the envelope freshness window).
62
+ """
63
+ override = (os.environ.get("SKCOMMS_ACCESS_NONCE_DB") or "").strip()
64
+ if override:
65
+ return Path(override).expanduser()
66
+ from ..home import resolve_nonce_db
67
+
68
+ return resolve_nonce_db("access_nonce_cache.db")
69
+
70
+
71
+ def _default_nonce_cache() -> Union["fed.NonceCache", "fed.DurableNonceCache"]:
72
+ """Build the sk-access replay guard: durable by default (coord f465b407).
73
+
74
+ Mirrors :func:`skcomms.api._get_nonce_cache` semantics for the sk-access
75
+ surface: a SQLite-backed :class:`~skcomms.federation.DurableNonceCache`
76
+ so a daemon restart does not reopen a replay window on signed tool calls.
77
+ ``SKCOMMS_NONCE_CACHE=memory`` opts back into the legacy in-memory cache
78
+ (explicit, single-process/ephemeral use only).
79
+
80
+ Fail-closed: if the durable store cannot be opened this raises, so the
81
+ server refuses to start rather than silently accepting replays with no
82
+ persistence (systemd Restart=always keeps retrying).
83
+ """
84
+ mode = (os.environ.get("SKCOMMS_NONCE_CACHE") or "").strip().lower()
85
+ if mode == "memory":
86
+ return fed.NonceCache()
87
+ return fed.DurableNonceCache(_access_nonce_db_path())
88
+
89
+
45
90
  # ---------------------------------------------------------------------------
46
91
  # Errors
47
92
  # ---------------------------------------------------------------------------
@@ -104,6 +149,9 @@ class AccessServer:
104
149
  verifier: An :class:`~skcomms.signing.EnvelopeVerifier`. If omitted, one
105
150
  is created and seeded TOFU-style from the :class:`PeerStore`.
106
151
  peer_store: Peer store for TOFU key resolution (default: a fresh one).
152
+ nonce_cache: Replay guard override (tests / embedding). Default is the
153
+ durable per-node store from :func:`_default_nonce_cache`, so a
154
+ daemon restart does not reopen a replay window (coord f465b407).
107
155
  """
108
156
 
109
157
  def __init__(
@@ -113,12 +161,18 @@ class AccessServer:
113
161
  verifier: Optional[EnvelopeVerifier] = None,
114
162
  peer_store: Optional[PeerStore] = None,
115
163
  audit: Optional[AccessAuditLog] = None,
164
+ nonce_cache: Optional[
165
+ Union[fed.NonceCache, fed.DurableNonceCache]
166
+ ] = None,
116
167
  ) -> None:
117
168
  self.config = config or AccessConfig.load()
118
169
  self.registry = registry or DEFAULT_REGISTRY
119
170
  self._peer_store = peer_store if peer_store is not None else PeerStore()
120
171
  self.verifier = verifier or self._build_verifier()
121
- self.nonce_cache = fed.NonceCache()
172
+ # Durable by default; fail-closed if the store cannot be opened.
173
+ self.nonce_cache = (
174
+ nonce_cache if nonce_cache is not None else _default_nonce_cache()
175
+ )
122
176
  self.audit = audit or AccessAuditLog(node=self.config.node_name)
123
177
  self._register_builtins()
124
178
 
@@ -20,7 +20,7 @@ import logging
20
20
  from datetime import datetime, timezone
21
21
  from enum import Enum
22
22
  from pathlib import Path
23
- from typing import Optional
23
+ from typing import Callable, Optional
24
24
 
25
25
  from pydantic import BaseModel, Field
26
26
 
@@ -81,16 +81,27 @@ class AckTracker:
81
81
  Args:
82
82
  acks_dir: Directory for ACK tracking files.
83
83
  default_timeout: Default seconds to wait for ACK.
84
+ sender_verifier: Optional callable authenticating an ACK envelope
85
+ against its claimed sender (CapAuth/PGP signature). Fails closed:
86
+ a verifier that returns falsy or raises rejects the ACK.
84
87
  """
85
88
 
86
89
  def __init__(
87
90
  self,
88
91
  acks_dir: Optional[Path] = None,
89
92
  default_timeout: int = DEFAULT_ACK_TIMEOUT,
93
+ sender_verifier: Optional[Callable[[MessageEnvelope], bool]] = None,
90
94
  ):
91
95
  self._dir = acks_dir or Path(SKCOMMS_HOME).expanduser() / ACKS_DIR_NAME
92
96
  self._dir.mkdir(parents=True, exist_ok=True)
93
97
  self._default_timeout = default_timeout
98
+ # Optional cryptographic authenticator for inbound ACKs. Given the ACK
99
+ # envelope, returns True iff the ACK is authenticated as coming from its
100
+ # claimed sender (e.g. its CapAuth/PGP payload signature verifies). When
101
+ # wired in, string identity alone is not trusted. Reuses the existing
102
+ # signing stack (skcomms.crypto.EnvelopeCrypto.verify_signature), no new
103
+ # crypto scheme.
104
+ self._sender_verifier = sender_verifier
94
105
 
95
106
  @property
96
107
  def acks_dir(self) -> Path:
@@ -124,16 +135,51 @@ class AckTracker:
124
135
  logger.debug("Tracking ACK for %s -> %s", envelope.envelope_id[:8], envelope.recipient)
125
136
  return pending
126
137
 
138
+ @staticmethod
139
+ def _identity_matches(a: str, b: str) -> bool:
140
+ """Compare two identities (agent name, FQID, or PGP fingerprint).
141
+
142
+ Whitespace- and case-insensitive: a PGP fingerprint may be stored
143
+ spaced or unspaced as it crosses different transports. Empty values
144
+ never match (fail closed).
145
+
146
+ Args:
147
+ a: First identity string.
148
+ b: Second identity string.
149
+
150
+ Returns:
151
+ True if both are non-empty and equal after normalization.
152
+ """
153
+ if not a or not b:
154
+ return False
155
+ return a.replace(" ", "").casefold() == b.replace(" ", "").casefold()
156
+
127
157
  def process_ack(self, ack_envelope: MessageEnvelope) -> Optional[PendingAck]:
128
158
  """Process a received ACK envelope and resolve the pending entry.
129
159
 
130
- The ACK's content holds the original envelope_id.
160
+ The ACK's content holds the original envelope_id. Before an ACK is
161
+ allowed to confirm delivery it is bound to the sender identity and
162
+ checked for replay:
163
+
164
+ * Sender binding (anti-forgery): only the party the original message
165
+ was delivered to (``pending.recipient``) may acknowledge it. An ACK
166
+ whose ``sender`` is not that recipient is rejected, so a third party
167
+ cannot forge a delivery confirmation just by learning the
168
+ ``envelope_id``.
169
+ * Cryptographic authentication (optional): when a ``sender_verifier``
170
+ is wired in, the ACK's CapAuth/PGP signature must authenticate its
171
+ claimed sender; string identity alone is not trusted. Fails closed.
172
+ * Replay / stale rejection: an ACK may confirm a pending entry exactly
173
+ once. A duplicate ACK for an already-confirmed message, or a stale
174
+ ACK replayed after the entry timed out, does not mutate state (no
175
+ re-confirmation, no resurrection).
131
176
 
132
177
  Args:
133
178
  ack_envelope: A received ACK-type envelope.
134
179
 
135
180
  Returns:
136
- The resolved PendingAck, or None if no matching pending found.
181
+ The resolved PendingAck, or None if the ACK is unknown, forged,
182
+ unauthenticated, or a replay.
137
183
  """
138
184
  if not ack_envelope.is_ack:
139
185
  return None
@@ -142,7 +188,9 @@ class AckTracker:
142
188
  path = self._dir / f"{original_id}{ACK_SUFFIX}"
143
189
 
144
190
  if not path.exists():
145
- logger.debug("ACK for unknown envelope %s — ignoring", original_id[:8])
191
+ # No pending entry: a forged ACK for a message we never sent, or a
192
+ # replay after the entry was purged. Either way, ignore.
193
+ logger.debug("ACK for unknown envelope %s: ignoring", original_id[:8])
146
194
  return None
147
195
 
148
196
  try:
@@ -151,27 +199,46 @@ class AckTracker:
151
199
  logger.warning("Failed to read pending ACK %s: %s", original_id[:8], exc)
152
200
  return None
153
201
 
154
- # Replay/dedupe protection: only a still-pending entry may be confirmed.
155
- # A replayed ACK (or an ACK for an already-resolved envelope) is rejected
156
- # so it cannot re-confirm or mutate the entry.
157
- if pending.status != AckStatus.PENDING:
202
+ # Sender-identity binding (anti-forgery): the ACK must come from the
203
+ # identity the original message was delivered to. Reject any ACK whose
204
+ # sender is not the intended recipient (forgery by a third party).
205
+ # Comparison is fingerprint-normalization aware and fails closed on
206
+ # empty identities.
207
+ if not self._identity_matches(ack_envelope.sender, pending.recipient):
158
208
  logger.warning(
159
- "Duplicate/replayed ACK for %s (status=%s) — ignoring",
209
+ "Rejecting forged ACK for %s: sender %r is not tracked recipient %r",
160
210
  original_id[:8],
161
- pending.status.value,
211
+ ack_envelope.sender,
212
+ pending.recipient,
162
213
  )
163
214
  return None
164
215
 
165
- # Binding: the ACK must come from the identity the original message was
166
- # delivered to. The transport already verified the envelope signature, so
167
- # ack_envelope.sender is an authenticated identity — reject any ACK whose
168
- # sender is not the intended recipient (forgery by a third party).
169
- if ack_envelope.sender != pending.recipient:
216
+ # Optional cryptographic authentication of the ACK sender. Fail closed:
217
+ # a verifier error or a falsy result rejects the ACK.
218
+ if self._sender_verifier is not None:
219
+ try:
220
+ authenticated = bool(self._sender_verifier(ack_envelope))
221
+ except Exception as exc:
222
+ logger.warning(
223
+ "ACK sender verification errored for %s: %s", original_id[:8], exc
224
+ )
225
+ authenticated = False
226
+ if not authenticated:
227
+ logger.warning(
228
+ "Rejecting unauthenticated ACK for %s from %s (signature check failed)",
229
+ original_id[:8],
230
+ ack_envelope.sender,
231
+ )
232
+ return None
233
+
234
+ # Replay/dedupe protection: only a still-pending entry may be confirmed.
235
+ # A replayed ACK (or an ACK for an already-resolved or timed-out
236
+ # envelope) is rejected so it cannot re-confirm or mutate the entry.
237
+ if pending.status != AckStatus.PENDING:
170
238
  logger.warning(
171
- "ACK for %s from unauthorized sender %r (expected recipient %r) — rejecting",
239
+ "Duplicate/replayed ACK for %s (status=%s): ignoring",
172
240
  original_id[:8],
173
- ack_envelope.sender,
174
- pending.recipient,
241
+ pending.status.value,
175
242
  )
176
243
  return None
177
244