skcomms 0.2.2__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 (342) hide show
  1. {skcomms-0.2.2 → skcomms-0.2.3}/PKG-INFO +1 -1
  2. {skcomms-0.2.2 → skcomms-0.2.3}/pyproject.toml +1 -1
  3. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/__init__.py +1 -1
  4. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/api.py +66 -0
  5. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/capabilities.py +1 -0
  6. skcomms-0.2.3/src/skcomms/geo_seed.py +157 -0
  7. skcomms-0.2.3/src/skcomms/geo_store.py +231 -0
  8. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/operator_probe.py +26 -5
  9. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqdm.py +229 -0
  10. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/PKG-INFO +1 -1
  11. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/SOURCES.txt +8 -0
  12. skcomms-0.2.3/tests/test_api_geo_seed.py +119 -0
  13. skcomms-0.2.3/tests/test_api_geo_skcot_bridge.py +223 -0
  14. skcomms-0.2.3/tests/test_api_geo_units.py +139 -0
  15. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capabilities.py +4 -0
  16. skcomms-0.2.3/tests/test_geo_roundtrip_skcot.py +141 -0
  17. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_operator_cli.py +20 -0
  18. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_p2p_media.py +10 -3
  19. skcomms-0.2.3/tests/test_pqdm2.py +108 -0
  20. skcomms-0.2.3/tests/test_pqdm2_interop.py +80 -0
  21. {skcomms-0.2.2 → skcomms-0.2.3}/LICENSE +0 -0
  22. {skcomms-0.2.2 → skcomms-0.2.3}/README.md +0 -0
  23. {skcomms-0.2.2 → skcomms-0.2.3}/setup.cfg +0 -0
  24. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/__init__.py +0 -0
  25. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/audit.py +0 -0
  26. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/config.py +0 -0
  27. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/exec.py +0 -0
  28. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/files.py +0 -0
  29. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/grants.py +0 -0
  30. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/knowledge.py +0 -0
  31. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/registry.py +0 -0
  32. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/routing.py +0 -0
  33. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/server.py +0 -0
  34. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/wiring.py +0 -0
  35. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/ack.py +0 -0
  36. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/__init__.py +0 -0
  37. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/base.py +0 -0
  38. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/discord.py +0 -0
  39. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/factory.py +0 -0
  40. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/fake.py +0 -0
  41. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/matrix.py +0 -0
  42. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/models.py +0 -0
  43. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/registry.py +0 -0
  44. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/slack.py +0 -0
  45. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/telegram.py +0 -0
  46. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/anon_queue.py +0 -0
  47. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/anon_transport.py +0 -0
  48. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/capauth_validator.py +0 -0
  49. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/cli.py +0 -0
  50. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/cluster.py +0 -0
  51. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/compression.py +0 -0
  52. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/config.py +0 -0
  53. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent.py +0 -0
  54. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_banfeeds.py +0 -0
  55. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_captcha.py +0 -0
  56. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_greylist.py +0 -0
  57. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_group_gate.py +0 -0
  58. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_groups.py +0 -0
  59. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_moderation.py +0 -0
  60. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_pipeline.py +0 -0
  61. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_policy.py +0 -0
  62. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_requests.py +0 -0
  63. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_runtime.py +0 -0
  64. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_tiering.py +0 -0
  65. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_tokens.py +0 -0
  66. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/core.py +0 -0
  67. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/crypto.py +0 -0
  68. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/crypto_suites.py +0 -0
  69. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/did_router.py +0 -0
  70. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/discovery.py +0 -0
  71. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/envelope.py +0 -0
  72. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/federation.py +0 -0
  73. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/__init__.py +0 -0
  74. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/codebook.py +0 -0
  75. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/codec.py +0 -0
  76. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/emergent.py +0 -0
  77. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/gloss.py +0 -0
  78. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/handshake.py +0 -0
  79. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/macros.py +0 -0
  80. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/message.py +0 -0
  81. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/session.py +0 -0
  82. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/grants.py +0 -0
  83. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/heartbeat.py +0 -0
  84. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/home.py +0 -0
  85. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/household_router.py +0 -0
  86. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/housekeeping.py +0 -0
  87. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/identity.py +0 -0
  88. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/integration.py +0 -0
  89. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/key_exchange.py +0 -0
  90. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/mailbox.py +0 -0
  91. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/marketplace.py +0 -0
  92. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/mcp_server.py +0 -0
  93. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/metrics.py +0 -0
  94. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/models.py +0 -0
  95. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/node_registry.py +0 -0
  96. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/nostr_discovery.py +0 -0
  97. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/observability.py +0 -0
  98. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/outbox.py +0 -0
  99. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/outbox_migrate.py +0 -0
  100. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/padding.py +0 -0
  101. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pairing.py +0 -0
  102. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pairing_mirror.py +0 -0
  103. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/paths.py +0 -0
  104. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/peers.py +0 -0
  105. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pq_provider.py +0 -0
  106. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqkem.py +0 -0
  107. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqkem_backend.py +0 -0
  108. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqroute.py +0 -0
  109. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqroute_transport.py +0 -0
  110. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqsig.py +0 -0
  111. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/profile_router.py +0 -0
  112. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/public_pairing.py +0 -0
  113. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pubsub.py +0 -0
  114. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pubsub_transport.py +0 -0
  115. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/queue.py +0 -0
  116. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/ratelimit.py +0 -0
  117. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/realm.py +0 -0
  118. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/registry.py +0 -0
  119. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/router.py +0 -0
  120. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/signaling.py +0 -0
  121. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/signing.py +0 -0
  122. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skfed_announce.py +0 -0
  123. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skfed_directory.py +0 -0
  124. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skfed_readdr.py +0 -0
  125. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skfed_resolve.py +0 -0
  126. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skworld_manifest.py +0 -0
  127. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/souls_router.py +0 -0
  128. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/ssrf.py +0 -0
  129. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/store_forward.py +0 -0
  130. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/syncthing_rail.py +0 -0
  131. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/tofu.py +0 -0
  132. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transport.py +0 -0
  133. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/__init__.py +0 -0
  134. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/audio_track.py +0 -0
  135. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/__init__.py +0 -0
  136. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/gatt.py +0 -0
  137. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/identity.py +0 -0
  138. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/node.py +0 -0
  139. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/noise.py +0 -0
  140. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/protocol.py +0 -0
  141. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/radio.py +0 -0
  142. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/relay.py +0 -0
  143. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/broker_server.py +0 -0
  144. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/file.py +0 -0
  145. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/http_s2s.py +0 -0
  146. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/__init__.py +0 -0
  147. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/addressing.py +0 -0
  148. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/framing.py +0 -0
  149. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/interface.py +0 -0
  150. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/meshtastic_iface.py +0 -0
  151. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/store.py +0 -0
  152. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/transport.py +0 -0
  153. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/nostr.py +0 -0
  154. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/p2p_connector.py +0 -0
  155. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/p2p_manager.py +0 -0
  156. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/p2p_session.py +0 -0
  157. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/signaling_base.py +0 -0
  158. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/signaling_broker.py +0 -0
  159. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/signaling_mailbox.py +0 -0
  160. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/syncthing.py +0 -0
  161. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/tailscale.py +0 -0
  162. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/video_track.py +0 -0
  163. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/webrtc.py +0 -0
  164. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/webrtc_media.py +0 -0
  165. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/websocket.py +0 -0
  166. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/trustbackup.py +0 -0
  167. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/dependency_links.txt +0 -0
  168. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/entry_points.txt +0 -0
  169. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/requires.txt +0 -0
  170. {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/top_level.txt +0 -0
  171. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_exec.py +0 -0
  172. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_files.py +0 -0
  173. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_nonce_durable.py +0 -0
  174. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_rbac.py +0 -0
  175. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_routing.py +0 -0
  176. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_server.py +0 -0
  177. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_wiring.py +0 -0
  178. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ack_bind.py +0 -0
  179. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_adapter_factory.py +0 -0
  180. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_adapter_fake.py +0 -0
  181. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_anon_queue.py +0 -0
  182. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_anon_transport.py +0 -0
  183. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_access_token.py +0 -0
  184. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_consent.py +0 -0
  185. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_consent_token_gate.py +0 -0
  186. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_cors.py +0 -0
  187. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_federation_inbox.py +0 -0
  188. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_health.py +0 -0
  189. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_send_signed_wire.py +0 -0
  190. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_skfed_directory.py +0 -0
  191. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_backfill_file_locations.py +0 -0
  192. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_fragment.py +0 -0
  193. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_gatt.py +0 -0
  194. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_identity.py +0 -0
  195. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_mesh.py +0 -0
  196. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_noise.py +0 -0
  197. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_pairing_bundle.py +0 -0
  198. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_protocol.py +0 -0
  199. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_radio.py +0 -0
  200. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_relay.py +0 -0
  201. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_broker_server.py +0 -0
  202. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capauth_failclosed.py +0 -0
  203. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capauth_key_reconcile.py +0 -0
  204. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capauth_resolution.py +0 -0
  205. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capauth_revoked_expired.py +0 -0
  206. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_channel_adapter.py +0 -0
  207. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_cluster_schema.py +0 -0
  208. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_config.py +0 -0
  209. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent.py +0 -0
  210. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_ban_gate_fail_closed.py +0 -0
  211. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_banfeeds.py +0 -0
  212. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_captcha.py +0 -0
  213. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_cli.py +0 -0
  214. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_cli_mgmt.py +0 -0
  215. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_greylist.py +0 -0
  216. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_group_gate.py +0 -0
  217. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_groups.py +0 -0
  218. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_moderation.py +0 -0
  219. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_pipeline.py +0 -0
  220. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_policy.py +0 -0
  221. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_requests.py +0 -0
  222. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_runtime.py +0 -0
  223. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_tiering.py +0 -0
  224. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_tokens.py +0 -0
  225. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_core_inbound_beacon_skip.py +0 -0
  226. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_core_send_federated.py +0 -0
  227. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_core_send_signed.py +0 -0
  228. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_core_stop.py +0 -0
  229. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_crypto_failclosed.py +0 -0
  230. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_crypto_suites_q0.py +0 -0
  231. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_daemon_adapter_lifecycle.py +0 -0
  232. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_deadletter_ops.py +0 -0
  233. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_delivery_semantics.py +0 -0
  234. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_discord_adapter.py +0 -0
  235. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_encrypt_fail_closed.py +0 -0
  236. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_envelope_consent_token.py +0 -0
  237. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_envelope_v1.py +0 -0
  238. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_federation.py +0 -0
  239. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_federation_integration.py +0 -0
  240. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_codebook.py +0 -0
  241. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_codec_l0.py +0 -0
  242. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_codec_l1.py +0 -0
  243. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_codec_l2.py +0 -0
  244. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_emergent.py +0 -0
  245. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_emergent_negotiator.py +0 -0
  246. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_emergent_protocol.py +0 -0
  247. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_gloss.py +0 -0
  248. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_handshake.py +0 -0
  249. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_handshake_lexicon.py +0 -0
  250. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_macros.py +0 -0
  251. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_macros_render.py +0 -0
  252. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_message.py +0 -0
  253. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_session.py +0 -0
  254. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_session_macros.py +0 -0
  255. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_to_human.py +0 -0
  256. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_grants.py +0 -0
  257. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_home_scaffold.py +0 -0
  258. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_housekeeping.py +0 -0
  259. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_http_s2s_star_recipient.py +0 -0
  260. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_http_s2s_transport.py +0 -0
  261. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_hybrid_envelope_sig.py +0 -0
  262. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_identity_env_override.py +0 -0
  263. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_identity_key_binding.py +0 -0
  264. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_identity_realm.py +0 -0
  265. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_identity_trust_backup.py +0 -0
  266. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_integration.py +0 -0
  267. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_key_exchange_transport_scoping.py +0 -0
  268. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_knowledge.py +0 -0
  269. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_addressing.py +0 -0
  270. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_framing.py +0 -0
  271. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_interface.py +0 -0
  272. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_meshtastic_iface.py +0 -0
  273. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_store.py +0 -0
  274. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_transport.py +0 -0
  275. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_mailbox.py +0 -0
  276. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_mailbox_encrypt_at_rest.py +0 -0
  277. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_mailbox_recipient_key.py +0 -0
  278. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_matrix_adapter.py +0 -0
  279. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_no_subapp_dependency.py +0 -0
  280. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_node_registry.py +0 -0
  281. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_nonce_durable.py +0 -0
  282. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_nostr_discovery.py +0 -0
  283. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_observability.py +0 -0
  284. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_outbox_home_alignment.py +0 -0
  285. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_outbox_migrate.py +0 -0
  286. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_p2p_connector.py +0 -0
  287. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_p2p_manager.py +0 -0
  288. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_p2p_session.py +0 -0
  289. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_padding.py +0 -0
  290. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pairing.py +0 -0
  291. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pairing_cli.py +0 -0
  292. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pairing_mirror.py +0 -0
  293. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_paths_scoping.py +0 -0
  294. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_peer_inbox_traversal.py +0 -0
  295. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_peers.py +0 -0
  296. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqc_cutover.py +0 -0
  297. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqc_shim.py +0 -0
  298. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqdm.py +0 -0
  299. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqkem.py +0 -0
  300. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqroute.py +0 -0
  301. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqroute_padding.py +0 -0
  302. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqroute_transport.py +0 -0
  303. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqsig.py +0 -0
  304. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_prune_inbox.py +0 -0
  305. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_public_pairing.py +0 -0
  306. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_queue_bounds.py +0 -0
  307. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_receiver_wire_ttl_expiry.py +0 -0
  308. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_registry.py +0 -0
  309. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_registry_cli.py +0 -0
  310. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_failure_log_dedup.py +0 -0
  311. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_perm_backoff.py +0 -0
  312. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_perm_backoff_fixes.py +0 -0
  313. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_quarantine.py +0 -0
  314. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_rail_ordering.py +0 -0
  315. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_route_anon.py +0 -0
  316. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_route_signed.py +0 -0
  317. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_star_broadcast_routing.py +0 -0
  318. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_unsigned_excludes_https_s2s.py +0 -0
  319. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_sign_fail_closed.py +0 -0
  320. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_signaling_broker.py +0 -0
  321. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_signaling_mailbox.py +0 -0
  322. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_signaling_select.py +0 -0
  323. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_announce.py +0 -0
  324. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_cli.py +0 -0
  325. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_directory.py +0 -0
  326. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_operator_pin.py +0 -0
  327. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_readdr.py +0 -0
  328. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_resolve.py +0 -0
  329. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_ssrf.py +0 -0
  330. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skworld_manifest.py +0 -0
  331. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_slack_adapter.py +0 -0
  332. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_smoke.py +0 -0
  333. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ssrf_guard.py +0 -0
  334. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_store_forward.py +0 -0
  335. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_syncthing_rail.py +0 -0
  336. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_syncthing_recipient_guard.py +0 -0
  337. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_telegram_adapter.py +0 -0
  338. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_tofu.py +0 -0
  339. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_typed_message_content.py +0 -0
  340. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_webrtc_bridge_lifecycle.py +0 -0
  341. {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_webrtc_ice_logging.py +0 -0
  342. {skcomms-0.2.2 → 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.2
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.2"
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
@@ -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...")
@@ -777,6 +791,58 @@ async def get_capabilities():
777
791
  return build_capabilities(get_skcomms())
778
792
 
779
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
+
780
846
  class AccessTokenRequest(BaseModel):
781
847
  """Request to mint a capauth token for a single sk-access tool call.
782
848
 
@@ -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",
@@ -0,0 +1,157 @@
1
+ """Fleet self-seed for the SKMap geo store (``GET /api/v1/geo/units``).
2
+
3
+ Why this exists
4
+ ---------------
5
+ The situational picture served by ``/api/v1/geo/units`` is fed by the CoT
6
+ (Cursor-on-Target) plane. On this fleet the live CoT service (``skcot.service``,
7
+ TAK/ATAK streaming on :8087/:8089/:6969) runs as a **separate process** with its
8
+ own in-memory ``GeoStore``; it only fills up when a real ATAK/iTAK device (or a
9
+ federated peer's CoT) actually beacons a position. Until a device connects, that
10
+ store -- and therefore the ``skcomms-api`` process's own ``get_geo_store()`` -- is
11
+ empty, so the Flutter ``skmap`` pane shows nothing.
12
+
13
+ This module upserts the **sovereign fleet nodes** as CoT units so the map is not
14
+ empty and the whole plumbing (``upsert_from_cot`` -> store -> endpoint ->
15
+ ``GeoUnit.fromJson``) is demonstrably working end to end. It is fed through the
16
+ *real* :meth:`skcot.geo.GeoStore.upsert_from_cot` path (building genuine
17
+ :class:`skcot.codec.CotEvent` atoms), not a shortcut, so it exercises exactly the
18
+ code a live feed uses. On a deployment without ``skcot`` installed it falls back
19
+ to the store's plain ``upsert`` so the seed still lands.
20
+
21
+ Clearly a seed, not real telemetry
22
+ -----------------------------------
23
+ Every seeded entity is unmistakable as a placeholder:
24
+
25
+ * ``uid`` / ``callsign`` are prefixed ``fleet:`` (e.g. ``fleet:chiap08``),
26
+ * ``source`` is ``"seed:self"``,
27
+ * coordinates are **placeholders** -- these compute nodes have no GPS. They are
28
+ spread out only so the markers don't stack on the map.
29
+
30
+ Replacing the seed with a real CoT feed
31
+ ---------------------------------------
32
+ When a real position feed is wired (an ATAK/iTAK device connecting to
33
+ ``skcot.service``, or CoT federated in from a peer), those live units flow into
34
+ the same store and supersede-by-``uid``. The seed units keep their ``fleet:``
35
+ uids and simply coexist; set ``SKCOMMS_GEO_SEED_FLEET=0`` to turn the seed off
36
+ entirely once real telemetry is present.
37
+
38
+ Enablement
39
+ ----------
40
+ Off by default (the empty-store contract stays intact for anything that doesn't
41
+ opt in). Set ``SKCOMMS_GEO_SEED_FLEET`` to a truthy value (``1``/``true``/``yes``/
42
+ ``on``) to have the ``skcomms-api`` lifespan seed the fleet on startup.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import logging
48
+ import os
49
+ from typing import Any, Optional
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+ # Env flag that opts the api-server lifespan into seeding the fleet on startup.
54
+ SEED_ENV = "SKCOMMS_GEO_SEED_FLEET"
55
+
56
+ # Attribution tag stored on each seeded unit's ``GeoUnit.source``.
57
+ SEED_SOURCE = "seed:self"
58
+
59
+ # CoT type for a friendly ground unit (atom -> classified as ``kind='unit'``).
60
+ _FLEET_COT_TYPE = "a-f-G-U-C"
61
+
62
+
63
+ # Known sovereign fleet nodes. ``lat``/``lon`` are PLACEHOLDERS (these nodes have
64
+ # no GPS); they are only spread apart so seeded markers are individually visible.
65
+ # ``hint`` is the tailscale/LAN address for human reference (not a GeoUnit field).
66
+ _FLEET_NODES: list[dict[str, Any]] = [
67
+ {"name": "noroc2027", "hint": "100.108.59.57 / .158 dev+primary (this host)",
68
+ "lat": 39.0000, "lon": -77.0000},
69
+ {"name": "dot100", "hint": "192.168.0.100 RTX 5060 Ti / inference",
70
+ "lat": 39.0200, "lon": -77.0200},
71
+ {"name": "dot41", "hint": "100.86.156.5 / .41 tailscale-only heavy-build",
72
+ "lat": 38.9800, "lon": -77.0200},
73
+ {"name": "chiap08", "hint": "100.81.238.58 terminal node",
74
+ "lat": 39.0200, "lon": -76.9800},
75
+ ]
76
+
77
+
78
+ def is_seed_enabled() -> bool:
79
+ """Whether the fleet self-seed is opted in via :data:`SEED_ENV`."""
80
+ return os.environ.get(SEED_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
81
+
82
+
83
+ def _upsert_node(store: Any, node: dict[str, Any]) -> Optional[str]:
84
+ """Upsert a single fleet node into *store*; return its uid or ``None``.
85
+
86
+ Feeds through the real ``upsert_from_cot`` path (a genuine CoT atom) when the
87
+ ``skcot`` plane is available, else falls back to the store's plain ``upsert``
88
+ (used by the in-process fallback store). Fails soft: logs and returns
89
+ ``None`` on any error so one bad node can't abort the whole seed.
90
+ """
91
+ name = node["name"]
92
+ uid = f"fleet:{name}"
93
+ lat, lon = float(node["lat"]), float(node["lon"])
94
+ try:
95
+ try:
96
+ # Real path: build a CoT atom and let the geo plane classify + store it.
97
+ from skcot.codec import CotEvent, CotPoint
98
+
99
+ cot = CotEvent(
100
+ uid=uid,
101
+ type=_FLEET_COT_TYPE,
102
+ how="m-g",
103
+ point=CotPoint(lat=lat, lon=lon),
104
+ callsign=uid,
105
+ )
106
+ unit = store.upsert_from_cot(cot, source=SEED_SOURCE)
107
+ # upsert_from_cot returns None only if it classified the event as
108
+ # non-mappable; an ``a-*`` atom with a real fix never does.
109
+ return getattr(unit, "uid", uid) if unit is not None else None
110
+ except (ImportError, NotImplementedError):
111
+ # Fallback store (no skcot): upsert a plain GeoUnit-shaped dict.
112
+ store.upsert(
113
+ {
114
+ "uid": uid,
115
+ "callsign": uid,
116
+ "cot_type": _FLEET_COT_TYPE,
117
+ "lat": lat,
118
+ "lon": lon,
119
+ "kind": "unit",
120
+ "source": SEED_SOURCE,
121
+ }
122
+ )
123
+ return uid
124
+ except Exception as exc: # noqa: BLE001 — one bad node must not abort the seed
125
+ logger.warning("geo seed: could not upsert %s (%s): %s", uid, node.get("hint"), exc)
126
+ return None
127
+
128
+
129
+ def seed_fleet(store: Any = None) -> list[str]:
130
+ """Upsert the sovereign fleet nodes as seed CoT units into *store*.
131
+
132
+ Idempotent: entities are keyed by ``uid`` (``fleet:<name>``), so re-running
133
+ updates in place rather than duplicating. Returns the list of upserted uids.
134
+
135
+ Args:
136
+ store: The geo store to seed. Defaults to the process-global store from
137
+ :func:`skcomms.geo_store.get_geo_store` (the one the endpoint reads).
138
+ """
139
+ if store is None:
140
+ from .geo_store import get_geo_store
141
+
142
+ store = get_geo_store()
143
+ uids = [uid for node in _FLEET_NODES if (uid := _upsert_node(store, node))]
144
+ logger.info("geo seed: upserted %d fleet unit(s): %s", len(uids), uids)
145
+ return uids
146
+
147
+
148
+ def seed_fleet_if_enabled(store: Any = None) -> list[str]:
149
+ """Seed the fleet only when :data:`SEED_ENV` is opted in; else a no-op.
150
+
151
+ The startup hook: safe to call unconditionally from the api-server lifespan.
152
+ Returns the upserted uids (empty when the seed is not enabled).
153
+ """
154
+ if not is_seed_enabled():
155
+ logger.info("geo seed: %s not set — fleet self-seed disabled", SEED_ENV)
156
+ return []
157
+ return seed_fleet(store)
@@ -0,0 +1,231 @@
1
+ """Geo / situational-awareness store accessor for the SKComms daemon.
2
+
3
+ Serves ``GET /api/v1/geo/units`` (the backend half of the Flutter ``skmap``
4
+ pane). The situational picture, where every unit / marker / waypoint is, is
5
+ produced by the CoT (Cursor-on-Target) bridge, which now lives in the extracted
6
+ ``skcot`` package. This module exposes a single **process-global** store that:
7
+
8
+ * the CoT bridge / service FEEDS →
9
+ ``get_geo_store().upsert_from_cot(cot, source=...)``
10
+ (or ``.upsert(unit)`` for a pre-built / federated entity), and
11
+ * the ``/api/v1/geo/units`` endpoint READS →
12
+ ``get_geo_store().units_json(include_stale=...)``.
13
+
14
+ That single accessor is the documented seam for wiring the real position feed:
15
+ whoever runs the CoT service inside (or alongside) the daemon upserts into the
16
+ store returned by :func:`get_geo_store`, and the endpoint publishes it.
17
+
18
+ One shared type
19
+ ---------------
20
+ There is a single ``GeoUnit`` / ``GeoStore`` type, and it lives in
21
+ :mod:`skcot.geo` (the authoritative, CoT-fed store). This module does **not**
22
+ reimplement it. :class:`SkcotGeoStore` is a *thin adapter* over that shared
23
+ type: it holds a real :class:`skcot.geo.GeoStore` and delegates everything to
24
+ it, adding only one skcomms-side convenience -- dict-coercion on ``upsert`` so
25
+ the fleet self-seed (and tests) can feed plain GeoUnit-shaped dicts. All
26
+ storage, staleness, GeoJSON serialization and CoT classification come from the
27
+ shared skcot type, so the two processes agree on unit serialization by
28
+ construction (a GeoUnit serialized by skcot deserializes identically here).
29
+
30
+ The local adapter store is only ever the *fallback* the endpoint reads when the
31
+ live skcot HTTP bridge (:func:`fetch_skcot_geo`) has no units; it is seeded with
32
+ sovereign fleet placeholders that must not expire, so its inner store is built
33
+ with an infinite TTL (the real freshness clock lives on the live skcot store
34
+ behind the HTTP bridge).
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import logging
41
+ import os
42
+ import threading
43
+ import urllib.parse
44
+ import urllib.request
45
+ from typing import Any, Optional
46
+
47
+ logger = logging.getLogger("skcomms.geo_store")
48
+
49
+ # --------------------------------------------------------------------------
50
+ # Live skcot bridge (source of truth = skcot's process, not this one).
51
+ # --------------------------------------------------------------------------
52
+ #
53
+ # The real situational picture lives in the `skcot` service process (fed from
54
+ # TCP/TLS/mesh CoT + federation). That process exposes it read-only over HTTP
55
+ # (`skcot.geo_http`, default 127.0.0.1:8091). When that endpoint is reachable
56
+ # AND has units, it is the source of truth; otherwise we fall back to this
57
+ # process's local store (the opt-in fleet seed), so the map endpoint always
58
+ # returns a valid shape and never 500s.
59
+
60
+ SKCOT_GEO_URL_ENV = "SKCOT_GEO_URL"
61
+ DEFAULT_SKCOT_GEO_URL = "http://127.0.0.1:8091/geo/units"
62
+ SKCOT_GEO_TIMEOUT_ENV = "SKCOT_GEO_TIMEOUT_S"
63
+ DEFAULT_SKCOT_GEO_TIMEOUT_S = 0.75
64
+
65
+
66
+ def skcot_geo_url() -> Optional[str]:
67
+ """The skcot geo endpoint URL, or None if the bridge is disabled.
68
+
69
+ Defaults to :data:`DEFAULT_SKCOT_GEO_URL`. Set ``SKCOT_GEO_URL`` to an empty
70
+ string to disable the bridge and serve only the local in-process store.
71
+ """
72
+ url = os.environ.get(SKCOT_GEO_URL_ENV, DEFAULT_SKCOT_GEO_URL)
73
+ return url or None
74
+
75
+
76
+ def _skcot_geo_timeout() -> float:
77
+ try:
78
+ return float(os.environ.get(SKCOT_GEO_TIMEOUT_ENV, DEFAULT_SKCOT_GEO_TIMEOUT_S))
79
+ except (TypeError, ValueError):
80
+ return DEFAULT_SKCOT_GEO_TIMEOUT_S
81
+
82
+
83
+ def fetch_skcot_geo(*, include_stale: bool = False, fmt: str = "units") -> Optional[Any]:
84
+ """Fetch the live situational picture from skcot's HTTP endpoint.
85
+
86
+ Blocking (stdlib ``urllib``) with a short timeout; call from an async
87
+ handler via ``asyncio.to_thread`` so the event loop is never blocked.
88
+
89
+ Returns the parsed JSON payload (a ``{"units": [...], "count": N}`` dict for
90
+ ``fmt="units"``, a GeoJSON ``FeatureCollection`` for ``fmt="geojson"``), or
91
+ ``None`` on ANY failure (bridge disabled, connection refused, timeout,
92
+ non-200, unparseable body). Fail-soft: never raises to the caller.
93
+ """
94
+ url = skcot_geo_url()
95
+ if not url:
96
+ return None
97
+ params: dict[str, str] = {}
98
+ if include_stale:
99
+ params["include_stale"] = "1"
100
+ if fmt == "geojson":
101
+ params["format"] = "geojson"
102
+ if params:
103
+ sep = "&" if urllib.parse.urlsplit(url).query else "?"
104
+ url = url + sep + urllib.parse.urlencode(params)
105
+ try:
106
+ req = urllib.request.Request(url, method="GET")
107
+ with urllib.request.urlopen(req, timeout=_skcot_geo_timeout()) as resp:
108
+ if getattr(resp, "status", 200) != 200:
109
+ return None
110
+ body = resp.read()
111
+ return json.loads(body.decode("utf-8"))
112
+ except Exception as exc: # noqa: BLE001 - map endpoint must never 500 on this
113
+ logger.debug("skcot geo fetch failed (%s): %s", url, exc)
114
+ return None
115
+
116
+
117
+ def geo_payload_has_units(payload: Any) -> bool:
118
+ """Whether a skcot geo payload actually carries at least one unit.
119
+
120
+ Handles both response shapes: the flat ``{"units": [...], "count": N}`` and
121
+ the GeoJSON ``{"type": "FeatureCollection", "features": [...]}``.
122
+ """
123
+ if not isinstance(payload, dict):
124
+ return False
125
+ units = payload.get("units")
126
+ if isinstance(units, list):
127
+ return len(units) > 0
128
+ feats = payload.get("features")
129
+ if isinstance(feats, list):
130
+ return len(feats) > 0
131
+ return False
132
+
133
+ # --------------------------------------------------------------------------
134
+ # Thin adapter over the ONE shared skcot GeoStore type.
135
+ # --------------------------------------------------------------------------
136
+
137
+
138
+ class SkcotGeoStore:
139
+ """Thin skcomms-side adapter over the shared :class:`skcot.geo.GeoStore`.
140
+
141
+ This is **not** a reimplementation. It composes a real
142
+ :class:`skcot.geo.GeoStore` (the single shared type -- CoT classification,
143
+ staleness/TTL, GeoJSON serialization, federation envelopes all live there)
144
+ and delegates every attribute to it via :meth:`__getattr__`. The only thing
145
+ it adds is dict-coercion on :meth:`upsert`, so the fleet self-seed and the
146
+ API can feed plain GeoUnit-shaped dicts as well as real
147
+ :class:`skcot.geo.GeoUnit` objects.
148
+
149
+ ``skcot`` is imported lazily (in ``__init__``) rather than at module import
150
+ time: ``skcot`` depends on ``skcomms``, so a top-level import here would be a
151
+ packaging cycle. Because this module is itself imported lazily (only from the
152
+ endpoint / seed paths, after ``skcomms`` has fully loaded), the lazy import
153
+ resolves cleanly.
154
+
155
+ The inner store is built with an infinite TTL by default: the local store is
156
+ only the endpoint's *fallback* (fleet-seed placeholders that must not
157
+ expire); the authoritative freshness clock lives on the live skcot store
158
+ reached over the HTTP bridge.
159
+ """
160
+
161
+ def __init__(self, inner: Any = None, *, ttl_s: float = float("inf")) -> None:
162
+ if inner is None:
163
+ from skcot.geo import GeoStore # type: ignore # lazy: avoid dep cycle
164
+
165
+ inner = GeoStore(ttl_s=ttl_s)
166
+ self._inner = inner
167
+
168
+ def upsert(self, unit: Any) -> Any:
169
+ """Upsert a :class:`skcot.geo.GeoUnit` or a GeoUnit-shaped dict.
170
+
171
+ A dict is coerced into the shared :class:`skcot.geo.GeoUnit` (unknown
172
+ keys dropped) so the whole store speaks one type; then it is stored by
173
+ the shared store verbatim.
174
+ """
175
+ if isinstance(unit, dict):
176
+ from skcot.geo import GeoUnit # type: ignore # lazy: avoid dep cycle
177
+
178
+ fields = set(GeoUnit.model_fields)
179
+ unit = GeoUnit(**{k: v for k, v in unit.items() if k in fields})
180
+ return self._inner.upsert(unit)
181
+
182
+ def __getattr__(self, name: str) -> Any:
183
+ # Delegate everything else (upsert_from_cot, units_json,
184
+ # to_feature_collection, get_all, remove, clear, ...) to the shared
185
+ # skcot store. Only reached for names not defined on the adapter.
186
+ return getattr(self._inner, name)
187
+
188
+
189
+ # --------------------------------------------------------------------------
190
+ # Process-global accessor.
191
+ # --------------------------------------------------------------------------
192
+
193
+ _store_lock = threading.Lock()
194
+ _store: Any = None
195
+
196
+
197
+ def _new_store() -> Any:
198
+ """Construct the process-global local store: the shared skcot GeoStore,
199
+ wrapped in the thin :class:`SkcotGeoStore` adapter."""
200
+ return SkcotGeoStore()
201
+
202
+
203
+ def get_geo_store() -> Any:
204
+ """Return the process-global geo store singleton (creating it on first use).
205
+
206
+ This is the **feed point**: the CoT service upserts into the returned store
207
+ (``upsert_from_cot`` / ``upsert``) and the endpoint reads from it. One store
208
+ per process keeps the daemon's map view and any co-resident agent tool
209
+ reading the same ground truth.
210
+ """
211
+ global _store
212
+ if _store is None:
213
+ with _store_lock:
214
+ if _store is None:
215
+ _store = _new_store()
216
+ return _store
217
+
218
+
219
+ def set_geo_store(store: Any) -> None:
220
+ """Inject a store (used by the CoT service to share its own GeoStore, and
221
+ by tests to seed a known picture)."""
222
+ global _store
223
+ with _store_lock:
224
+ _store = store
225
+
226
+
227
+ def reset_geo_store() -> None:
228
+ """Drop the singleton so the next :func:`get_geo_store` rebuilds it (tests)."""
229
+ global _store
230
+ with _store_lock:
231
+ _store = None
@@ -119,16 +119,36 @@ def _count_queue(pending_dir) -> int:
119
119
  return sum(1 for f in p.glob("*.json") if f.is_file())
120
120
 
121
121
 
122
+ def queue_depth() -> int:
123
+ """The unified PersistentOutbox pending depth: the ONE backlog metric.
124
+
125
+ This is the single canonical outbox-depth probe (coord eb659f61 / roadmap
126
+ CR-5.3). It counts pending entries under the consolidated skcomms
127
+ :class:`~skcomms.outbox.PersistentOutbox` retry store
128
+ (``skcomms.paths.retry_outbox_dir()/pending``, honoring the
129
+ ``SKCOMMS_OUTBOX_DIR`` override). Both this module's ``QueueDrained``
130
+ condition and the skchat ``OutboxBounded`` condition (the skchat operator
131
+ CLI and Atlas's skchat adapter, "one probe, two consumers") read this same
132
+ function, so outbox depth is a single source of truth across the fleet.
133
+
134
+ Fails SAFE (returns 0 = drained) when the store cannot be read, so a probe
135
+ failure never raises a false 'outbox flooded' alarm.
136
+
137
+ Returns:
138
+ int: Number of pending entries in the unified retry store.
139
+ """
140
+ try:
141
+ return _count_queue(_pending_dir())
142
+ except Exception:
143
+ return 0
144
+
145
+
122
146
  def _default_probe() -> dict:
123
147
  """Best-effort skcomms health from real signals. Fails SAFE (healthy) when
124
148
  skcomms is unreachable, so an inability to probe never raises a false alarm."""
125
- try:
126
- depth = _count_queue(_pending_dir())
127
- except Exception:
128
- depth = 0
129
149
  return {
130
150
  "path_healthy": _probe_path_healthy(),
131
- "queue_depth": depth,
151
+ "queue_depth": queue_depth(),
132
152
  "queue_limit": _QUEUE_LIMIT,
133
153
  }
134
154
 
@@ -243,4 +263,5 @@ __all__ = [
243
263
  "explain",
244
264
  "observe",
245
265
  "act",
266
+ "queue_depth",
246
267
  ]