@smilintux/skcapstone 0.1.0 → 0.2.3

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 (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. package/src/skcapstone/cli.py +0 -1441
@@ -0,0 +1,324 @@
1
+ """
2
+ Peer Directory — transport address map for the sovereignty mesh.
3
+
4
+ Maps agent names to their SKComm transport addresses (Syncthing outbox
5
+ paths, WebRTC fingerprints, Tailscale IPs, etc.).
6
+
7
+ Separate from PeerRecord (PGP identity in peers.py) — this module owns
8
+ the *routing* layer, not the trust/cryptography layer.
9
+
10
+ Storage: {skcapstone_home}/peers/directory.yaml
11
+
12
+ Entry format:
13
+ lumina:
14
+ address: /home/user/.skcapstone/sync/comms/outbox/lumina
15
+ transport: syncthing
16
+ fingerprint: ABCD1234...
17
+ last_seen: 2026-03-02T...
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ from datetime import datetime, timezone
25
+ from pathlib import Path
26
+ from typing import Optional
27
+
28
+ import yaml
29
+ from pydantic import BaseModel, Field
30
+
31
+ from . import SHARED_ROOT
32
+
33
+ logger = logging.getLogger("skcapstone.peer_directory")
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Model
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ class DirectoryEntry(BaseModel):
42
+ """A single peer's transport routing entry.
43
+
44
+ Attributes:
45
+ name: Normalized peer name (lowercase).
46
+ address: Transport address — Syncthing outbox path, Tailscale IP,
47
+ WebRTC fingerprint URI, or other transport-specific locator.
48
+ transport: Transport type label (syncthing, webrtc, tailscale, file).
49
+ fingerprint: Optional PGP fingerprint for cross-referencing.
50
+ last_seen: ISO-8601 UTC timestamp of last known activity.
51
+ """
52
+
53
+ name: str
54
+ address: str
55
+ transport: str = "syncthing"
56
+ fingerprint: str = ""
57
+ last_seen: Optional[str] = None
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # PeerDirectory
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ class PeerDirectory:
66
+ """Transport address directory for the sovereign agent mesh.
67
+
68
+ Maps agent names → transport addresses. Separate from the PGP identity
69
+ peer store (peers.py); this module owns routing, not trust.
70
+
71
+ Storage: {home}/peers/directory.yaml
72
+
73
+ Args:
74
+ home: skcapstone home directory. Defaults to SHARED_ROOT (~/.skcapstone).
75
+ """
76
+
77
+ def __init__(self, home: Optional[Path] = None) -> None:
78
+ self._home = home or Path(SHARED_ROOT).expanduser()
79
+ self._path = self._home / "peers" / "directory.yaml"
80
+ self._entries: dict[str, DirectoryEntry] = {}
81
+ self._loaded = False
82
+
83
+ # ------------------------------------------------------------------
84
+ # Public API
85
+ # ------------------------------------------------------------------
86
+
87
+ def load(self) -> dict[str, DirectoryEntry]:
88
+ """Read the directory from disk.
89
+
90
+ Safe to call multiple times — idempotent re-load.
91
+
92
+ Returns:
93
+ Dict mapping normalized name -> DirectoryEntry.
94
+ """
95
+ if not self._path.exists():
96
+ self._entries = {}
97
+ self._loaded = True
98
+ return {}
99
+
100
+ try:
101
+ raw = yaml.safe_load(self._path.read_text(encoding="utf-8")) or {}
102
+ entries: dict[str, DirectoryEntry] = {}
103
+ for name, entry in raw.items():
104
+ if not isinstance(entry, dict):
105
+ continue
106
+ try:
107
+ entries[str(name).lower()] = DirectoryEntry.model_validate(
108
+ {"name": str(name).lower(), **entry}
109
+ )
110
+ except Exception as exc:
111
+ logger.debug("Skipping malformed entry '%s': %s", name, exc)
112
+ self._entries = entries
113
+ except Exception as exc:
114
+ logger.warning("Failed to load peer directory: %s", exc)
115
+ self._entries = {}
116
+
117
+ self._loaded = True
118
+ return dict(self._entries)
119
+
120
+ def resolve(self, name: str) -> Optional[str]:
121
+ """Get the transport address for a named peer.
122
+
123
+ Args:
124
+ name: Peer name (case-insensitive).
125
+
126
+ Returns:
127
+ Transport address string, or None if the peer is not in the
128
+ directory.
129
+ """
130
+ self._ensure_loaded()
131
+ entry = self._entries.get(name.lower())
132
+ return entry.address if entry else None
133
+
134
+ def add_peer(
135
+ self,
136
+ name: str,
137
+ address: str,
138
+ transport: str = "syncthing",
139
+ fingerprint: str = "",
140
+ ) -> DirectoryEntry:
141
+ """Add or update a peer's transport entry.
142
+
143
+ If the peer already exists it is overwritten. Persists atomically.
144
+
145
+ Args:
146
+ name: Peer display name (normalised to lowercase as the key).
147
+ address: Transport address (path, IP, URI, etc.).
148
+ transport: Transport type — syncthing, webrtc, tailscale, file.
149
+ fingerprint: Optional PGP fingerprint for cross-referencing.
150
+
151
+ Returns:
152
+ The created or updated DirectoryEntry.
153
+ """
154
+ self._ensure_loaded()
155
+ entry = DirectoryEntry(
156
+ name=name.lower(),
157
+ address=address,
158
+ transport=transport,
159
+ fingerprint=fingerprint,
160
+ last_seen=datetime.now(timezone.utc).isoformat(),
161
+ )
162
+ self._entries[name.lower()] = entry
163
+ self._save()
164
+ logger.info("Directory: added '%s' → %s (%s)", name, address, transport)
165
+ return entry
166
+
167
+ def remove_peer(self, name: str) -> bool:
168
+ """Remove a peer from the directory.
169
+
170
+ Args:
171
+ name: Peer name to remove (case-insensitive).
172
+
173
+ Returns:
174
+ True if the peer was found and removed, False otherwise.
175
+ """
176
+ self._ensure_loaded()
177
+ key = name.lower()
178
+ if key not in self._entries:
179
+ return False
180
+ del self._entries[key]
181
+ self._save()
182
+ logger.info("Directory: removed '%s'", name)
183
+ return True
184
+
185
+ def list_peers(self) -> list[DirectoryEntry]:
186
+ """Return all known peers, sorted by name.
187
+
188
+ Returns:
189
+ List of DirectoryEntry sorted alphabetically.
190
+ """
191
+ self._ensure_loaded()
192
+ return sorted(self._entries.values(), key=lambda e: e.name)
193
+
194
+ def update_last_seen(self, name: str) -> None:
195
+ """Touch the last_seen timestamp for a peer (in-place + save).
196
+
197
+ Called by the consciousness loop whenever a message arrives from
198
+ a known peer, so the directory stays current without a full re-add.
199
+
200
+ Args:
201
+ name: Peer name (case-insensitive). No-op if peer is unknown.
202
+ """
203
+ self._ensure_loaded()
204
+ key = name.lower()
205
+ if key not in self._entries:
206
+ return
207
+ self._entries[key].last_seen = datetime.now(timezone.utc).isoformat()
208
+ self._save()
209
+
210
+ def auto_discover(
211
+ self,
212
+ heartbeats_dir: Optional[Path] = None,
213
+ ) -> list[DirectoryEntry]:
214
+ """Discover peers from heartbeat files and Syncthing outbox dirs.
215
+
216
+ Scans two sources and adds any *new* peers (existing entries are
217
+ never overwritten):
218
+
219
+ 1. ``{home}/heartbeats/*.json`` — live heartbeat files published by
220
+ each agent via HeartbeatBeacon.
221
+ 2. ``{home}/sync/comms/outbox/`` — one sub-directory per peer that
222
+ Syncthing keeps in sync.
223
+
224
+ Syncthing outbox path is used as the default address because that
225
+ is where SKComm writes messages for the peer.
226
+
227
+ Args:
228
+ heartbeats_dir: Override for the heartbeats directory. Defaults
229
+ to ``{home}/heartbeats``.
230
+
231
+ Returns:
232
+ List of newly-added DirectoryEntry objects (empty if all peers
233
+ were already known).
234
+ """
235
+ self._ensure_loaded()
236
+ added: list[DirectoryEntry] = []
237
+
238
+ hb_dir = heartbeats_dir or (self._home / "heartbeats")
239
+
240
+ # 1. Scan heartbeat files
241
+ if hb_dir.exists():
242
+ for hb_file in sorted(hb_dir.glob("*.json")):
243
+ if hb_file.name.endswith(".tmp"):
244
+ continue
245
+ agent_name = hb_file.stem.lower()
246
+ if agent_name in self._entries:
247
+ # Still update last_seen from heartbeat timestamp
248
+ try:
249
+ data = json.loads(hb_file.read_text(encoding="utf-8"))
250
+ ts = data.get("timestamp", "")
251
+ if ts:
252
+ self._entries[agent_name].last_seen = ts
253
+ except Exception:
254
+ pass
255
+ continue
256
+
257
+ try:
258
+ data = json.loads(hb_file.read_text(encoding="utf-8"))
259
+ # Default Syncthing outbox path for this peer
260
+ outbox = self._home / "sync" / "comms" / "outbox" / agent_name
261
+ entry = DirectoryEntry(
262
+ name=agent_name,
263
+ address=str(outbox),
264
+ transport="syncthing",
265
+ fingerprint=data.get("fingerprint", ""),
266
+ last_seen=data.get("timestamp", datetime.now(timezone.utc).isoformat()),
267
+ )
268
+ self._entries[agent_name] = entry
269
+ added.append(entry)
270
+ logger.info("Auto-discovered '%s' from heartbeat", agent_name)
271
+ except Exception as exc:
272
+ logger.debug("Cannot parse heartbeat %s: %s", hb_file.name, exc)
273
+
274
+ # 2. Scan Syncthing outbox dirs
275
+ outbox_root = self._home / "sync" / "comms" / "outbox"
276
+ if outbox_root.exists():
277
+ for peer_dir in sorted(outbox_root.iterdir()):
278
+ if not peer_dir.is_dir():
279
+ continue
280
+ agent_name = peer_dir.name.lower()
281
+ if agent_name in self._entries:
282
+ continue
283
+ entry = DirectoryEntry(
284
+ name=agent_name,
285
+ address=str(peer_dir),
286
+ transport="syncthing",
287
+ )
288
+ self._entries[agent_name] = entry
289
+ added.append(entry)
290
+ logger.info("Auto-discovered '%s' from outbox", agent_name)
291
+
292
+ if added:
293
+ self._save()
294
+
295
+ return added
296
+
297
+ # ------------------------------------------------------------------
298
+ # Internal helpers
299
+ # ------------------------------------------------------------------
300
+
301
+ def _ensure_loaded(self) -> None:
302
+ """Load from disk on first access."""
303
+ if not self._loaded:
304
+ self.load()
305
+
306
+ def _save(self) -> None:
307
+ """Atomically serialize the directory to YAML."""
308
+ self._path.parent.mkdir(parents=True, exist_ok=True)
309
+
310
+ data: dict[str, dict] = {}
311
+ for name, entry in sorted(self._entries.items()):
312
+ row: dict = {
313
+ "address": entry.address,
314
+ "transport": entry.transport,
315
+ }
316
+ if entry.fingerprint:
317
+ row["fingerprint"] = entry.fingerprint
318
+ if entry.last_seen:
319
+ row["last_seen"] = entry.last_seen
320
+ data[name] = row
321
+
322
+ tmp = self._path.with_suffix(".yaml.tmp")
323
+ tmp.write_text(yaml.dump(data, default_flow_style=False), encoding="utf-8")
324
+ tmp.rename(self._path)
@@ -0,0 +1,329 @@
1
+ """
2
+ Sovereign peer management — the other half of P2P discovery.
3
+
4
+ whoami exports your identity card. This module imports someone
5
+ else's card and registers them as a peer in the SKComm keystore.
6
+ The two together form the complete P2P discovery loop.
7
+
8
+ Flow:
9
+ 1. Agent A runs: skcapstone whoami --export card.json
10
+ 2. Agent A shares card.json with Agent B (USB, chat, email, QR)
11
+ 3. Agent B runs: skcapstone peer add --card card.json
12
+ 4. Agent B can now send encrypted messages to Agent A
13
+
14
+ Peer data is stored at:
15
+ ~/.skcomm/peers/<name>.yml — SKComm peer config
16
+ ~/.skcapstone/peers/<name>.json — Extended peer metadata
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ import yaml
28
+ from pydantic import BaseModel, Field
29
+
30
+ from . import SHARED_ROOT
31
+
32
+ logger = logging.getLogger("skcapstone.peers")
33
+
34
+
35
+ class PeerRecord(BaseModel):
36
+ """A known peer agent.
37
+
38
+ Attributes:
39
+ name: Peer display name.
40
+ fingerprint: CapAuth PGP fingerprint.
41
+ public_key: ASCII-armored PGP public key.
42
+ entity_type: human, ai, or organization.
43
+ handle: CapAuth identity handle.
44
+ email: Contact email.
45
+ capabilities: What this peer can do.
46
+ contact_uris: How to reach this peer.
47
+ trust_level: verified, trusted, sovereign, or unknown.
48
+ added_at: When the peer was added.
49
+ last_seen: Last known activity.
50
+ source: How we learned about this peer (card, discovery, manual).
51
+ """
52
+
53
+ name: str
54
+ fingerprint: str = ""
55
+ public_key: str = ""
56
+ entity_type: str = "unknown"
57
+ handle: str = ""
58
+ email: str = ""
59
+ capabilities: list[str] = Field(default_factory=list)
60
+ contact_uris: list[str] = Field(default_factory=list)
61
+ trust_level: str = "unknown"
62
+ added_at: str = Field(
63
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
64
+ )
65
+ last_seen: Optional[str] = None
66
+ source: str = "manual"
67
+
68
+
69
+ def add_peer_from_card(
70
+ card_path: Path,
71
+ skcapstone_home: Optional[Path] = None,
72
+ skcomm_home: Optional[Path] = None,
73
+ ) -> PeerRecord:
74
+ """Import a peer from a whoami identity card.
75
+
76
+ Reads the card JSON, creates peer records in both skcapstone
77
+ and skcomm directories, and writes the public key for SKComm
78
+ encryption.
79
+
80
+ Args:
81
+ card_path: Path to the exported card.json.
82
+ skcapstone_home: Override skcapstone home. Defaults to ~/.skcapstone/.
83
+ skcomm_home: Override skcomm home. Defaults to ~/.skcomm/.
84
+
85
+ Returns:
86
+ PeerRecord: The registered peer.
87
+
88
+ Raises:
89
+ FileNotFoundError: If the card file doesn't exist.
90
+ ValueError: If the card is missing required fields.
91
+ """
92
+ card_path = Path(card_path)
93
+ if not card_path.exists():
94
+ raise FileNotFoundError(f"Card not found: {card_path}")
95
+
96
+ try:
97
+ card_data = json.loads(card_path.read_text(encoding="utf-8"))
98
+ except json.JSONDecodeError as exc:
99
+ raise ValueError(f"Invalid card JSON: {exc}") from exc
100
+
101
+ name = card_data.get("name", "").strip()
102
+ if not name:
103
+ raise ValueError("Card is missing a 'name' field")
104
+
105
+ peer = PeerRecord(
106
+ name=name,
107
+ fingerprint=card_data.get("fingerprint", ""),
108
+ public_key=card_data.get("public_key", ""),
109
+ entity_type=card_data.get("entity_type", "unknown"),
110
+ handle=card_data.get("handle", ""),
111
+ email=card_data.get("email", ""),
112
+ capabilities=card_data.get("capabilities", []),
113
+ contact_uris=card_data.get("contact_uris", []),
114
+ trust_level="verified",
115
+ source="card",
116
+ )
117
+
118
+ sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
119
+ sc_home = skcomm_home or Path.home() / ".skcomm"
120
+
121
+ _save_skcapstone_peer(sk_home, peer)
122
+ _save_skcomm_peer(sc_home, peer)
123
+
124
+ logger.info("Added peer '%s' (fingerprint: %s)", name, peer.fingerprint[:16])
125
+ return peer
126
+
127
+
128
+ def add_peer_manual(
129
+ name: str,
130
+ fingerprint: str = "",
131
+ public_key_path: Optional[Path] = None,
132
+ email: str = "",
133
+ skcapstone_home: Optional[Path] = None,
134
+ skcomm_home: Optional[Path] = None,
135
+ ) -> PeerRecord:
136
+ """Add a peer manually by name and optional key file.
137
+
138
+ Args:
139
+ name: Peer display name.
140
+ fingerprint: PGP fingerprint (optional).
141
+ public_key_path: Path to a .asc public key file (optional).
142
+ email: Contact email (optional).
143
+ skcapstone_home: Override skcapstone home.
144
+ skcomm_home: Override skcomm home.
145
+
146
+ Returns:
147
+ PeerRecord: The registered peer.
148
+ """
149
+ public_key = ""
150
+ if public_key_path and Path(public_key_path).exists():
151
+ public_key = Path(public_key_path).read_text(encoding="utf-8").strip()
152
+
153
+ peer = PeerRecord(
154
+ name=name,
155
+ fingerprint=fingerprint,
156
+ public_key=public_key,
157
+ email=email,
158
+ source="manual",
159
+ trust_level="verified" if public_key else "unknown",
160
+ )
161
+
162
+ sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
163
+ sc_home = skcomm_home or Path.home() / ".skcomm"
164
+
165
+ _save_skcapstone_peer(sk_home, peer)
166
+ _save_skcomm_peer(sc_home, peer)
167
+
168
+ return peer
169
+
170
+
171
+ def list_peers(
172
+ skcapstone_home: Optional[Path] = None,
173
+ ) -> list[PeerRecord]:
174
+ """List all known peers.
175
+
176
+ Args:
177
+ skcapstone_home: Override skcapstone home.
178
+
179
+ Returns:
180
+ list[PeerRecord]: All registered peers.
181
+ """
182
+ sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
183
+ peers_dir = sk_home / "peers"
184
+ if not peers_dir.exists():
185
+ return []
186
+
187
+ peers = []
188
+ for f in sorted(peers_dir.glob("*.json")):
189
+ try:
190
+ data = json.loads(f.read_text(encoding="utf-8"))
191
+ peers.append(PeerRecord.model_validate(data))
192
+ except (json.JSONDecodeError, Exception):
193
+ continue
194
+
195
+ return peers
196
+
197
+
198
+ def get_peer(
199
+ name: str,
200
+ skcapstone_home: Optional[Path] = None,
201
+ ) -> Optional[PeerRecord]:
202
+ """Get a specific peer by name.
203
+
204
+ Args:
205
+ name: Peer name to look up.
206
+ skcapstone_home: Override skcapstone home.
207
+
208
+ Returns:
209
+ PeerRecord or None if not found.
210
+ """
211
+ sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
212
+ peer_file = sk_home / "peers" / f"{_safe_filename(name)}.json"
213
+ if not peer_file.exists():
214
+ return None
215
+
216
+ try:
217
+ data = json.loads(peer_file.read_text(encoding="utf-8"))
218
+ return PeerRecord.model_validate(data)
219
+ except (json.JSONDecodeError, Exception):
220
+ return None
221
+
222
+
223
+ def remove_peer(
224
+ name: str,
225
+ skcapstone_home: Optional[Path] = None,
226
+ skcomm_home: Optional[Path] = None,
227
+ ) -> bool:
228
+ """Remove a peer from both skcapstone and skcomm registries.
229
+
230
+ Args:
231
+ name: Peer name to remove.
232
+ skcapstone_home: Override skcapstone home.
233
+ skcomm_home: Override skcomm home.
234
+
235
+ Returns:
236
+ bool: True if the peer was found and removed.
237
+ """
238
+ sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
239
+ sc_home = skcomm_home or Path.home() / ".skcomm"
240
+ safe = _safe_filename(name)
241
+ removed = False
242
+
243
+ sk_file = sk_home / "peers" / f"{safe}.json"
244
+ if sk_file.exists():
245
+ sk_file.unlink()
246
+ removed = True
247
+
248
+ sc_file = sc_home / "peers" / f"{safe}.yml"
249
+ if sc_file.exists():
250
+ sc_file.unlink()
251
+ removed = True
252
+
253
+ sc_key = sc_home / "peers" / f"{safe}.pub.asc"
254
+ if sc_key.exists():
255
+ sc_key.unlink()
256
+
257
+ logger.info("Removed peer '%s'", name)
258
+ return removed
259
+
260
+
261
+ def _save_skcapstone_peer(home: Path, peer: PeerRecord) -> Path:
262
+ """Save peer record to skcapstone peers directory.
263
+
264
+ Args:
265
+ home: skcapstone home directory.
266
+ peer: Peer to save.
267
+
268
+ Returns:
269
+ Path: Written file path.
270
+ """
271
+ peers_dir = home / "peers"
272
+ peers_dir.mkdir(parents=True, exist_ok=True)
273
+
274
+ path = peers_dir / f"{_safe_filename(peer.name)}.json"
275
+ path.write_text(peer.model_dump_json(indent=2), encoding="utf-8")
276
+ return path
277
+
278
+
279
+ def _save_skcomm_peer(home: Path, peer: PeerRecord) -> Path:
280
+ """Save peer to SKComm peers directory (YAML + public key).
281
+
282
+ Creates the YAML config that SKComm's KeyStore reads, and
283
+ writes the public key as a separate .asc file.
284
+
285
+ Args:
286
+ home: skcomm home directory.
287
+ peer: Peer to save.
288
+
289
+ Returns:
290
+ Path: Written YAML file path.
291
+ """
292
+ peers_dir = home / "peers"
293
+ peers_dir.mkdir(parents=True, exist_ok=True)
294
+
295
+ safe = _safe_filename(peer.name)
296
+
297
+ if peer.public_key:
298
+ key_path = peers_dir / f"{safe}.pub.asc"
299
+ key_path.write_text(peer.public_key, encoding="utf-8")
300
+ pubkey_ref = str(key_path)
301
+ else:
302
+ pubkey_ref = ""
303
+
304
+ peer_yml = {
305
+ "name": peer.name,
306
+ "fingerprint": peer.fingerprint,
307
+ "public_key": pubkey_ref,
308
+ "email": peer.email,
309
+ "trust_level": peer.trust_level,
310
+ "added_at": peer.added_at,
311
+ }
312
+
313
+ path = peers_dir / f"{safe}.yml"
314
+ path.write_text(yaml.dump(peer_yml, default_flow_style=False), encoding="utf-8")
315
+ return path
316
+
317
+
318
+ def _safe_filename(name: str) -> str:
319
+ """Convert a peer name to a safe filename.
320
+
321
+ Args:
322
+ name: Peer display name.
323
+
324
+ Returns:
325
+ str: Filesystem-safe version of the name.
326
+ """
327
+ safe = name.lower().strip().replace(" ", "-")
328
+ safe = "".join(c for c in safe if c.isalnum() or c in "-_.")
329
+ return safe or "unnamed"