skcomms 0.2.8__tar.gz → 0.2.9__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 (411) hide show
  1. {skcomms-0.2.8/src/skcomms.egg-info → skcomms-0.2.9}/PKG-INFO +1 -1
  2. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pairing_mirror.py +46 -1
  3. {skcomms-0.2.8 → skcomms-0.2.9/src/skcomms.egg-info}/PKG-INFO +1 -1
  4. {skcomms-0.2.8 → skcomms-0.2.9}/tests/conftest.py +11 -0
  5. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pairing_mirror.py +51 -0
  6. {skcomms-0.2.8 → skcomms-0.2.9}/.github/workflows/ci.yml +0 -0
  7. {skcomms-0.2.8 → skcomms-0.2.9}/.github/workflows/publish.yml +0 -0
  8. {skcomms-0.2.8 → skcomms-0.2.9}/.github/workflows/pytest.yml +0 -0
  9. {skcomms-0.2.8 → skcomms-0.2.9}/.github/workflows/qa.yml +0 -0
  10. {skcomms-0.2.8 → skcomms-0.2.9}/.github/workflows/secret-scan.yml +0 -0
  11. {skcomms-0.2.8 → skcomms-0.2.9}/.gitignore +0 -0
  12. {skcomms-0.2.8 → skcomms-0.2.9}/.gitleaks.toml +0 -0
  13. {skcomms-0.2.8 → skcomms-0.2.9}/CHANGELOG.md +0 -0
  14. {skcomms-0.2.8 → skcomms-0.2.9}/CODE_OF_CONDUCT.md +0 -0
  15. {skcomms-0.2.8 → skcomms-0.2.9}/CONTRIBUTING.md +0 -0
  16. {skcomms-0.2.8 → skcomms-0.2.9}/DEPLOY-NOTES.md +0 -0
  17. {skcomms-0.2.8 → skcomms-0.2.9}/Dockerfile +0 -0
  18. {skcomms-0.2.8 → skcomms-0.2.9}/LICENSE +0 -0
  19. {skcomms-0.2.8 → skcomms-0.2.9}/MISSION.md +0 -0
  20. {skcomms-0.2.8 → skcomms-0.2.9}/README.md +0 -0
  21. {skcomms-0.2.8 → skcomms-0.2.9}/SECURITY.md +0 -0
  22. {skcomms-0.2.8 → skcomms-0.2.9}/SOP.md +0 -0
  23. {skcomms-0.2.8 → skcomms-0.2.9}/constraints.txt +0 -0
  24. {skcomms-0.2.8 → skcomms-0.2.9}/contrib/systemd/skcomms-api.service +0 -0
  25. {skcomms-0.2.8 → skcomms-0.2.9}/contrib/systemd/skcomms-housekeep.service +0 -0
  26. {skcomms-0.2.8 → skcomms-0.2.9}/contrib/systemd/skcomms-housekeep.timer +0 -0
  27. {skcomms-0.2.8 → skcomms-0.2.9}/docs/ARCHITECTURE.md +0 -0
  28. {skcomms-0.2.8 → skcomms-0.2.9}/docs/DISCORD_SETUP.md +0 -0
  29. {skcomms-0.2.8 → skcomms-0.2.9}/docs/HANDOFF-skfed-s2s-opus-delivery.md +0 -0
  30. {skcomms-0.2.8 → skcomms-0.2.9}/docs/PAIRING.md +0 -0
  31. {skcomms-0.2.8 → skcomms-0.2.9}/docs/REVOCATION_LATENCY.md +0 -0
  32. {skcomms-0.2.8 → skcomms-0.2.9}/docs/SYNCTHING_TOPOLOGY.md +0 -0
  33. {skcomms-0.2.8 → skcomms-0.2.9}/docs/access-plane-p7.md +0 -0
  34. {skcomms-0.2.8 → skcomms-0.2.9}/docs/access-plane-p8.md +0 -0
  35. {skcomms-0.2.8 → skcomms-0.2.9}/docs/cot-bridge.md +0 -0
  36. {skcomms-0.2.8 → skcomms-0.2.9}/docs/crypto-architecture.md +0 -0
  37. {skcomms-0.2.8 → skcomms-0.2.9}/docs/deploy-plan/skcomms-bulletproof-deploy.md +0 -0
  38. {skcomms-0.2.8 → skcomms-0.2.9}/docs/federation-data-comms-architecture.md +0 -0
  39. {skcomms-0.2.8 → skcomms-0.2.9}/docs/funnel-privacy.md +0 -0
  40. {skcomms-0.2.8 → skcomms-0.2.9}/docs/mode-b-tailnet-deploy.md +0 -0
  41. {skcomms-0.2.8 → skcomms-0.2.9}/docs/node_registry.example.yml +0 -0
  42. {skcomms-0.2.8 → skcomms-0.2.9}/docs/rfcs/RFC-0001-pq-ratchet-metadata-dual-identity.md +0 -0
  43. {skcomms-0.2.8 → skcomms-0.2.9}/docs/skfed-consent-design.md +0 -0
  44. {skcomms-0.2.8 → skcomms-0.2.9}/docs/skfed-federation-guide.md +0 -0
  45. {skcomms-0.2.8 → skcomms-0.2.9}/docs/state-sync-rearch-plan.md +0 -0
  46. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/plans/2026-06-10-qr-pairing.md +0 -0
  47. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/plans/2026-06-13-ble-mesh-p1-core.md +0 -0
  48. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/plans/2026-06-13-lora-l1-core.md +0 -0
  49. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/plans/2026-06-13-skglossa-g1-core.md +0 -0
  50. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/plans/2026-06-13-skglossa-g2-macro-lexicon.md +0 -0
  51. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/plans/2026-06-13-skglossa-g4-emergent.md +0 -0
  52. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/plans/2026-06-13-tier3-adapter-runtime.md +0 -0
  53. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/specs/2026-06-13-ble-mesh-proximity-transport-design.md +0 -0
  54. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/specs/2026-06-13-lora-meshtastic-transport-design.md +0 -0
  55. {skcomms-0.2.8 → skcomms-0.2.9}/docs/superpowers/specs/2026-06-13-skglossa-design.md +0 -0
  56. {skcomms-0.2.8 → skcomms-0.2.9}/docs/tak-vs-skfed-analysis.md +0 -0
  57. {skcomms-0.2.8 → skcomms-0.2.9}/pyproject.toml +0 -0
  58. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/backfill_file_locations.py +0 -0
  59. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/bootstrap.sh +0 -0
  60. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/discord_smoke.py +0 -0
  61. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/drain_orphan_outbox.py +0 -0
  62. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/mode-b-setup.sh +0 -0
  63. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/refresh-constraints.sh +0 -0
  64. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/skfed-dns.sh +0 -0
  65. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/skfed-neutral-addresses.sh +0 -0
  66. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/skfed-sync-rearch.sh +0 -0
  67. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/telegram_smoke.py +0 -0
  68. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/templates/consent-mode.conf.tmpl +0 -0
  69. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/templates/realms.yml.tmpl +0 -0
  70. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/templates/tailscale-serve.sh.tmpl +0 -0
  71. {skcomms-0.2.8 → skcomms-0.2.9}/scripts/test_skfed_sync_rearch.sh +0 -0
  72. {skcomms-0.2.8 → skcomms-0.2.9}/setup.cfg +0 -0
  73. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/__init__.py +0 -0
  74. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/__init__.py +0 -0
  75. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/audit.py +0 -0
  76. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/config.py +0 -0
  77. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/exec.py +0 -0
  78. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/files.py +0 -0
  79. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/grants.py +0 -0
  80. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/knowledge.py +0 -0
  81. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/registry.py +0 -0
  82. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/routing.py +0 -0
  83. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/server.py +0 -0
  84. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/access/wiring.py +0 -0
  85. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/ack.py +0 -0
  86. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/__init__.py +0 -0
  87. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/base.py +0 -0
  88. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/discord.py +0 -0
  89. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/factory.py +0 -0
  90. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/fake.py +0 -0
  91. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/matrix.py +0 -0
  92. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/models.py +0 -0
  93. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/registry.py +0 -0
  94. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/slack.py +0 -0
  95. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/adapters/telegram.py +0 -0
  96. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/anon_queue.py +0 -0
  97. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/anon_transport.py +0 -0
  98. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/api.py +0 -0
  99. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/capabilities.py +0 -0
  100. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/capauth_validator.py +0 -0
  101. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/cli.py +0 -0
  102. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/cluster.py +0 -0
  103. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/compression.py +0 -0
  104. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/config.py +0 -0
  105. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent.py +0 -0
  106. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_banfeeds.py +0 -0
  107. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_captcha.py +0 -0
  108. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_greylist.py +0 -0
  109. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_group_gate.py +0 -0
  110. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_groups.py +0 -0
  111. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_moderation.py +0 -0
  112. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_pipeline.py +0 -0
  113. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_policy.py +0 -0
  114. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_requests.py +0 -0
  115. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_runtime.py +0 -0
  116. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_tiering.py +0 -0
  117. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/consent_tokens.py +0 -0
  118. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/core.py +0 -0
  119. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/crypto.py +0 -0
  120. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/crypto_suites.py +0 -0
  121. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/did_router.py +0 -0
  122. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/discovery.py +0 -0
  123. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/envelope.py +0 -0
  124. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/federation.py +0 -0
  125. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/geo_seed.py +0 -0
  126. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/geo_store.py +0 -0
  127. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/__init__.py +0 -0
  128. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/codebook.py +0 -0
  129. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/codec.py +0 -0
  130. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/emergent.py +0 -0
  131. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/gloss.py +0 -0
  132. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/handshake.py +0 -0
  133. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/macros.py +0 -0
  134. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/message.py +0 -0
  135. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/glossa/session.py +0 -0
  136. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/grants.py +0 -0
  137. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/heartbeat.py +0 -0
  138. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/home.py +0 -0
  139. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/household_router.py +0 -0
  140. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/housekeeping.py +0 -0
  141. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/identity.py +0 -0
  142. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/integration.py +0 -0
  143. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/key_exchange.py +0 -0
  144. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/mailbox.py +0 -0
  145. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/marketplace.py +0 -0
  146. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/mcp_server.py +0 -0
  147. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/metrics.py +0 -0
  148. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/models.py +0 -0
  149. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/node_registry.py +0 -0
  150. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/nostr_discovery.py +0 -0
  151. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/observability.py +0 -0
  152. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/operator_probe.py +0 -0
  153. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/outbox.py +0 -0
  154. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/outbox_migrate.py +0 -0
  155. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/padding.py +0 -0
  156. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pairing.py +0 -0
  157. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/paths.py +0 -0
  158. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/peers.py +0 -0
  159. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pq_provider.py +0 -0
  160. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pqdm.py +0 -0
  161. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pqkem.py +0 -0
  162. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pqkem_backend.py +0 -0
  163. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pqroute.py +0 -0
  164. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pqroute_transport.py +0 -0
  165. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pqsig.py +0 -0
  166. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/profile_router.py +0 -0
  167. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/public_pairing.py +0 -0
  168. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pubsub.py +0 -0
  169. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/pubsub_transport.py +0 -0
  170. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/queue.py +0 -0
  171. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/ratelimit.py +0 -0
  172. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/realm.py +0 -0
  173. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/registry.py +0 -0
  174. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/router.py +0 -0
  175. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/signaling.py +0 -0
  176. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/signing.py +0 -0
  177. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/skfed_announce.py +0 -0
  178. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/skfed_directory.py +0 -0
  179. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/skfed_readdr.py +0 -0
  180. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/skfed_resolve.py +0 -0
  181. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/skworld_manifest.py +0 -0
  182. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/souls_router.py +0 -0
  183. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/ssrf.py +0 -0
  184. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/store_forward.py +0 -0
  185. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/syncthing_rail.py +0 -0
  186. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/tofu.py +0 -0
  187. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transport.py +0 -0
  188. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/__init__.py +0 -0
  189. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/audio_track.py +0 -0
  190. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/ble/__init__.py +0 -0
  191. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/ble/gatt.py +0 -0
  192. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/ble/identity.py +0 -0
  193. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/ble/node.py +0 -0
  194. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/ble/noise.py +0 -0
  195. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/ble/protocol.py +0 -0
  196. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/ble/radio.py +0 -0
  197. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/ble/relay.py +0 -0
  198. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/broker_server.py +0 -0
  199. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/file.py +0 -0
  200. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/http_s2s.py +0 -0
  201. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/lora/__init__.py +0 -0
  202. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/lora/addressing.py +0 -0
  203. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/lora/framing.py +0 -0
  204. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/lora/interface.py +0 -0
  205. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/lora/meshtastic_iface.py +0 -0
  206. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/lora/store.py +0 -0
  207. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/lora/transport.py +0 -0
  208. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/nostr.py +0 -0
  209. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/p2p_connector.py +0 -0
  210. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/p2p_manager.py +0 -0
  211. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/p2p_session.py +0 -0
  212. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/signaling_base.py +0 -0
  213. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/signaling_broker.py +0 -0
  214. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/signaling_mailbox.py +0 -0
  215. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/syncthing.py +0 -0
  216. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/tailscale.py +0 -0
  217. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/video_track.py +0 -0
  218. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/webrtc.py +0 -0
  219. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/webrtc_media.py +0 -0
  220. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/transports/websocket.py +0 -0
  221. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms/trustbackup.py +0 -0
  222. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms.egg-info/SOURCES.txt +0 -0
  223. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms.egg-info/dependency_links.txt +0 -0
  224. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms.egg-info/entry_points.txt +0 -0
  225. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms.egg-info/requires.txt +0 -0
  226. {skcomms-0.2.8 → skcomms-0.2.9}/src/skcomms.egg-info/top_level.txt +0 -0
  227. {skcomms-0.2.8 → skcomms-0.2.9}/tests/__init__.py +0 -0
  228. {skcomms-0.2.8 → skcomms-0.2.9}/tests/fixtures/pqdm2_from_dart.json +0 -0
  229. {skcomms-0.2.8 → skcomms-0.2.9}/tests/gen_pqdm2_fixture.py +0 -0
  230. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_access_exec.py +0 -0
  231. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_access_files.py +0 -0
  232. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_access_nonce_durable.py +0 -0
  233. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_access_rbac.py +0 -0
  234. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_access_routing.py +0 -0
  235. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_access_server.py +0 -0
  236. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_access_wiring.py +0 -0
  237. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ack_bind.py +0 -0
  238. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_adapter_factory.py +0 -0
  239. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_adapter_fake.py +0 -0
  240. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_anon_queue.py +0 -0
  241. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_anon_transport.py +0 -0
  242. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_access_token.py +0 -0
  243. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_consent.py +0 -0
  244. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_consent_token_gate.py +0 -0
  245. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_cors.py +0 -0
  246. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_federation_inbox.py +0 -0
  247. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_geo_seed.py +0 -0
  248. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_geo_skcot_bridge.py +0 -0
  249. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_geo_units.py +0 -0
  250. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_health.py +0 -0
  251. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_send_signed_wire.py +0 -0
  252. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_api_skfed_directory.py +0 -0
  253. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_backfill_file_locations.py +0 -0
  254. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_fragment.py +0 -0
  255. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_gatt.py +0 -0
  256. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_identity.py +0 -0
  257. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_mesh.py +0 -0
  258. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_noise.py +0 -0
  259. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_pairing_bundle.py +0 -0
  260. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_protocol.py +0 -0
  261. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_radio.py +0 -0
  262. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ble_relay.py +0 -0
  263. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_broker_server.py +0 -0
  264. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_capabilities.py +0 -0
  265. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_capauth_failclosed.py +0 -0
  266. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_capauth_key_reconcile.py +0 -0
  267. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_capauth_resolution.py +0 -0
  268. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_capauth_revoked_expired.py +0 -0
  269. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_channel_adapter.py +0 -0
  270. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_cluster_schema.py +0 -0
  271. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_config.py +0 -0
  272. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent.py +0 -0
  273. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_ban_gate_fail_closed.py +0 -0
  274. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_banfeeds.py +0 -0
  275. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_captcha.py +0 -0
  276. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_cli.py +0 -0
  277. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_cli_mgmt.py +0 -0
  278. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_greylist.py +0 -0
  279. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_group_gate.py +0 -0
  280. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_groups.py +0 -0
  281. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_moderation.py +0 -0
  282. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_pipeline.py +0 -0
  283. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_policy.py +0 -0
  284. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_requests.py +0 -0
  285. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_runtime.py +0 -0
  286. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_tiering.py +0 -0
  287. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_consent_tokens.py +0 -0
  288. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_core_inbound_beacon_skip.py +0 -0
  289. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_core_send_federated.py +0 -0
  290. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_core_send_signed.py +0 -0
  291. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_core_stop.py +0 -0
  292. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_crypto_failclosed.py +0 -0
  293. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_crypto_suites_q0.py +0 -0
  294. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_daemon_adapter_lifecycle.py +0 -0
  295. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_deadletter_ops.py +0 -0
  296. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_delivery_semantics.py +0 -0
  297. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_discord_adapter.py +0 -0
  298. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_encrypt_fail_closed.py +0 -0
  299. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_envelope_consent_token.py +0 -0
  300. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_envelope_v1.py +0 -0
  301. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_federation.py +0 -0
  302. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_federation_integration.py +0 -0
  303. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_geo_roundtrip_skcot.py +0 -0
  304. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_codebook.py +0 -0
  305. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_codec_l0.py +0 -0
  306. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_codec_l1.py +0 -0
  307. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_codec_l2.py +0 -0
  308. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_emergent.py +0 -0
  309. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_emergent_negotiator.py +0 -0
  310. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_emergent_protocol.py +0 -0
  311. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_gloss.py +0 -0
  312. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_handshake.py +0 -0
  313. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_handshake_lexicon.py +0 -0
  314. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_macros.py +0 -0
  315. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_macros_render.py +0 -0
  316. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_message.py +0 -0
  317. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_session.py +0 -0
  318. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_session_macros.py +0 -0
  319. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_glossa_to_human.py +0 -0
  320. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_grants.py +0 -0
  321. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_home_scaffold.py +0 -0
  322. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_housekeeping.py +0 -0
  323. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_http_s2s_star_recipient.py +0 -0
  324. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_http_s2s_transport.py +0 -0
  325. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_hybrid_envelope_sig.py +0 -0
  326. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_identity_env_override.py +0 -0
  327. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_identity_key_binding.py +0 -0
  328. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_identity_realm.py +0 -0
  329. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_identity_trust_backup.py +0 -0
  330. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_integration.py +0 -0
  331. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_key_exchange_transport_scoping.py +0 -0
  332. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_knowledge.py +0 -0
  333. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_lora_addressing.py +0 -0
  334. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_lora_framing.py +0 -0
  335. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_lora_interface.py +0 -0
  336. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_lora_meshtastic_iface.py +0 -0
  337. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_lora_store.py +0 -0
  338. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_lora_transport.py +0 -0
  339. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_mailbox.py +0 -0
  340. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_mailbox_encrypt_at_rest.py +0 -0
  341. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_mailbox_recipient_key.py +0 -0
  342. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_matrix_adapter.py +0 -0
  343. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_mode_b_setup.sh +0 -0
  344. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_no_subapp_dependency.py +0 -0
  345. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_node_registry.py +0 -0
  346. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_nonce_durable.py +0 -0
  347. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_nostr_discovery.py +0 -0
  348. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_observability.py +0 -0
  349. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_operator_cli.py +0 -0
  350. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_outbox_home_alignment.py +0 -0
  351. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_outbox_migrate.py +0 -0
  352. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_p2p_connector.py +0 -0
  353. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_p2p_manager.py +0 -0
  354. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_p2p_media.py +0 -0
  355. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_p2p_session.py +0 -0
  356. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_padding.py +0 -0
  357. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pairing.py +0 -0
  358. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pairing_cli.py +0 -0
  359. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_paths_scoping.py +0 -0
  360. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_peer_inbox_traversal.py +0 -0
  361. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_peers.py +0 -0
  362. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqc_cutover.py +0 -0
  363. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqc_shim.py +0 -0
  364. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqdm.py +0 -0
  365. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqdm2.py +0 -0
  366. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqdm2_interop.py +0 -0
  367. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqkem.py +0 -0
  368. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqroute.py +0 -0
  369. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqroute_padding.py +0 -0
  370. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqroute_transport.py +0 -0
  371. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_pqsig.py +0 -0
  372. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_prune_inbox.py +0 -0
  373. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_public_pairing.py +0 -0
  374. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_queue_bounds.py +0 -0
  375. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_receiver_wire_ttl_expiry.py +0 -0
  376. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_registry.py +0 -0
  377. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_registry_cli.py +0 -0
  378. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_failure_log_dedup.py +0 -0
  379. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_perm_backoff.py +0 -0
  380. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_perm_backoff_fixes.py +0 -0
  381. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_quarantine.py +0 -0
  382. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_rail_ordering.py +0 -0
  383. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_route_anon.py +0 -0
  384. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_route_signed.py +0 -0
  385. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_star_broadcast_routing.py +0 -0
  386. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_router_unsigned_excludes_https_s2s.py +0 -0
  387. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_sign_fail_closed.py +0 -0
  388. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_signaling_broker.py +0 -0
  389. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_signaling_mailbox.py +0 -0
  390. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_signaling_select.py +0 -0
  391. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skfed_announce.py +0 -0
  392. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skfed_cli.py +0 -0
  393. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skfed_directory.py +0 -0
  394. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skfed_dns.sh +0 -0
  395. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skfed_operator_pin.py +0 -0
  396. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skfed_readdr.py +0 -0
  397. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skfed_resolve.py +0 -0
  398. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skfed_ssrf.py +0 -0
  399. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_skworld_manifest.py +0 -0
  400. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_slack_adapter.py +0 -0
  401. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_smoke.py +0 -0
  402. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_ssrf_guard.py +0 -0
  403. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_store_forward.py +0 -0
  404. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_syncthing_rail.py +0 -0
  405. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_syncthing_recipient_guard.py +0 -0
  406. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_telegram_adapter.py +0 -0
  407. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_tofu.py +0 -0
  408. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_typed_message_content.py +0 -0
  409. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_webrtc_bridge_lifecycle.py +0 -0
  410. {skcomms-0.2.8 → skcomms-0.2.9}/tests/test_webrtc_ice_logging.py +0 -0
  411. {skcomms-0.2.8 → skcomms-0.2.9}/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.8
3
+ Version: 0.2.9
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
@@ -55,15 +55,36 @@ def _base() -> Optional[str]:
55
55
  def mirror_pairing(fqid: str, pubkey: str) -> None:
56
56
  """Record an accepted peer as a TOFU enrollment in capauth.pairing.
57
57
 
58
+ Idempotent per (subject, key): re-mirroring an already-enrolled key is a
59
+ no-op. Without that guard every accept minted ANOTHER approved DeviceRecord,
60
+ and because each accept presents a freshly generated key, TOFU pinned
61
+ nothing: the subject simply accumulated approved devices. Observed on
62
+ ``opus@chef.skworld``, 22 records in one afternoon, every one a distinct
63
+ fingerprint, in pairs because both :func:`skcomms.pairing.accept_pairing`
64
+ and the public-pairing path mirror the same accept.
65
+
66
+ Enrollments are NOT deduped by content upstream (``enroll_device`` +
67
+ ``approve`` always create), so the check belongs here at the call site.
68
+
58
69
  Best-effort: gated on the kernel flag, and any capauth error is logged at
59
70
  debug and swallowed so it can never break :func:`accept_pairing`.
60
71
  """
61
72
  if not kernel_enabled() or not fqid or not pubkey:
62
73
  return
63
74
  try:
64
- from capauth.pairing import approve, enroll_device
75
+ from capauth.pairing import approve, enroll_device, list_devices
65
76
 
66
77
  base = _base()
78
+
79
+ # Trust on FIRST use: if this subject already has a live device for this
80
+ # key, keep the existing record rather than minting a second one. A
81
+ # revoked record is deliberately not resurrected here; re-enrolling a
82
+ # revoked key must be an explicit operator action, not a side effect of
83
+ # a peer re-accept.
84
+ if _already_enrolled(fqid, pubkey, base):
85
+ logger.debug("pairing mirror: %s already enrolled for this key, skipping", fqid)
86
+ return
87
+
67
88
  enr = enroll_device(
68
89
  pubkey,
69
90
  list(_DEFAULT_SCOPES),
@@ -76,6 +97,30 @@ def mirror_pairing(fqid: str, pubkey: str) -> None:
76
97
  logger.debug("capauth pairing mirror failed", exc_info=True)
77
98
 
78
99
 
100
+ def _already_enrolled(fqid: str, pubkey: str, base) -> bool:
101
+ """Whether *fqid* already has a live capauth device carrying *pubkey*.
102
+
103
+ Compares the armored public key stored on the DeviceRecord. A record with
104
+ no stored key never matches, so it enrolls rather than silently treating an
105
+ unknown key as already trusted. Any lookup failure also returns False: a
106
+ duplicate record is far better than dropping a real pairing.
107
+ """
108
+ from capauth.pairing import list_devices
109
+
110
+ try:
111
+ existing = list_devices(fqid, base_dir=base, include_revoked=False)
112
+ except Exception:
113
+ logger.debug("pairing mirror: device lookup failed, enrolling anyway", exc_info=True)
114
+ return False
115
+
116
+ presented = (pubkey or "").strip()
117
+ for dev in existing:
118
+ stored = (getattr(dev, "pubkey", None) or "").strip()
119
+ if stored and stored == presented:
120
+ return True
121
+ return False
122
+
123
+
79
124
  def mirror_revocation(fqid: str) -> None:
80
125
  """Revoke every non-revoked capauth device whose subject is this peer's fqid.
81
126
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skcomms
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -25,8 +25,19 @@ def _isolate_skcomms_home(tmp_path, monkeypatch):
25
25
  isolation, tests that build servers with defaults would write replay
26
26
  state into the operator's live home. Tests that need a specific home
27
27
  still win: their own ``monkeypatch.setenv`` runs after this fixture.
28
+
29
+ The pairing kernel base is isolated for the same reason, and it is the
30
+ sharper case: ``mirror_pairing`` enrolls + approves into the capauth
31
+ pairing store, which is the store ``capauth.authz.decide()`` reads to
32
+ authorize. Four test files exercise pairing without setting the base
33
+ themselves (test_pairing, test_pairing_cli, test_public_pairing,
34
+ test_key_exchange_transport_scoping), so a plain ``pytest tests/`` was
35
+ minting real approved DeviceRecords into the operator's live security
36
+ store. Measured: running test_public_pairing.py alone added 2 records,
37
+ and 58 accumulated records for ``opus@chef.skworld`` trace to this.
28
38
  """
29
39
  monkeypatch.setenv("SKCOMMS_HOME", str(tmp_path / "skcomms-home"))
40
+ monkeypatch.setenv("SKCOMMS_PAIRING_KERNEL_BASE", str(tmp_path / "capauth-pairing"))
30
41
  # Nonce caches are node-local (outside the synced home) by default; keep
31
42
  # tests off the operator's real ~/.local/state/skcomms as well.
32
43
  monkeypatch.setenv("SKCOMMS_NONCE_CACHE_DIR", str(tmp_path / "skcomms-local-state"))
@@ -308,3 +308,54 @@ def test_public_pairing_kernel_disabled_writes_nothing(tmp_path, monkeypatch):
308
308
  )
309
309
  assert res["paired"]["fqid"] == "opus@chef.skworld"
310
310
  assert list_devices(subject="opus@chef.skworld", base_dir=str(tmp_path / "capauth")) == []
311
+
312
+
313
+ # ── idempotency (trust on FIRST use, not on every use) ───────────────────
314
+
315
+
316
+ # Deliberately NOT shaped like real base64 key material. mirror_pairing only
317
+ # needs two distinct opaque strings (it stores them and fingerprints them), and
318
+ # a realistic-looking prefix made the secret scanner flag these as a leaked
319
+ # key. Suppressing that with an allowlist entry would blunt a gate that only
320
+ # started working on 2026-08-14 (see the secret-scan workflow), so the fixture
321
+ # avoids tripping it in the first place.
322
+ _KEY_A = "not-a-real-key-fixture-alpha"
323
+ _KEY_B = "not-a-real-key-fixture-bravo"
324
+
325
+
326
+ def test_re_mirroring_the_same_key_does_not_mint_a_second_device(tmp_path, monkeypatch):
327
+ """Repeated accepts of one peer+key must converge on ONE device record.
328
+
329
+ Regression pin for the observed drift: opus@chef.skworld accumulated 22
330
+ approved TOFU records in a single afternoon because every accept called
331
+ enroll_device + approve unconditionally.
332
+ """
333
+ monkeypatch.setenv("SKCOMMS_PAIRING_KERNEL_BASE", str(tmp_path))
334
+ from capauth.pairing.kernel import list_devices
335
+
336
+ from skcomms.pairing_mirror import mirror_pairing
337
+
338
+ for _ in range(5):
339
+ mirror_pairing("peer@example.skworld", _KEY_A)
340
+
341
+ devices = list_devices("peer@example.skworld", base_dir=tmp_path, include_revoked=True)
342
+ assert len(devices) == 1
343
+
344
+
345
+ def test_a_genuinely_new_key_still_enrolls(tmp_path, monkeypatch):
346
+ """The guard must not block a real second device for the same subject."""
347
+ monkeypatch.setenv("SKCOMMS_PAIRING_KERNEL_BASE", str(tmp_path))
348
+ from capauth.pairing.kernel import list_devices
349
+
350
+ from skcomms.pairing_mirror import mirror_pairing
351
+
352
+ mirror_pairing("peer@example.skworld", _KEY_A)
353
+ mirror_pairing("peer@example.skworld", _KEY_B)
354
+
355
+ devices = list_devices("peer@example.skworld", base_dir=tmp_path, include_revoked=True)
356
+ assert len(devices) == 2
357
+
358
+ # And re-presenting the first key still does not add a third.
359
+ mirror_pairing("peer@example.skworld", _KEY_A)
360
+ devices = list_devices("peer@example.skworld", base_dir=tmp_path, include_revoked=True)
361
+ assert len(devices) == 2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes