skcomms 0.2.1__tar.gz → 0.2.3__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 (343) hide show
  1. {skcomms-0.2.1 → skcomms-0.2.3}/PKG-INFO +1 -1
  2. {skcomms-0.2.1 → skcomms-0.2.3}/pyproject.toml +1 -1
  3. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/__init__.py +1 -1
  4. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/server.py +56 -5
  5. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/api.py +219 -37
  6. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/capabilities.py +1 -0
  7. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/capauth_validator.py +107 -15
  8. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/cli.py +299 -25
  9. skcomms-0.2.3/src/skcomms/cluster.py +219 -0
  10. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/core.py +56 -13
  11. skcomms-0.2.3/src/skcomms/geo_seed.py +157 -0
  12. skcomms-0.2.3/src/skcomms/geo_store.py +231 -0
  13. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/home.py +100 -0
  14. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/integration.py +41 -12
  15. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/key_exchange.py +19 -10
  16. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/mailbox.py +12 -1
  17. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/nostr_discovery.py +165 -5
  18. skcomms-0.2.3/src/skcomms/operator_probe.py +267 -0
  19. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/outbox.py +21 -4
  20. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pairing.py +4 -0
  21. skcomms-0.2.3/src/skcomms/pairing_mirror.py +103 -0
  22. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/paths.py +31 -0
  23. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/peers.py +35 -0
  24. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqdm.py +229 -0
  25. skcomms-0.2.3/src/skcomms/public_pairing.py +286 -0
  26. skcomms-0.2.3/src/skcomms/skworld_manifest.py +82 -0
  27. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/store_forward.py +3 -2
  28. skcomms-0.2.3/src/skcomms/syncthing_rail.py +920 -0
  29. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/file.py +23 -0
  30. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/webrtc.py +17 -1
  31. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/trustbackup.py +5 -3
  32. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/PKG-INFO +1 -1
  33. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/SOURCES.txt +28 -0
  34. skcomms-0.2.3/tests/test_access_nonce_durable.py +216 -0
  35. skcomms-0.2.3/tests/test_api_geo_seed.py +119 -0
  36. skcomms-0.2.3/tests/test_api_geo_skcot_bridge.py +223 -0
  37. skcomms-0.2.3/tests/test_api_geo_units.py +139 -0
  38. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_capabilities.py +4 -0
  39. skcomms-0.2.3/tests/test_capauth_resolution.py +58 -0
  40. skcomms-0.2.3/tests/test_capauth_revoked_expired.py +180 -0
  41. skcomms-0.2.3/tests/test_cluster_schema.py +165 -0
  42. skcomms-0.2.3/tests/test_core_stop.py +47 -0
  43. skcomms-0.2.3/tests/test_geo_roundtrip_skcot.py +141 -0
  44. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_housekeeping.py +166 -0
  45. skcomms-0.2.3/tests/test_key_exchange_transport_scoping.py +132 -0
  46. skcomms-0.2.3/tests/test_mailbox_recipient_key.py +40 -0
  47. skcomms-0.2.3/tests/test_no_subapp_dependency.py +114 -0
  48. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_nonce_durable.py +104 -22
  49. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_nostr_discovery.py +188 -0
  50. skcomms-0.2.3/tests/test_operator_cli.py +213 -0
  51. skcomms-0.2.3/tests/test_outbox_home_alignment.py +104 -0
  52. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_p2p_media.py +10 -3
  53. skcomms-0.2.3/tests/test_pairing_mirror.py +285 -0
  54. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_paths_scoping.py +38 -0
  55. skcomms-0.2.3/tests/test_pqdm2.py +108 -0
  56. skcomms-0.2.3/tests/test_pqdm2_interop.py +80 -0
  57. skcomms-0.2.3/tests/test_public_pairing.py +245 -0
  58. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_queue_bounds.py +41 -0
  59. skcomms-0.2.3/tests/test_skworld_manifest.py +114 -0
  60. skcomms-0.2.3/tests/test_syncthing_rail.py +415 -0
  61. skcomms-0.2.3/tests/test_webrtc_bridge_lifecycle.py +404 -0
  62. skcomms-0.2.1/src/skcomms/cluster.py +0 -67
  63. {skcomms-0.2.1 → skcomms-0.2.3}/LICENSE +0 -0
  64. {skcomms-0.2.1 → skcomms-0.2.3}/README.md +0 -0
  65. {skcomms-0.2.1 → skcomms-0.2.3}/setup.cfg +0 -0
  66. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/__init__.py +0 -0
  67. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/audit.py +0 -0
  68. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/config.py +0 -0
  69. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/exec.py +0 -0
  70. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/files.py +0 -0
  71. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/grants.py +0 -0
  72. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/knowledge.py +0 -0
  73. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/registry.py +0 -0
  74. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/routing.py +0 -0
  75. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/wiring.py +0 -0
  76. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/ack.py +0 -0
  77. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/__init__.py +0 -0
  78. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/base.py +0 -0
  79. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/discord.py +0 -0
  80. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/factory.py +0 -0
  81. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/fake.py +0 -0
  82. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/matrix.py +0 -0
  83. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/models.py +0 -0
  84. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/registry.py +0 -0
  85. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/slack.py +0 -0
  86. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/telegram.py +0 -0
  87. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/anon_queue.py +0 -0
  88. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/anon_transport.py +0 -0
  89. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/compression.py +0 -0
  90. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/config.py +0 -0
  91. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent.py +0 -0
  92. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_banfeeds.py +0 -0
  93. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_captcha.py +0 -0
  94. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_greylist.py +0 -0
  95. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_group_gate.py +0 -0
  96. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_groups.py +0 -0
  97. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_moderation.py +0 -0
  98. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_pipeline.py +0 -0
  99. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_policy.py +0 -0
  100. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_requests.py +0 -0
  101. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_runtime.py +0 -0
  102. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_tiering.py +0 -0
  103. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_tokens.py +0 -0
  104. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/crypto.py +0 -0
  105. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/crypto_suites.py +0 -0
  106. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/did_router.py +0 -0
  107. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/discovery.py +0 -0
  108. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/envelope.py +0 -0
  109. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/federation.py +0 -0
  110. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/__init__.py +0 -0
  111. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/codebook.py +0 -0
  112. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/codec.py +0 -0
  113. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/emergent.py +0 -0
  114. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/gloss.py +0 -0
  115. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/handshake.py +0 -0
  116. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/macros.py +0 -0
  117. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/message.py +0 -0
  118. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/session.py +0 -0
  119. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/grants.py +0 -0
  120. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/heartbeat.py +0 -0
  121. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/household_router.py +0 -0
  122. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/housekeeping.py +0 -0
  123. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/identity.py +0 -0
  124. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/marketplace.py +0 -0
  125. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/mcp_server.py +0 -0
  126. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/metrics.py +0 -0
  127. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/models.py +0 -0
  128. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/node_registry.py +0 -0
  129. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/observability.py +0 -0
  130. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/outbox_migrate.py +0 -0
  131. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/padding.py +0 -0
  132. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pq_provider.py +0 -0
  133. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqkem.py +0 -0
  134. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqkem_backend.py +0 -0
  135. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqroute.py +0 -0
  136. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqroute_transport.py +0 -0
  137. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqsig.py +0 -0
  138. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/profile_router.py +0 -0
  139. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pubsub.py +0 -0
  140. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pubsub_transport.py +0 -0
  141. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/queue.py +0 -0
  142. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/ratelimit.py +0 -0
  143. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/realm.py +0 -0
  144. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/registry.py +0 -0
  145. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/router.py +0 -0
  146. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/signaling.py +0 -0
  147. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/signing.py +0 -0
  148. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/skfed_announce.py +0 -0
  149. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/skfed_directory.py +0 -0
  150. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/skfed_readdr.py +0 -0
  151. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/skfed_resolve.py +0 -0
  152. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/souls_router.py +0 -0
  153. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/ssrf.py +0 -0
  154. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/tofu.py +0 -0
  155. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transport.py +0 -0
  156. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/__init__.py +0 -0
  157. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/audio_track.py +0 -0
  158. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/__init__.py +0 -0
  159. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/gatt.py +0 -0
  160. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/identity.py +0 -0
  161. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/node.py +0 -0
  162. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/noise.py +0 -0
  163. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/protocol.py +0 -0
  164. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/radio.py +0 -0
  165. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/relay.py +0 -0
  166. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/broker_server.py +0 -0
  167. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/http_s2s.py +0 -0
  168. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/__init__.py +0 -0
  169. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/addressing.py +0 -0
  170. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/framing.py +0 -0
  171. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/interface.py +0 -0
  172. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/meshtastic_iface.py +0 -0
  173. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/store.py +0 -0
  174. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/transport.py +0 -0
  175. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/nostr.py +0 -0
  176. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/p2p_connector.py +0 -0
  177. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/p2p_manager.py +0 -0
  178. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/p2p_session.py +0 -0
  179. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/signaling_base.py +0 -0
  180. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/signaling_broker.py +0 -0
  181. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/signaling_mailbox.py +0 -0
  182. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/syncthing.py +0 -0
  183. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/tailscale.py +0 -0
  184. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/video_track.py +0 -0
  185. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/webrtc_media.py +0 -0
  186. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/websocket.py +0 -0
  187. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/dependency_links.txt +0 -0
  188. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/entry_points.txt +0 -0
  189. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/requires.txt +0 -0
  190. {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/top_level.txt +0 -0
  191. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_exec.py +0 -0
  192. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_files.py +0 -0
  193. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_rbac.py +0 -0
  194. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_routing.py +0 -0
  195. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_server.py +0 -0
  196. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_wiring.py +0 -0
  197. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ack_bind.py +0 -0
  198. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_adapter_factory.py +0 -0
  199. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_adapter_fake.py +0 -0
  200. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_anon_queue.py +0 -0
  201. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_anon_transport.py +0 -0
  202. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_access_token.py +0 -0
  203. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_consent.py +0 -0
  204. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_consent_token_gate.py +0 -0
  205. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_cors.py +0 -0
  206. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_federation_inbox.py +0 -0
  207. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_health.py +0 -0
  208. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_send_signed_wire.py +0 -0
  209. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_skfed_directory.py +0 -0
  210. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_backfill_file_locations.py +0 -0
  211. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_fragment.py +0 -0
  212. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_gatt.py +0 -0
  213. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_identity.py +0 -0
  214. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_mesh.py +0 -0
  215. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_noise.py +0 -0
  216. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_pairing_bundle.py +0 -0
  217. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_protocol.py +0 -0
  218. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_radio.py +0 -0
  219. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_relay.py +0 -0
  220. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_broker_server.py +0 -0
  221. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_capauth_failclosed.py +0 -0
  222. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_capauth_key_reconcile.py +0 -0
  223. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_channel_adapter.py +0 -0
  224. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_config.py +0 -0
  225. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent.py +0 -0
  226. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_ban_gate_fail_closed.py +0 -0
  227. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_banfeeds.py +0 -0
  228. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_captcha.py +0 -0
  229. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_cli.py +0 -0
  230. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_cli_mgmt.py +0 -0
  231. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_greylist.py +0 -0
  232. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_group_gate.py +0 -0
  233. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_groups.py +0 -0
  234. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_moderation.py +0 -0
  235. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_pipeline.py +0 -0
  236. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_policy.py +0 -0
  237. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_requests.py +0 -0
  238. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_runtime.py +0 -0
  239. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_tiering.py +0 -0
  240. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_tokens.py +0 -0
  241. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_core_inbound_beacon_skip.py +0 -0
  242. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_core_send_federated.py +0 -0
  243. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_core_send_signed.py +0 -0
  244. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_crypto_failclosed.py +0 -0
  245. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_crypto_suites_q0.py +0 -0
  246. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_daemon_adapter_lifecycle.py +0 -0
  247. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_deadletter_ops.py +0 -0
  248. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_delivery_semantics.py +0 -0
  249. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_discord_adapter.py +0 -0
  250. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_encrypt_fail_closed.py +0 -0
  251. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_envelope_consent_token.py +0 -0
  252. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_envelope_v1.py +0 -0
  253. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_federation.py +0 -0
  254. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_federation_integration.py +0 -0
  255. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_codebook.py +0 -0
  256. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_codec_l0.py +0 -0
  257. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_codec_l1.py +0 -0
  258. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_codec_l2.py +0 -0
  259. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_emergent.py +0 -0
  260. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_emergent_negotiator.py +0 -0
  261. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_emergent_protocol.py +0 -0
  262. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_gloss.py +0 -0
  263. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_handshake.py +0 -0
  264. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_handshake_lexicon.py +0 -0
  265. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_macros.py +0 -0
  266. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_macros_render.py +0 -0
  267. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_message.py +0 -0
  268. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_session.py +0 -0
  269. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_session_macros.py +0 -0
  270. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_to_human.py +0 -0
  271. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_grants.py +0 -0
  272. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_home_scaffold.py +0 -0
  273. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_http_s2s_star_recipient.py +0 -0
  274. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_http_s2s_transport.py +0 -0
  275. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_hybrid_envelope_sig.py +0 -0
  276. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_identity_env_override.py +0 -0
  277. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_identity_key_binding.py +0 -0
  278. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_identity_realm.py +0 -0
  279. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_identity_trust_backup.py +0 -0
  280. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_integration.py +0 -0
  281. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_knowledge.py +0 -0
  282. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_addressing.py +0 -0
  283. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_framing.py +0 -0
  284. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_interface.py +0 -0
  285. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_meshtastic_iface.py +0 -0
  286. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_store.py +0 -0
  287. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_transport.py +0 -0
  288. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_mailbox.py +0 -0
  289. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_mailbox_encrypt_at_rest.py +0 -0
  290. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_matrix_adapter.py +0 -0
  291. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_node_registry.py +0 -0
  292. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_observability.py +0 -0
  293. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_outbox_migrate.py +0 -0
  294. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_p2p_connector.py +0 -0
  295. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_p2p_manager.py +0 -0
  296. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_p2p_session.py +0 -0
  297. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_padding.py +0 -0
  298. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pairing.py +0 -0
  299. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pairing_cli.py +0 -0
  300. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_peer_inbox_traversal.py +0 -0
  301. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_peers.py +0 -0
  302. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqc_cutover.py +0 -0
  303. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqc_shim.py +0 -0
  304. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqdm.py +0 -0
  305. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqkem.py +0 -0
  306. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqroute.py +0 -0
  307. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqroute_padding.py +0 -0
  308. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqroute_transport.py +0 -0
  309. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqsig.py +0 -0
  310. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_prune_inbox.py +0 -0
  311. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_receiver_wire_ttl_expiry.py +0 -0
  312. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_registry.py +0 -0
  313. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_registry_cli.py +0 -0
  314. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_failure_log_dedup.py +0 -0
  315. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_perm_backoff.py +0 -0
  316. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_perm_backoff_fixes.py +0 -0
  317. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_quarantine.py +0 -0
  318. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_rail_ordering.py +0 -0
  319. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_route_anon.py +0 -0
  320. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_route_signed.py +0 -0
  321. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_star_broadcast_routing.py +0 -0
  322. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_unsigned_excludes_https_s2s.py +0 -0
  323. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_sign_fail_closed.py +0 -0
  324. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_signaling_broker.py +0 -0
  325. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_signaling_mailbox.py +0 -0
  326. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_signaling_select.py +0 -0
  327. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_announce.py +0 -0
  328. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_cli.py +0 -0
  329. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_directory.py +0 -0
  330. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_operator_pin.py +0 -0
  331. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_readdr.py +0 -0
  332. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_resolve.py +0 -0
  333. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_ssrf.py +0 -0
  334. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_slack_adapter.py +0 -0
  335. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_smoke.py +0 -0
  336. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ssrf_guard.py +0 -0
  337. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_store_forward.py +0 -0
  338. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_syncthing_recipient_guard.py +0 -0
  339. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_telegram_adapter.py +0 -0
  340. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_tofu.py +0 -0
  341. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_typed_message_content.py +0 -0
  342. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_webrtc_ice_logging.py +0 -0
  343. {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_webrtc_signaling_log_dedup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skcomms
3
- Version: 0.2.1
3
+ Version: 0.2.3
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.1"
7
+ version = "0.2.3"
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"}
@@ -15,7 +15,7 @@ FQID / realm layer (pre-alpha stubs, implementations landing in coord tasks):
15
15
  from skcomms.cluster import ...
16
16
  """
17
17
 
18
- __version__ = "0.1.7"
18
+ __version__ = "0.2.3"
19
19
 
20
20
  from .core import SKComms, SKComm
21
21
  from .crypto import EnvelopeCrypto, KeyStore
@@ -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,15 +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
- # KNOWN GAP (follow-up coord f465b407): in-memory cache means a
122
- # daemon restart reopens a replay window on the sk-access surface.
123
- # Migrate to the durable per-node store (see api._get_nonce_cache).
124
- 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
+ )
125
176
  self.audit = audit or AccessAuditLog(node=self.config.node_name)
126
177
  self._register_builtins()
127
178
 
@@ -27,9 +27,9 @@ from fastapi.middleware.cors import CORSMiddleware
27
27
  from pydantic import BaseModel, Field
28
28
  from starlette.middleware.base import BaseHTTPMiddleware
29
29
 
30
+ from . import skfed_directory
30
31
  from .capauth_validator import CapAuthValidator
31
32
  from .core import SKComms
32
- from . import skfed_directory
33
33
  from .discovery import PeerInfo, PeerStore
34
34
  from .heartbeat import HeartbeatConfig, HeartbeatPublisher
35
35
  from .models import (
@@ -224,6 +224,20 @@ async def lifespan(app: FastAPI):
224
224
  logger.exception("Failed to start depth-monitor loop")
225
225
  _depth_monitor_task = None
226
226
 
227
+ # Geo self-seed: opt-in (SKCOMMS_GEO_SEED_FLEET) upsert of the sovereign
228
+ # fleet nodes into the process-global geo store so the skmap pane
229
+ # (GET /api/v1/geo/units) shows units before a live CoT device connects.
230
+ # Fed through the real upsert_from_cot path; clearly labeled fleet:<node>
231
+ # seeds; superseded by real CoT once a feed is wired. See geo_seed.py.
232
+ try:
233
+ from .geo_seed import seed_fleet_if_enabled
234
+
235
+ seeded = seed_fleet_if_enabled()
236
+ if seeded:
237
+ logger.info("Geo store seeded with %d fleet unit(s): %s", len(seeded), seeded)
238
+ except Exception:
239
+ logger.exception("Failed to seed geo store")
240
+
227
241
  yield
228
242
 
229
243
  logger.info("Shutting down SKComms API server...")
@@ -642,6 +656,22 @@ async def health():
642
656
  return body
643
657
 
644
658
 
659
+ @app.get("/.well-known/skworld-module.json", tags=["health"])
660
+ async def skworld_module_manifest(request: Request):
661
+ """skcomms' SKWorld module manifest (public discovery metadata, no bearer).
662
+
663
+ skcomms is a UI-less backend transport service, so this manifest declares no
664
+ UI module: only the operator facet plus a service marker. The shell reads it
665
+ to discover skcomms and learn its health + operator contract before it has a
666
+ token. URLs are origin-relative to the request, so health resolves against
667
+ wherever the api actually answers. Served unauthenticated, like skchat's and
668
+ skcode's manifests.
669
+ """
670
+ from .skworld_manifest import skcomms_module_manifest
671
+
672
+ return skcomms_module_manifest(str(request.base_url))
673
+
674
+
645
675
  # ---------------------------------------------------------------------------
646
676
  # MCP tool relay — used by the consciousness-swipe browser extension.
647
677
  #
@@ -761,6 +791,58 @@ async def get_capabilities():
761
791
  return build_capabilities(get_skcomms())
762
792
 
763
793
 
794
+ @app.get("/api/v1/geo/units", tags=["geo"])
795
+ async def get_geo_units(include_stale: bool = False, format: str = "units"):
796
+ """Current situational picture: live unit / marker / waypoint positions.
797
+
798
+ The backend half of the Flutter ``skmap`` pane. Source of truth is the live
799
+ CoT store in the separate ``skcot`` service process, exposed read-only over
800
+ HTTP (``skcot.geo_http``, default ``http://127.0.0.1:8091/geo/units``, env
801
+ ``SKCOT_GEO_URL``). When that bridge is reachable AND has units, its REAL
802
+ telemetry is returned. Otherwise this falls back to the process-global local
803
+ store (see :mod:`skcomms.geo_store`, the opt-in fleet seed). Both paths are
804
+ fail-soft: an empty or unreachable bridge yields the correct shape, never a
805
+ 500.
806
+
807
+ Args:
808
+ include_stale: Include fixes past their CoT stale time / TTL. Default
809
+ ``False`` (only fresh positions, what a live map wants).
810
+ format: ``"units"`` (default) → ``{"units": [...], "count": N}`` where
811
+ each item is a flat GeoUnit dict (``uid``, ``callsign``, ``lat``,
812
+ ``lon``, ``cot_type``, ``last_seen``, ...); ``"geojson"`` → a
813
+ GeoJSON ``FeatureCollection``. Both are accepted by the app's
814
+ ``GeoUnit.fromJson`` (which reads ``features`` then ``units``).
815
+
816
+ Returns:
817
+ A dict the ``skmap`` client renders directly.
818
+ """
819
+ from .geo_store import fetch_skcot_geo, geo_payload_has_units, get_geo_store
820
+
821
+ fmt = "geojson" if format == "geojson" else "units"
822
+
823
+ # Prefer the live skcot store (real CoT). Blocking fetch off the event loop.
824
+ remote = await asyncio.to_thread(
825
+ fetch_skcot_geo, include_stale=include_stale, fmt=fmt
826
+ )
827
+ if remote is not None and geo_payload_has_units(remote):
828
+ return remote
829
+
830
+ # Fall back to the local in-process store (fleet seed) when skcot is
831
+ # unreachable OR reports zero units. Fail-soft: the local store wraps the
832
+ # shared skcot GeoStore, so if the skcot package is somehow absent this
833
+ # still yields the correct empty shape rather than a 500.
834
+ try:
835
+ store = get_geo_store()
836
+ if fmt == "geojson":
837
+ return store.to_feature_collection(include_stale=include_stale)
838
+ units = store.units_json(include_stale=include_stale)
839
+ return {"units": units, "count": len(units)}
840
+ except Exception: # noqa: BLE001 - the map endpoint must never 500
841
+ if fmt == "geojson":
842
+ return {"type": "FeatureCollection", "features": []}
843
+ return {"units": [], "count": 0}
844
+
845
+
764
846
  class AccessTokenRequest(BaseModel):
765
847
  """Request to mint a capauth token for a single sk-access tool call.
766
848
 
@@ -986,30 +1068,31 @@ _MAX_INBOX_BYTES = 256 * 1024
986
1068
 
987
1069
 
988
1070
  def _nonce_db_path() -> Path:
989
- """Resolve the durable nonce-cache SQLite path.
990
-
991
- ``SKCOMMS_NONCE_DB`` (explicit file path) wins; otherwise the store lives
992
- at ``skcomms_home()/state/nonce_cache.db``. The ``state/`` subtree is
993
- per-node (Syncthing-ignored via .stignore): the replay cache must never
994
- be shared between nodes through file sync (see SOP.md).
995
-
996
- Because the default path sits inside the Syncthing-shared skcomms home,
997
- this also heals a pre-existing ``.stignore`` that lacks the ``state/``
998
- line (live fleets scaffolded before the durable cache existed). Without
999
- that, a live WAL SQLite would sync between nodes: corruption risk plus
1000
- mixed per-node replay history. A failure to write ``.stignore`` raises,
1001
- and :func:`_get_nonce_cache` then fails closed.
1071
+ """Resolve the durable nonce-cache SQLite path (NODE-LOCAL, never synced).
1072
+
1073
+ Precedence: ``SKCOMMS_NONCE_DB`` (explicit file path) wins, then the
1074
+ ``SKCOMMS_NONCE_CACHE_DIR`` directory override, then the XDG state
1075
+ default (``$XDG_STATE_HOME/skcomms/`` or ``~/.local/state/skcomms/``):
1076
+ see :func:`skcomms.home.resolve_nonce_db`.
1077
+
1078
+ The store used to live at ``skcomms_home()/state/nonce_cache.db``; on
1079
+ live fleets that home is inside a Syncthing-shared tree, and when the
1080
+ Syncthing folder is rooted ABOVE the home the home's own ``.stignore``
1081
+ ``state/`` line has no effect, so two nodes synced one live WAL SQLite
1082
+ (conflict copies observed on .158/.41). A replay guard is strictly
1083
+ per-node state and now defaults outside any synced tree. A healthy
1084
+ legacy DB is migrated once; otherwise a fresh cache starts, which is
1085
+ safe because the replay window is bounded by the envelope freshness
1086
+ check (see SOP.md).
1002
1087
  """
1003
1088
  import os as _os
1004
1089
 
1005
1090
  override = (_os.environ.get("SKCOMMS_NONCE_DB") or "").strip()
1006
1091
  if override:
1007
1092
  return Path(override).expanduser()
1008
- from .home import ensure_state_ignored, skcomms_home
1093
+ from .home import resolve_nonce_db
1009
1094
 
1010
- home = skcomms_home()
1011
- ensure_state_ignored(home)
1012
- return home / "state" / "nonce_cache.db"
1095
+ return resolve_nonce_db("nonce_cache.db")
1013
1096
 
1014
1097
 
1015
1098
  def _get_nonce_cache():
@@ -1351,6 +1434,80 @@ async def post_inbox(request: Request):
1351
1434
  return {"ok": True, "id": env.id}
1352
1435
 
1353
1436
 
1437
+ # ---------------------------------------------------------------------------
1438
+ # Public P2P pairing over Tailscale Funnel (card 2ab5aa6c): PUBLIC, opt-in
1439
+ # ---------------------------------------------------------------------------
1440
+ #
1441
+ # Off-tailnet peers cannot reach the tailnet-only mesh, so this route lets a
1442
+ # peer POST their peer bundle to the node's Funnel-exposed pairing endpoint and
1443
+ # bootstrap a P2P pairing. It is the inbound half of ``skcomms.public_pairing``:
1444
+ # the request is routed straight into the EXISTING key exchange
1445
+ # (``import_peer_bundle``), which does all the crypto (armor validation +
1446
+ # fingerprint derivation). The public Funnel is only the bootstrap channel.
1447
+ #
1448
+ # Disabled by default: with no ``SKCOMMS_FUNNEL_URL`` configured the route 404s,
1449
+ # exactly as if it did not exist. When a ``SKCOMMS_FUNNEL_PAIR_TOKEN`` is set,
1450
+ # an inbound request must present a matching token or it is rejected 401.
1451
+
1452
+ _MAX_PAIR_BYTES = 256 * 1024
1453
+
1454
+
1455
+ @app.post("/api/v1/pair/public", tags=["pairing"])
1456
+ async def post_public_pairing(request: Request):
1457
+ """Accept an off-tailnet peer's public pairing request over the Funnel.
1458
+
1459
+ Reads a ``{"bundle": {...}, "token": "..."}`` body, routes it into the
1460
+ existing key exchange, and returns this node's own bundle for mutual
1461
+ bootstrap. 404 when public pairing is not configured (feature off).
1462
+
1463
+ Raises:
1464
+ HTTPException: 404 (feature off), 401 (bad/missing token), 422
1465
+ (missing/malformed bundle), 413 (too large).
1466
+ """
1467
+ from .key_exchange import KeyExchangeError
1468
+ from .public_pairing import (
1469
+ PublicPairingError,
1470
+ PublicPairingRequest,
1471
+ configured_token,
1472
+ funnel_enabled,
1473
+ handle_public_pairing_request,
1474
+ )
1475
+
1476
+ # Disabled by default: no Funnel base configured -> the route does not exist.
1477
+ if not funnel_enabled():
1478
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="not found")
1479
+
1480
+ raw = await request.body()
1481
+ if len(raw) > _MAX_PAIR_BYTES:
1482
+ raise HTTPException(
1483
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
1484
+ detail=f"pairing request exceeds {_MAX_PAIR_BYTES} bytes",
1485
+ )
1486
+
1487
+ import json as _json
1488
+
1489
+ try:
1490
+ body = _json.loads(raw or b"{}")
1491
+ req = PublicPairingRequest(**body)
1492
+ except Exception as exc:
1493
+ raise HTTPException(
1494
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1495
+ detail=f"unparseable pairing request: {exc}",
1496
+ ) from exc
1497
+
1498
+ try:
1499
+ result = handle_public_pairing_request(req, expected_token=configured_token())
1500
+ except PublicPairingError as exc:
1501
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
1502
+ except KeyExchangeError as exc:
1503
+ raise HTTPException(
1504
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)
1505
+ ) from exc
1506
+
1507
+ logger.info("public pairing accepted from %s", (result.get("paired") or {}).get("fqid"))
1508
+ return {"ok": True, **result}
1509
+
1510
+
1354
1511
  # ---------------------------------------------------------------------------
1355
1512
  # Operator consent surface (skfed-consent-design gate 5) — LOCAL ONLY
1356
1513
  # ---------------------------------------------------------------------------
@@ -2515,25 +2672,50 @@ async def get_webrtc_peers(room: Optional[str] = None):
2515
2672
  # Consciousness / Soul Snapshot endpoints
2516
2673
  # ---------------------------------------------------------------------------
2517
2674
 
2518
- try:
2519
- from skcapstone.snapshots import (
2520
- ConversationMessage as _ConversationMessage,
2521
- )
2522
- from skcapstone.snapshots import (
2523
- OOFState as _OOFState,
2524
- )
2525
- from skcapstone.snapshots import (
2526
- PersonalityTraits as _PersonalityTraits,
2527
- )
2528
- from skcapstone.snapshots import (
2529
- SnapshotIndex,
2530
- SnapshotStore,
2531
- SoulSnapshot,
2532
- )
2675
+ # skcapstone.snapshots is an OPTIONAL higher-layer subapp surface. skcomms is
2676
+ # the L0 core and must import without it, so the import is resolved LAZILY on
2677
+ # first use (see ``_load_snapshots``) rather than at module load. These names
2678
+ # stay ``None`` until then; ``from __future__ import annotations`` (top of file)
2679
+ # keeps the string annotations below valid while they are unbound.
2680
+ SnapshotStore = None # type: ignore[assignment]
2681
+ SnapshotIndex = None # type: ignore[assignment]
2682
+ SoulSnapshot = None # type: ignore[assignment]
2683
+ _OOFState = None # type: ignore[assignment]
2684
+ _PersonalityTraits = None # type: ignore[assignment]
2685
+ _ConversationMessage = None # type: ignore[assignment]
2686
+
2687
+ #: Tri-state: ``None`` = not yet probed, ``True``/``False`` = resolved.
2688
+ _SNAPSHOTS_AVAILABLE: Optional[bool] = None
2689
+
2690
+
2691
+ def _load_snapshots() -> bool:
2692
+ """Resolve ``skcapstone.snapshots`` lazily on first use, caching the result.
2693
+
2694
+ The consciousness/snapshot endpoints legitimately bridge to skcapstone, but
2695
+ deferring the import to first use means merely importing ``skcomms.api``
2696
+ never pulls skcapstone into ``sys.modules``. Returns whether the snapshot
2697
+ types are available; callers 501 when they are not.
2698
+ """
2699
+ global _SNAPSHOTS_AVAILABLE
2700
+ global SnapshotStore, SnapshotIndex, SoulSnapshot
2701
+ global _OOFState, _PersonalityTraits, _ConversationMessage
2702
+ if _SNAPSHOTS_AVAILABLE is None:
2703
+ try:
2704
+ # ``global`` above routes each of these bindings to the module name.
2705
+ from skcapstone.snapshots import ConversationMessage as _ConversationMessage
2706
+ from skcapstone.snapshots import OOFState as _OOFState
2707
+ from skcapstone.snapshots import PersonalityTraits as _PersonalityTraits
2708
+ from skcapstone.snapshots import (
2709
+ SnapshotIndex,
2710
+ SnapshotStore,
2711
+ SoulSnapshot,
2712
+ )
2713
+
2714
+ _SNAPSHOTS_AVAILABLE = True
2715
+ except Exception: # ImportError, or a broken partial install
2716
+ _SNAPSHOTS_AVAILABLE = False
2717
+ return _SNAPSHOTS_AVAILABLE
2533
2718
 
2534
- _SNAPSHOTS_AVAILABLE = True
2535
- except ImportError:
2536
- _SNAPSHOTS_AVAILABLE = False
2537
2719
 
2538
2720
  _snapshot_store: Optional[SnapshotStore] = None
2539
2721
 
@@ -2548,7 +2730,7 @@ def _get_store() -> "SnapshotStore":
2548
2730
  HTTPException: If skcapstone is not installed.
2549
2731
  """
2550
2732
  global _snapshot_store
2551
- if not _SNAPSHOTS_AVAILABLE:
2733
+ if not _load_snapshots():
2552
2734
  raise HTTPException(
2553
2735
  status_code=status.HTTP_501_NOT_IMPLEMENTED,
2554
2736
  detail="skcapstone package not installed — cannot manage snapshots",
@@ -128,6 +128,7 @@ _TRANSPORT_PROBES: dict[str, int] = {
128
128
  _MODULE_HINTS: list[str] = [
129
129
  "chats",
130
130
  "spaces",
131
+ "skcode",
131
132
  "activity",
132
133
  "profile",
133
134
  "skmap",
@@ -15,8 +15,8 @@ The detached PGP signature covers the UTF-8 string::
15
15
  A ±300-second window is accepted to tolerate clock skew while preventing
16
16
  replay attacks. The signer's public key is resolved from:
17
17
 
18
- 1. ``~/.skcapstone/skcomms/keys/<FINGERPRINT>.asc`` — SKComms per-agent keystore
19
- 2. ``gpg --export --armor <FINGERPRINT>`` — system GPG keyring
18
+ 1. ``~/.skcapstone/skcomms/keys/<FINGERPRINT>.asc`` - SKComms per-agent keystore
19
+ 2. ``gpg --export --armor <FINGERPRINT>`` - system GPG keyring
20
20
 
21
21
  **Remote validation** is available by setting ``capauth_url`` to a
22
22
  CapAuth API base URL. Remote is tried first; on unreachable server the
@@ -77,7 +77,7 @@ def _find_pubkey_by_fingerprint(
77
77
  for path in glob.glob(pattern, recursive=True):
78
78
  try:
79
79
  key, _ = pgpy.PGPKey.from_file(path)
80
- except Exception: # noqa: BLE001 — skip unreadable/non-key files
80
+ except Exception: # noqa: BLE001 - skip unreadable/non-key files
81
81
  continue
82
82
  if str(key.fingerprint).replace(" ", "").upper() == want:
83
83
  return str(key)
@@ -88,6 +88,78 @@ logger = logging.getLogger("skcomms.capauth_validator")
88
88
  # PGP fingerprint: 40 hex characters
89
89
  _FINGERPRINT_RE = re.compile(r"^[0-9A-Fa-f]{40}$")
90
90
 
91
+
92
+ def _reject_reason_if_key_unusable(pub_key, signer_keyids) -> Optional[str]:
93
+ """Return a reason string if the signing key is revoked or expired, else None.
94
+
95
+ SECURITY: pgpy's own ``PGPKey.verify()`` performs NEITHER revocation NOR
96
+ expiry checks (its source literally warns "Revocation checks are not yet
97
+ implemented"), so a signature made by a revoked or expired key verifies as
98
+ valid. That let a revoked/expired CapAuth signing key keep authenticating on
99
+ the WebRTC signaling / SDP auth path (card 6abe9bef).
100
+
101
+ This routes the usability decision through capauth's single source of truth,
102
+ ``capauth.crypto.pgpy_backend._assert_key_usable`` (the same helper the
103
+ capauth backend uses, card a93b0528). When that helper cannot be imported we
104
+ fall back to an inline check that mirrors it, so skcomms stays fail-closed
105
+ and never authenticates a revoked/expired signing key even if capauth's
106
+ internal layout changes.
107
+
108
+ Args:
109
+ pub_key: Parsed pgpy public key (primary, with subkeys).
110
+ signer_keyids: Set of 16-hex key IDs that produced the signature being
111
+ verified. Scopes subkey checks to the actual signer.
112
+
113
+ Returns:
114
+ A human-readable reason string when the key must be rejected, or None
115
+ when the key is usable.
116
+ """
117
+ try:
118
+ from capauth.crypto.pgpy_backend import _assert_key_usable
119
+ from capauth.exceptions import KeyExpiredError, KeyRevokedError
120
+ except Exception: # noqa: BLE001 - capauth helper unavailable, use fallback
121
+ _assert_key_usable = None
122
+
123
+ if _assert_key_usable is not None:
124
+ try:
125
+ _assert_key_usable(pub_key, signer_keyids)
126
+ return None
127
+ except (KeyRevokedError, KeyExpiredError) as exc:
128
+ return str(exc)
129
+ except Exception as exc: # noqa: BLE001 - unexpected: fail closed
130
+ return f"key usability check errored: {exc}"
131
+
132
+ # Inline fallback mirroring capauth.crypto.pgpy_backend._assert_key_usable.
133
+ try:
134
+ fingerprint = str(pub_key.fingerprint).replace(" ", "")
135
+ for _rev in pub_key.revocation_signatures:
136
+ return f"key {fingerprint} carries a revocation signature"
137
+ if pub_key.is_expired:
138
+ return f"key {fingerprint} is expired"
139
+ for keyid, subkey in pub_key.subkeys.items():
140
+ if signer_keyids and keyid not in signer_keyids:
141
+ continue
142
+ for _rev in subkey.revocation_signatures:
143
+ return f"signing subkey {keyid} of key {fingerprint} carries a revocation signature"
144
+ if signer_keyids and keyid in signer_keyids and subkey.is_expired:
145
+ return f"signing subkey {keyid} of key {fingerprint} is expired"
146
+ except Exception as exc: # noqa: BLE001 - cannot determine usability: fail closed
147
+ return f"could not verify key usability: {exc}"
148
+ return None
149
+
150
+
151
+ def _signer_keyids(sig) -> set:
152
+ """Best-effort set of 16-hex signer key IDs from a pgpy signature object.
153
+
154
+ Returns an empty set when the signer cannot be read; an empty set makes the
155
+ revocation/expiry check apply to the primary key and every subkey (the
156
+ broadest, safest scope).
157
+ """
158
+ try:
159
+ return {sig.signer}
160
+ except Exception: # noqa: BLE001
161
+ return set()
162
+
91
163
  # Replay-prevention window: tokens older than this (or future-dated beyond
92
164
  # this) are rejected.
93
165
  _TOKEN_WINDOW_SECS = 300 # ±5 minutes
@@ -152,7 +224,7 @@ class CapAuthValidator:
152
224
  """
153
225
  if not token:
154
226
  if self._require_auth:
155
- logger.warning("WebRTC signaling: no auth token — rejecting connection")
227
+ logger.warning("WebRTC signaling: no auth token - rejecting connection")
156
228
  return None
157
229
  return "anonymous"
158
230
 
@@ -233,13 +305,13 @@ class CapAuthValidator:
233
305
  # Require exactly 3 parts for signed tokens. #
234
306
  # ------------------------------------------------------------------ #
235
307
  # SECURITY (fail-closed): a token that *looks* signed (>1 part) but is
236
- # missing its signature CANNOT be verified. Deny it in every mode —
237
- # even permissive — so a peer can never be upgraded to a claimed
308
+ # missing its signature CANNOT be verified. Deny it in every mode -
309
+ # even permissive - so a peer can never be upgraded to a claimed
238
310
  # identity it did not prove. The only permissive convenience is the
239
311
  # explicit single-part dev token handled above.
240
312
  if len(parts) != 3:
241
313
  logger.warning(
242
- "CapAuth local: expected fingerprint.timestamp.sig, got %d parts — rejecting",
314
+ "CapAuth local: expected fingerprint.timestamp.sig, got %d parts - rejecting",
243
315
  len(parts),
244
316
  )
245
317
  return None
@@ -279,14 +351,24 @@ class CapAuthValidator:
279
351
  pub_key = self._load_public_key(fingerprint)
280
352
  if pub_key is None:
281
353
  # SECURITY (fail-closed): without the signer's public key we
282
- # cannot verify the signature. Deny in every mode — never trust
354
+ # cannot verify the signature. Deny in every mode - never trust
283
355
  # the self-asserted fingerprint just because we lack its key.
284
356
  logger.warning(
285
- "CapAuth local: public key not found for %s — rejecting (cannot verify)",
357
+ "CapAuth local: public key not found for %s - rejecting (cannot verify)",
286
358
  fingerprint,
287
359
  )
288
360
  return None
289
361
 
362
+ # SECURITY (fail-closed): reject revoked/expired signing keys before
363
+ # trusting the signature. pgpy.verify() ignores revocation/expiry, so
364
+ # route this decision through capauth's usability check (card 6abe9bef).
365
+ reason = _reject_reason_if_key_unusable(pub_key, _signer_keyids(sig))
366
+ if reason:
367
+ logger.warning(
368
+ "CapAuth local: signing key rejected for %s: %s", fingerprint, reason
369
+ )
370
+ return None
371
+
290
372
  # The message that was signed: "capauth:<FINGERPRINT>:<TIMESTAMP>"
291
373
  signed_text = f"capauth:{fingerprint}:{timestamp_str}"
292
374
  result = pub_key.verify(signed_text, sig)
@@ -299,7 +381,7 @@ class CapAuthValidator:
299
381
 
300
382
  except ImportError:
301
383
  logger.warning(
302
- "pgpy not installed — cannot verify CapAuth signature; rejecting. "
384
+ "pgpy not installed - cannot verify CapAuth signature; rejecting. "
303
385
  "Install skcomms[crypto] for full CapAuth PGP verification."
304
386
  )
305
387
  # SECURITY (fail-closed): without pgpy we cannot verify the
@@ -312,7 +394,7 @@ class CapAuthValidator:
312
394
  # SECURITY (fail-closed): any error mid-verification is a denial,
313
395
  # never a fall-through to the claimed identity.
314
396
  logger.error(
315
- "CapAuth local: PGP verification error for %s: %s — rejecting",
397
+ "CapAuth local: PGP verification error for %s: %s - rejecting",
316
398
  fingerprint,
317
399
  exc,
318
400
  )
@@ -323,8 +405,8 @@ class CapAuthValidator:
323
405
 
324
406
  Search order:
325
407
 
326
- 1. ``~/.skcapstone/skcomms/keys/<FINGERPRINT>.asc`` — SKComms per-agent keystore
327
- 2. ``gpg --export --armor <FINGERPRINT>`` — system GPG keyring
408
+ 1. ``~/.skcapstone/skcomms/keys/<FINGERPRINT>.asc`` - SKComms per-agent keystore
409
+ 2. ``gpg --export --armor <FINGERPRINT>`` - system GPG keyring
328
410
 
329
411
  Args:
330
412
  fingerprint: 40-char uppercase hex PGP fingerprint.
@@ -349,7 +431,7 @@ class CapAuthValidator:
349
431
  # 2. Per-agent CapAuth identity dirs + skcomms peers store, matched by
350
432
  # fingerprint. Reconciles the WebRTC verify path with the mailbox/TOFU
351
433
  # layer (2026-06-11) so a *local* agent's signed SDP (e.g. opus↔lumina)
352
- # verifies — those keys live under capauth/identity, not ~/.skcapstone/skcomms/keys.
434
+ # verifies - those keys live under capauth/identity, not ~/.skcapstone/skcomms/keys.
353
435
  armor = _find_pubkey_by_fingerprint(fingerprint)
354
436
  if armor:
355
437
  try:
@@ -434,6 +516,16 @@ class CapAuthValidator:
434
516
  logger.warning("verify_detached: public key not found for %s", fingerprint)
435
517
  return False
436
518
 
519
+ # SECURITY (fail-closed): reject revoked/expired signing keys before
520
+ # trusting the signature. pgpy.verify() ignores revocation/expiry, so
521
+ # route this decision through capauth's usability check (card 6abe9bef).
522
+ reason = _reject_reason_if_key_unusable(pub_key, _signer_keyids(pgp_sig))
523
+ if reason:
524
+ logger.warning(
525
+ "verify_detached: signing key rejected for %s: %s", fingerprint, reason
526
+ )
527
+ return False
528
+
437
529
  result = pub_key.verify(signed_payload, pgp_sig)
438
530
  if not bool(result):
439
531
  logger.warning("verify_detached: PGP signature INVALID for %s", fingerprint)
@@ -444,7 +536,7 @@ class CapAuthValidator:
444
536
 
445
537
  except ImportError:
446
538
  logger.warning(
447
- "pgpy not installed — cannot verify SDP signature. "
539
+ "pgpy not installed - cannot verify SDP signature. "
448
540
  "Install skcomms[crypto] for full CapAuth PGP verification."
449
541
  )
450
542
  return False