skcomms 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. {skcomms-0.2.1 → skcomms-0.2.2}/PKG-INFO +1 -1
  2. {skcomms-0.2.1 → skcomms-0.2.2}/pyproject.toml +1 -1
  3. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/server.py +56 -5
  4. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/api.py +153 -37
  5. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/capauth_validator.py +107 -15
  6. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/cli.py +299 -25
  7. skcomms-0.2.2/src/skcomms/cluster.py +219 -0
  8. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/core.py +56 -13
  9. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/home.py +100 -0
  10. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/integration.py +41 -12
  11. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/key_exchange.py +19 -10
  12. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/mailbox.py +12 -1
  13. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/nostr_discovery.py +165 -5
  14. skcomms-0.2.2/src/skcomms/operator_probe.py +246 -0
  15. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/outbox.py +21 -4
  16. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pairing.py +4 -0
  17. skcomms-0.2.2/src/skcomms/pairing_mirror.py +103 -0
  18. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/paths.py +31 -0
  19. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/peers.py +35 -0
  20. skcomms-0.2.2/src/skcomms/public_pairing.py +286 -0
  21. skcomms-0.2.2/src/skcomms/skworld_manifest.py +82 -0
  22. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/store_forward.py +3 -2
  23. skcomms-0.2.2/src/skcomms/syncthing_rail.py +920 -0
  24. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/file.py +23 -0
  25. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/webrtc.py +17 -1
  26. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/trustbackup.py +5 -3
  27. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms.egg-info/PKG-INFO +1 -1
  28. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms.egg-info/SOURCES.txt +20 -0
  29. skcomms-0.2.2/tests/test_access_nonce_durable.py +216 -0
  30. skcomms-0.2.2/tests/test_capauth_resolution.py +58 -0
  31. skcomms-0.2.2/tests/test_capauth_revoked_expired.py +180 -0
  32. skcomms-0.2.2/tests/test_cluster_schema.py +165 -0
  33. skcomms-0.2.2/tests/test_core_stop.py +47 -0
  34. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_housekeeping.py +166 -0
  35. skcomms-0.2.2/tests/test_key_exchange_transport_scoping.py +132 -0
  36. skcomms-0.2.2/tests/test_mailbox_recipient_key.py +40 -0
  37. skcomms-0.2.2/tests/test_no_subapp_dependency.py +114 -0
  38. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_nonce_durable.py +104 -22
  39. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_nostr_discovery.py +188 -0
  40. skcomms-0.2.2/tests/test_operator_cli.py +193 -0
  41. skcomms-0.2.2/tests/test_outbox_home_alignment.py +104 -0
  42. skcomms-0.2.2/tests/test_pairing_mirror.py +285 -0
  43. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_paths_scoping.py +38 -0
  44. skcomms-0.2.2/tests/test_public_pairing.py +245 -0
  45. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_queue_bounds.py +41 -0
  46. skcomms-0.2.2/tests/test_skworld_manifest.py +114 -0
  47. skcomms-0.2.2/tests/test_syncthing_rail.py +415 -0
  48. skcomms-0.2.2/tests/test_webrtc_bridge_lifecycle.py +404 -0
  49. skcomms-0.2.1/src/skcomms/cluster.py +0 -67
  50. {skcomms-0.2.1 → skcomms-0.2.2}/LICENSE +0 -0
  51. {skcomms-0.2.1 → skcomms-0.2.2}/README.md +0 -0
  52. {skcomms-0.2.1 → skcomms-0.2.2}/setup.cfg +0 -0
  53. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/__init__.py +0 -0
  54. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/__init__.py +0 -0
  55. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/audit.py +0 -0
  56. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/config.py +0 -0
  57. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/exec.py +0 -0
  58. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/files.py +0 -0
  59. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/grants.py +0 -0
  60. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/knowledge.py +0 -0
  61. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/registry.py +0 -0
  62. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/routing.py +0 -0
  63. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/access/wiring.py +0 -0
  64. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/ack.py +0 -0
  65. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/__init__.py +0 -0
  66. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/base.py +0 -0
  67. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/discord.py +0 -0
  68. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/factory.py +0 -0
  69. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/fake.py +0 -0
  70. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/matrix.py +0 -0
  71. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/models.py +0 -0
  72. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/registry.py +0 -0
  73. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/slack.py +0 -0
  74. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/adapters/telegram.py +0 -0
  75. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/anon_queue.py +0 -0
  76. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/anon_transport.py +0 -0
  77. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/capabilities.py +0 -0
  78. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/compression.py +0 -0
  79. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/config.py +0 -0
  80. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent.py +0 -0
  81. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_banfeeds.py +0 -0
  82. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_captcha.py +0 -0
  83. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_greylist.py +0 -0
  84. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_group_gate.py +0 -0
  85. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_groups.py +0 -0
  86. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_moderation.py +0 -0
  87. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_pipeline.py +0 -0
  88. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_policy.py +0 -0
  89. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_requests.py +0 -0
  90. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_runtime.py +0 -0
  91. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_tiering.py +0 -0
  92. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/consent_tokens.py +0 -0
  93. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/crypto.py +0 -0
  94. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/crypto_suites.py +0 -0
  95. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/did_router.py +0 -0
  96. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/discovery.py +0 -0
  97. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/envelope.py +0 -0
  98. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/federation.py +0 -0
  99. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/__init__.py +0 -0
  100. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/codebook.py +0 -0
  101. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/codec.py +0 -0
  102. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/emergent.py +0 -0
  103. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/gloss.py +0 -0
  104. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/handshake.py +0 -0
  105. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/macros.py +0 -0
  106. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/message.py +0 -0
  107. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/glossa/session.py +0 -0
  108. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/grants.py +0 -0
  109. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/heartbeat.py +0 -0
  110. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/household_router.py +0 -0
  111. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/housekeeping.py +0 -0
  112. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/identity.py +0 -0
  113. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/marketplace.py +0 -0
  114. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/mcp_server.py +0 -0
  115. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/metrics.py +0 -0
  116. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/models.py +0 -0
  117. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/node_registry.py +0 -0
  118. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/observability.py +0 -0
  119. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/outbox_migrate.py +0 -0
  120. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/padding.py +0 -0
  121. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pq_provider.py +0 -0
  122. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pqdm.py +0 -0
  123. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pqkem.py +0 -0
  124. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pqkem_backend.py +0 -0
  125. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pqroute.py +0 -0
  126. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pqroute_transport.py +0 -0
  127. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pqsig.py +0 -0
  128. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/profile_router.py +0 -0
  129. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pubsub.py +0 -0
  130. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/pubsub_transport.py +0 -0
  131. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/queue.py +0 -0
  132. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/ratelimit.py +0 -0
  133. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/realm.py +0 -0
  134. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/registry.py +0 -0
  135. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/router.py +0 -0
  136. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/signaling.py +0 -0
  137. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/signing.py +0 -0
  138. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/skfed_announce.py +0 -0
  139. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/skfed_directory.py +0 -0
  140. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/skfed_readdr.py +0 -0
  141. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/skfed_resolve.py +0 -0
  142. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/souls_router.py +0 -0
  143. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/ssrf.py +0 -0
  144. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/tofu.py +0 -0
  145. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transport.py +0 -0
  146. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/__init__.py +0 -0
  147. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/audio_track.py +0 -0
  148. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/ble/__init__.py +0 -0
  149. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/ble/gatt.py +0 -0
  150. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/ble/identity.py +0 -0
  151. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/ble/node.py +0 -0
  152. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/ble/noise.py +0 -0
  153. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/ble/protocol.py +0 -0
  154. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/ble/radio.py +0 -0
  155. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/ble/relay.py +0 -0
  156. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/broker_server.py +0 -0
  157. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/http_s2s.py +0 -0
  158. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/lora/__init__.py +0 -0
  159. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/lora/addressing.py +0 -0
  160. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/lora/framing.py +0 -0
  161. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/lora/interface.py +0 -0
  162. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/lora/meshtastic_iface.py +0 -0
  163. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/lora/store.py +0 -0
  164. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/lora/transport.py +0 -0
  165. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/nostr.py +0 -0
  166. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/p2p_connector.py +0 -0
  167. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/p2p_manager.py +0 -0
  168. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/p2p_session.py +0 -0
  169. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/signaling_base.py +0 -0
  170. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/signaling_broker.py +0 -0
  171. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/signaling_mailbox.py +0 -0
  172. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/syncthing.py +0 -0
  173. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/tailscale.py +0 -0
  174. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/video_track.py +0 -0
  175. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/webrtc_media.py +0 -0
  176. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms/transports/websocket.py +0 -0
  177. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms.egg-info/dependency_links.txt +0 -0
  178. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms.egg-info/entry_points.txt +0 -0
  179. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms.egg-info/requires.txt +0 -0
  180. {skcomms-0.2.1 → skcomms-0.2.2}/src/skcomms.egg-info/top_level.txt +0 -0
  181. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_access_exec.py +0 -0
  182. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_access_files.py +0 -0
  183. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_access_rbac.py +0 -0
  184. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_access_routing.py +0 -0
  185. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_access_server.py +0 -0
  186. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_access_wiring.py +0 -0
  187. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ack_bind.py +0 -0
  188. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_adapter_factory.py +0 -0
  189. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_adapter_fake.py +0 -0
  190. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_anon_queue.py +0 -0
  191. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_anon_transport.py +0 -0
  192. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_api_access_token.py +0 -0
  193. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_api_consent.py +0 -0
  194. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_api_consent_token_gate.py +0 -0
  195. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_api_cors.py +0 -0
  196. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_api_federation_inbox.py +0 -0
  197. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_api_health.py +0 -0
  198. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_api_send_signed_wire.py +0 -0
  199. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_api_skfed_directory.py +0 -0
  200. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_backfill_file_locations.py +0 -0
  201. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_fragment.py +0 -0
  202. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_gatt.py +0 -0
  203. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_identity.py +0 -0
  204. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_mesh.py +0 -0
  205. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_noise.py +0 -0
  206. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_pairing_bundle.py +0 -0
  207. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_protocol.py +0 -0
  208. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_radio.py +0 -0
  209. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ble_relay.py +0 -0
  210. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_broker_server.py +0 -0
  211. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_capabilities.py +0 -0
  212. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_capauth_failclosed.py +0 -0
  213. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_capauth_key_reconcile.py +0 -0
  214. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_channel_adapter.py +0 -0
  215. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_config.py +0 -0
  216. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent.py +0 -0
  217. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_ban_gate_fail_closed.py +0 -0
  218. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_banfeeds.py +0 -0
  219. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_captcha.py +0 -0
  220. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_cli.py +0 -0
  221. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_cli_mgmt.py +0 -0
  222. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_greylist.py +0 -0
  223. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_group_gate.py +0 -0
  224. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_groups.py +0 -0
  225. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_moderation.py +0 -0
  226. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_pipeline.py +0 -0
  227. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_policy.py +0 -0
  228. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_requests.py +0 -0
  229. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_runtime.py +0 -0
  230. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_tiering.py +0 -0
  231. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_consent_tokens.py +0 -0
  232. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_core_inbound_beacon_skip.py +0 -0
  233. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_core_send_federated.py +0 -0
  234. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_core_send_signed.py +0 -0
  235. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_crypto_failclosed.py +0 -0
  236. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_crypto_suites_q0.py +0 -0
  237. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_daemon_adapter_lifecycle.py +0 -0
  238. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_deadletter_ops.py +0 -0
  239. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_delivery_semantics.py +0 -0
  240. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_discord_adapter.py +0 -0
  241. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_encrypt_fail_closed.py +0 -0
  242. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_envelope_consent_token.py +0 -0
  243. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_envelope_v1.py +0 -0
  244. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_federation.py +0 -0
  245. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_federation_integration.py +0 -0
  246. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_codebook.py +0 -0
  247. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_codec_l0.py +0 -0
  248. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_codec_l1.py +0 -0
  249. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_codec_l2.py +0 -0
  250. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_emergent.py +0 -0
  251. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_emergent_negotiator.py +0 -0
  252. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_emergent_protocol.py +0 -0
  253. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_gloss.py +0 -0
  254. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_handshake.py +0 -0
  255. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_handshake_lexicon.py +0 -0
  256. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_macros.py +0 -0
  257. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_macros_render.py +0 -0
  258. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_message.py +0 -0
  259. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_session.py +0 -0
  260. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_session_macros.py +0 -0
  261. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_glossa_to_human.py +0 -0
  262. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_grants.py +0 -0
  263. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_home_scaffold.py +0 -0
  264. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_http_s2s_star_recipient.py +0 -0
  265. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_http_s2s_transport.py +0 -0
  266. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_hybrid_envelope_sig.py +0 -0
  267. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_identity_env_override.py +0 -0
  268. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_identity_key_binding.py +0 -0
  269. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_identity_realm.py +0 -0
  270. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_identity_trust_backup.py +0 -0
  271. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_integration.py +0 -0
  272. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_knowledge.py +0 -0
  273. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_lora_addressing.py +0 -0
  274. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_lora_framing.py +0 -0
  275. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_lora_interface.py +0 -0
  276. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_lora_meshtastic_iface.py +0 -0
  277. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_lora_store.py +0 -0
  278. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_lora_transport.py +0 -0
  279. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_mailbox.py +0 -0
  280. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_mailbox_encrypt_at_rest.py +0 -0
  281. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_matrix_adapter.py +0 -0
  282. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_node_registry.py +0 -0
  283. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_observability.py +0 -0
  284. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_outbox_migrate.py +0 -0
  285. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_p2p_connector.py +0 -0
  286. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_p2p_manager.py +0 -0
  287. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_p2p_media.py +0 -0
  288. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_p2p_session.py +0 -0
  289. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_padding.py +0 -0
  290. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pairing.py +0 -0
  291. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pairing_cli.py +0 -0
  292. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_peer_inbox_traversal.py +0 -0
  293. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_peers.py +0 -0
  294. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pqc_cutover.py +0 -0
  295. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pqc_shim.py +0 -0
  296. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pqdm.py +0 -0
  297. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pqkem.py +0 -0
  298. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pqroute.py +0 -0
  299. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pqroute_padding.py +0 -0
  300. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pqroute_transport.py +0 -0
  301. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_pqsig.py +0 -0
  302. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_prune_inbox.py +0 -0
  303. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_receiver_wire_ttl_expiry.py +0 -0
  304. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_registry.py +0 -0
  305. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_registry_cli.py +0 -0
  306. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_failure_log_dedup.py +0 -0
  307. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_perm_backoff.py +0 -0
  308. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_perm_backoff_fixes.py +0 -0
  309. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_quarantine.py +0 -0
  310. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_rail_ordering.py +0 -0
  311. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_route_anon.py +0 -0
  312. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_route_signed.py +0 -0
  313. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_star_broadcast_routing.py +0 -0
  314. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_router_unsigned_excludes_https_s2s.py +0 -0
  315. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_sign_fail_closed.py +0 -0
  316. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_signaling_broker.py +0 -0
  317. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_signaling_mailbox.py +0 -0
  318. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_signaling_select.py +0 -0
  319. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_skfed_announce.py +0 -0
  320. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_skfed_cli.py +0 -0
  321. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_skfed_directory.py +0 -0
  322. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_skfed_operator_pin.py +0 -0
  323. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_skfed_readdr.py +0 -0
  324. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_skfed_resolve.py +0 -0
  325. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_skfed_ssrf.py +0 -0
  326. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_slack_adapter.py +0 -0
  327. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_smoke.py +0 -0
  328. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_ssrf_guard.py +0 -0
  329. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_store_forward.py +0 -0
  330. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_syncthing_recipient_guard.py +0 -0
  331. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_telegram_adapter.py +0 -0
  332. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_tofu.py +0 -0
  333. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_typed_message_content.py +0 -0
  334. {skcomms-0.2.1 → skcomms-0.2.2}/tests/test_webrtc_ice_logging.py +0 -0
  335. {skcomms-0.2.1 → skcomms-0.2.2}/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.2
4
4
  Summary: Sovereign communications for AI agents — multi-channel transport + FQID realm addressing
5
5
  Author-email: smilinTux <chefboyrdave2.1@gmail.com>
6
6
  License: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skcomms"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Sovereign communications for AI agents — multi-channel transport + FQID realm addressing"
9
9
  readme = "README.md"
10
10
  license = {text = "GPL-3.0-or-later"}
@@ -27,9 +27,11 @@ from __future__ import annotations
27
27
 
28
28
  import json
29
29
  import logging
30
+ import os
30
31
  import socket
31
32
  from dataclasses import dataclass
32
- from typing import Any, Optional
33
+ from pathlib import Path
34
+ from typing import Any, Optional, Union
33
35
 
34
36
  from .. import federation as fed
35
37
  from ..discovery import PeerStore
@@ -42,6 +44,49 @@ from .registry import AccessRegistry, DEFAULT_REGISTRY, RegisteredTool, Scope
42
44
  logger = logging.getLogger("skcomms.access.server")
43
45
 
44
46
 
47
+ def _access_nonce_db_path() -> Path:
48
+ """Resolve the sk-access durable replay-cache path (NODE-LOCAL, never synced).
49
+
50
+ Precedence: ``SKCOMMS_ACCESS_NONCE_DB`` (explicit file path) wins, then
51
+ the ``SKCOMMS_NONCE_CACHE_DIR`` directory override, then the XDG state
52
+ default (``$XDG_STATE_HOME/skcomms/`` or ``~/.local/state/skcomms/``):
53
+ see :func:`skcomms.home.resolve_nonce_db`. It is a SEPARATE file from
54
+ the federation inbox cache (``nonce_cache.db``) so the two surfaces keep
55
+ independent replay history.
56
+
57
+ The store used to live at ``skcomms_home()/state/access_nonce_cache.db``,
58
+ inside the Syncthing-shared tree; a replay cache must never sync between
59
+ nodes, so it now defaults outside any synced dir. A healthy legacy DB is
60
+ migrated once; otherwise a fresh cache starts (safe: replay exposure is
61
+ bounded by the envelope freshness window).
62
+ """
63
+ override = (os.environ.get("SKCOMMS_ACCESS_NONCE_DB") or "").strip()
64
+ if override:
65
+ return Path(override).expanduser()
66
+ from ..home import resolve_nonce_db
67
+
68
+ return resolve_nonce_db("access_nonce_cache.db")
69
+
70
+
71
+ def _default_nonce_cache() -> Union["fed.NonceCache", "fed.DurableNonceCache"]:
72
+ """Build the sk-access replay guard: durable by default (coord f465b407).
73
+
74
+ Mirrors :func:`skcomms.api._get_nonce_cache` semantics for the sk-access
75
+ surface: a SQLite-backed :class:`~skcomms.federation.DurableNonceCache`
76
+ so a daemon restart does not reopen a replay window on signed tool calls.
77
+ ``SKCOMMS_NONCE_CACHE=memory`` opts back into the legacy in-memory cache
78
+ (explicit, single-process/ephemeral use only).
79
+
80
+ Fail-closed: if the durable store cannot be opened this raises, so the
81
+ server refuses to start rather than silently accepting replays with no
82
+ persistence (systemd Restart=always keeps retrying).
83
+ """
84
+ mode = (os.environ.get("SKCOMMS_NONCE_CACHE") or "").strip().lower()
85
+ if mode == "memory":
86
+ return fed.NonceCache()
87
+ return fed.DurableNonceCache(_access_nonce_db_path())
88
+
89
+
45
90
  # ---------------------------------------------------------------------------
46
91
  # Errors
47
92
  # ---------------------------------------------------------------------------
@@ -104,6 +149,9 @@ class AccessServer:
104
149
  verifier: An :class:`~skcomms.signing.EnvelopeVerifier`. If omitted, one
105
150
  is created and seeded TOFU-style from the :class:`PeerStore`.
106
151
  peer_store: Peer store for TOFU key resolution (default: a fresh one).
152
+ nonce_cache: Replay guard override (tests / embedding). Default is the
153
+ durable per-node store from :func:`_default_nonce_cache`, so a
154
+ daemon restart does not reopen a replay window (coord f465b407).
107
155
  """
108
156
 
109
157
  def __init__(
@@ -113,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 (
@@ -642,6 +642,22 @@ async def health():
642
642
  return body
643
643
 
644
644
 
645
+ @app.get("/.well-known/skworld-module.json", tags=["health"])
646
+ async def skworld_module_manifest(request: Request):
647
+ """skcomms' SKWorld module manifest (public discovery metadata, no bearer).
648
+
649
+ skcomms is a UI-less backend transport service, so this manifest declares no
650
+ UI module: only the operator facet plus a service marker. The shell reads it
651
+ to discover skcomms and learn its health + operator contract before it has a
652
+ token. URLs are origin-relative to the request, so health resolves against
653
+ wherever the api actually answers. Served unauthenticated, like skchat's and
654
+ skcode's manifests.
655
+ """
656
+ from .skworld_manifest import skcomms_module_manifest
657
+
658
+ return skcomms_module_manifest(str(request.base_url))
659
+
660
+
645
661
  # ---------------------------------------------------------------------------
646
662
  # MCP tool relay — used by the consciousness-swipe browser extension.
647
663
  #
@@ -986,30 +1002,31 @@ _MAX_INBOX_BYTES = 256 * 1024
986
1002
 
987
1003
 
988
1004
  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.
1005
+ """Resolve the durable nonce-cache SQLite path (NODE-LOCAL, never synced).
1006
+
1007
+ Precedence: ``SKCOMMS_NONCE_DB`` (explicit file path) wins, then the
1008
+ ``SKCOMMS_NONCE_CACHE_DIR`` directory override, then the XDG state
1009
+ default (``$XDG_STATE_HOME/skcomms/`` or ``~/.local/state/skcomms/``):
1010
+ see :func:`skcomms.home.resolve_nonce_db`.
1011
+
1012
+ The store used to live at ``skcomms_home()/state/nonce_cache.db``; on
1013
+ live fleets that home is inside a Syncthing-shared tree, and when the
1014
+ Syncthing folder is rooted ABOVE the home the home's own ``.stignore``
1015
+ ``state/`` line has no effect, so two nodes synced one live WAL SQLite
1016
+ (conflict copies observed on .158/.41). A replay guard is strictly
1017
+ per-node state and now defaults outside any synced tree. A healthy
1018
+ legacy DB is migrated once; otherwise a fresh cache starts, which is
1019
+ safe because the replay window is bounded by the envelope freshness
1020
+ check (see SOP.md).
1002
1021
  """
1003
1022
  import os as _os
1004
1023
 
1005
1024
  override = (_os.environ.get("SKCOMMS_NONCE_DB") or "").strip()
1006
1025
  if override:
1007
1026
  return Path(override).expanduser()
1008
- from .home import ensure_state_ignored, skcomms_home
1027
+ from .home import resolve_nonce_db
1009
1028
 
1010
- home = skcomms_home()
1011
- ensure_state_ignored(home)
1012
- return home / "state" / "nonce_cache.db"
1029
+ return resolve_nonce_db("nonce_cache.db")
1013
1030
 
1014
1031
 
1015
1032
  def _get_nonce_cache():
@@ -1351,6 +1368,80 @@ async def post_inbox(request: Request):
1351
1368
  return {"ok": True, "id": env.id}
1352
1369
 
1353
1370
 
1371
+ # ---------------------------------------------------------------------------
1372
+ # Public P2P pairing over Tailscale Funnel (card 2ab5aa6c): PUBLIC, opt-in
1373
+ # ---------------------------------------------------------------------------
1374
+ #
1375
+ # Off-tailnet peers cannot reach the tailnet-only mesh, so this route lets a
1376
+ # peer POST their peer bundle to the node's Funnel-exposed pairing endpoint and
1377
+ # bootstrap a P2P pairing. It is the inbound half of ``skcomms.public_pairing``:
1378
+ # the request is routed straight into the EXISTING key exchange
1379
+ # (``import_peer_bundle``), which does all the crypto (armor validation +
1380
+ # fingerprint derivation). The public Funnel is only the bootstrap channel.
1381
+ #
1382
+ # Disabled by default: with no ``SKCOMMS_FUNNEL_URL`` configured the route 404s,
1383
+ # exactly as if it did not exist. When a ``SKCOMMS_FUNNEL_PAIR_TOKEN`` is set,
1384
+ # an inbound request must present a matching token or it is rejected 401.
1385
+
1386
+ _MAX_PAIR_BYTES = 256 * 1024
1387
+
1388
+
1389
+ @app.post("/api/v1/pair/public", tags=["pairing"])
1390
+ async def post_public_pairing(request: Request):
1391
+ """Accept an off-tailnet peer's public pairing request over the Funnel.
1392
+
1393
+ Reads a ``{"bundle": {...}, "token": "..."}`` body, routes it into the
1394
+ existing key exchange, and returns this node's own bundle for mutual
1395
+ bootstrap. 404 when public pairing is not configured (feature off).
1396
+
1397
+ Raises:
1398
+ HTTPException: 404 (feature off), 401 (bad/missing token), 422
1399
+ (missing/malformed bundle), 413 (too large).
1400
+ """
1401
+ from .key_exchange import KeyExchangeError
1402
+ from .public_pairing import (
1403
+ PublicPairingError,
1404
+ PublicPairingRequest,
1405
+ configured_token,
1406
+ funnel_enabled,
1407
+ handle_public_pairing_request,
1408
+ )
1409
+
1410
+ # Disabled by default: no Funnel base configured -> the route does not exist.
1411
+ if not funnel_enabled():
1412
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="not found")
1413
+
1414
+ raw = await request.body()
1415
+ if len(raw) > _MAX_PAIR_BYTES:
1416
+ raise HTTPException(
1417
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
1418
+ detail=f"pairing request exceeds {_MAX_PAIR_BYTES} bytes",
1419
+ )
1420
+
1421
+ import json as _json
1422
+
1423
+ try:
1424
+ body = _json.loads(raw or b"{}")
1425
+ req = PublicPairingRequest(**body)
1426
+ except Exception as exc:
1427
+ raise HTTPException(
1428
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1429
+ detail=f"unparseable pairing request: {exc}",
1430
+ ) from exc
1431
+
1432
+ try:
1433
+ result = handle_public_pairing_request(req, expected_token=configured_token())
1434
+ except PublicPairingError as exc:
1435
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
1436
+ except KeyExchangeError as exc:
1437
+ raise HTTPException(
1438
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)
1439
+ ) from exc
1440
+
1441
+ logger.info("public pairing accepted from %s", (result.get("paired") or {}).get("fqid"))
1442
+ return {"ok": True, **result}
1443
+
1444
+
1354
1445
  # ---------------------------------------------------------------------------
1355
1446
  # Operator consent surface (skfed-consent-design gate 5) — LOCAL ONLY
1356
1447
  # ---------------------------------------------------------------------------
@@ -2515,25 +2606,50 @@ async def get_webrtc_peers(room: Optional[str] = None):
2515
2606
  # Consciousness / Soul Snapshot endpoints
2516
2607
  # ---------------------------------------------------------------------------
2517
2608
 
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
- )
2609
+ # skcapstone.snapshots is an OPTIONAL higher-layer subapp surface. skcomms is
2610
+ # the L0 core and must import without it, so the import is resolved LAZILY on
2611
+ # first use (see ``_load_snapshots``) rather than at module load. These names
2612
+ # stay ``None`` until then; ``from __future__ import annotations`` (top of file)
2613
+ # keeps the string annotations below valid while they are unbound.
2614
+ SnapshotStore = None # type: ignore[assignment]
2615
+ SnapshotIndex = None # type: ignore[assignment]
2616
+ SoulSnapshot = None # type: ignore[assignment]
2617
+ _OOFState = None # type: ignore[assignment]
2618
+ _PersonalityTraits = None # type: ignore[assignment]
2619
+ _ConversationMessage = None # type: ignore[assignment]
2620
+
2621
+ #: Tri-state: ``None`` = not yet probed, ``True``/``False`` = resolved.
2622
+ _SNAPSHOTS_AVAILABLE: Optional[bool] = None
2623
+
2624
+
2625
+ def _load_snapshots() -> bool:
2626
+ """Resolve ``skcapstone.snapshots`` lazily on first use, caching the result.
2627
+
2628
+ The consciousness/snapshot endpoints legitimately bridge to skcapstone, but
2629
+ deferring the import to first use means merely importing ``skcomms.api``
2630
+ never pulls skcapstone into ``sys.modules``. Returns whether the snapshot
2631
+ types are available; callers 501 when they are not.
2632
+ """
2633
+ global _SNAPSHOTS_AVAILABLE
2634
+ global SnapshotStore, SnapshotIndex, SoulSnapshot
2635
+ global _OOFState, _PersonalityTraits, _ConversationMessage
2636
+ if _SNAPSHOTS_AVAILABLE is None:
2637
+ try:
2638
+ # ``global`` above routes each of these bindings to the module name.
2639
+ from skcapstone.snapshots import ConversationMessage as _ConversationMessage
2640
+ from skcapstone.snapshots import OOFState as _OOFState
2641
+ from skcapstone.snapshots import PersonalityTraits as _PersonalityTraits
2642
+ from skcapstone.snapshots import (
2643
+ SnapshotIndex,
2644
+ SnapshotStore,
2645
+ SoulSnapshot,
2646
+ )
2647
+
2648
+ _SNAPSHOTS_AVAILABLE = True
2649
+ except Exception: # ImportError, or a broken partial install
2650
+ _SNAPSHOTS_AVAILABLE = False
2651
+ return _SNAPSHOTS_AVAILABLE
2533
2652
 
2534
- _SNAPSHOTS_AVAILABLE = True
2535
- except ImportError:
2536
- _SNAPSHOTS_AVAILABLE = False
2537
2653
 
2538
2654
  _snapshot_store: Optional[SnapshotStore] = None
2539
2655
 
@@ -2548,7 +2664,7 @@ def _get_store() -> "SnapshotStore":
2548
2664
  HTTPException: If skcapstone is not installed.
2549
2665
  """
2550
2666
  global _snapshot_store
2551
- if not _SNAPSHOTS_AVAILABLE:
2667
+ if not _load_snapshots():
2552
2668
  raise HTTPException(
2553
2669
  status_code=status.HTTP_501_NOT_IMPLEMENTED,
2554
2670
  detail="skcapstone package not installed — cannot manage snapshots",
@@ -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