@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,611 @@
1
+ """
2
+ Sovereign Heartbeat v2 — active health beacon for agent meshes.
3
+
4
+ Each agent node publishes a heartbeat file containing its current
5
+ state, capacity, capabilities, and TTL. Syncthing distributes these
6
+ files across the mesh. Other agents read heartbeats to understand
7
+ the network's health and available resources.
8
+
9
+ Conflict-free: each node writes only its own file. Stale heartbeats
10
+ (past TTL) are marked as offline.
11
+
12
+ Architecture:
13
+ ~/.skcapstone/heartbeats/
14
+ ├── opus.json # This node's heartbeat
15
+ ├── lumina.json # Peer heartbeat (via Syncthing)
16
+ ├── grok.json # Peer heartbeat
17
+ └── ...
18
+
19
+ Usage:
20
+ beacon = HeartbeatBeacon(home, agent_name="opus")
21
+ beacon.pulse() # Publish heartbeat
22
+ peers = beacon.discover_peers() # Find live peers
23
+ health = beacon.mesh_health() # Network overview
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import logging
30
+ import os
31
+ import platform
32
+ import shutil
33
+ from datetime import datetime, timedelta, timezone
34
+ from pathlib import Path
35
+ from typing import Any, Optional
36
+
37
+ from pydantic import BaseModel, Field, field_validator
38
+
39
+ logger = logging.getLogger("skcapstone.heartbeat")
40
+
41
+ DEFAULT_TTL_SECONDS = 300 # 5 minutes
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Models
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ class HeartbeatService(BaseModel):
50
+ """A backend service advertised in the heartbeat."""
51
+
52
+ name: str # "skvector", "skgraph"
53
+ port: int # 6333, 6379
54
+ protocol: str = "http" # http, redis
55
+
56
+
57
+ class AgentCapability(BaseModel):
58
+ """A single agent capability."""
59
+
60
+ name: str
61
+ version: str = "1.0"
62
+ enabled: bool = True
63
+
64
+
65
+ class NodeCapacity(BaseModel):
66
+ """Resource capacity of a node."""
67
+
68
+ cpu_count: int = 0
69
+ memory_total_mb: int = 0
70
+ memory_available_mb: int = 0
71
+ disk_free_gb: float = 0.0
72
+ gpu_available: bool = False
73
+ gpu_name: str = ""
74
+
75
+
76
+ class Heartbeat(BaseModel):
77
+ """A single agent heartbeat beacon."""
78
+
79
+ agent_name: str
80
+ status: str = "alive" # alive, busy, draining, offline
81
+
82
+ @field_validator("agent_name")
83
+ @classmethod
84
+ def _validate_agent_name(cls, v: str) -> str:
85
+ if not v or not v.strip():
86
+ raise ValueError("agent_name must be a non-empty string")
87
+ return v.strip()
88
+ hostname: str = ""
89
+ platform: str = ""
90
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
91
+ ttl_seconds: int = DEFAULT_TTL_SECONDS
92
+ uptime_hours: float = 0.0
93
+
94
+ # Agent state
95
+ soul_active: str = ""
96
+ claimed_tasks: list[str] = Field(default_factory=list)
97
+ loaded_model: str = ""
98
+ session_active: bool = False
99
+ consciousness_active: bool = False
100
+
101
+ # Live metrics (new in v2.1)
102
+ uptime_seconds: float = 0.0
103
+ cpu_load_1min: float = 0.0
104
+ memory_used_mb: int = 0
105
+ active_conversations: int = 0
106
+ messages_processed_24h: int = 0
107
+
108
+ # Resources
109
+ capacity: NodeCapacity = Field(default_factory=NodeCapacity)
110
+
111
+ # Capabilities
112
+ capabilities: list[AgentCapability] = Field(default_factory=list)
113
+
114
+ # Metadata
115
+ version: str = ""
116
+ fingerprint: str = ""
117
+ metadata: dict[str, Any] = Field(default_factory=dict)
118
+
119
+ # Service advertisement (optional — old heartbeats without these still parse)
120
+ services: list[HeartbeatService] = Field(default_factory=list)
121
+ tailscale_ip: str = ""
122
+
123
+ @property
124
+ def is_alive(self) -> bool:
125
+ """Whether this heartbeat is still valid (within TTL)."""
126
+ expires = self.timestamp + timedelta(seconds=self.ttl_seconds)
127
+ return datetime.now(timezone.utc) <= expires
128
+
129
+ @property
130
+ def age_seconds(self) -> float:
131
+ """Seconds since this heartbeat was published."""
132
+ delta = datetime.now(timezone.utc) - self.timestamp
133
+ return delta.total_seconds()
134
+
135
+
136
+ class PeerInfo(BaseModel):
137
+ """Summary of a discovered peer."""
138
+
139
+ agent_name: str
140
+ status: str
141
+ alive: bool
142
+ age_seconds: float
143
+ hostname: str = ""
144
+ capabilities: list[str] = Field(default_factory=list)
145
+ soul_active: str = ""
146
+ claimed_tasks: int = 0
147
+ services: list[str] = Field(default_factory=list)
148
+ tailscale_ip: str = ""
149
+
150
+
151
+ class MeshHealth(BaseModel):
152
+ """Health summary of the agent mesh."""
153
+
154
+ total_peers: int = 0
155
+ alive_peers: int = 0
156
+ offline_peers: int = 0
157
+ busy_peers: int = 0
158
+ total_capabilities: list[str] = Field(default_factory=list)
159
+ peers: list[PeerInfo] = Field(default_factory=list)
160
+ collected_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # HeartbeatBeacon
165
+ # ---------------------------------------------------------------------------
166
+
167
+
168
+ class HeartbeatBeacon:
169
+ """Active health beacon for sovereign agent meshes.
170
+
171
+ Publishes heartbeats, discovers peers, and reports mesh health.
172
+
173
+ Args:
174
+ home: Agent home directory (~/.skcapstone).
175
+ agent_name: Name of the local agent.
176
+ ttl_seconds: Heartbeat TTL before considered stale.
177
+ """
178
+
179
+ def __init__(
180
+ self,
181
+ home: Path,
182
+ agent_name: str = "anonymous",
183
+ ttl_seconds: int = DEFAULT_TTL_SECONDS,
184
+ heartbeats_dir: Optional[Path] = None,
185
+ ) -> None:
186
+ if not agent_name or not agent_name.strip():
187
+ raise ValueError(
188
+ "agent_name must be a non-empty string, got %r" % agent_name
189
+ )
190
+ self._home = home
191
+ self._agent = agent_name.strip()
192
+ self._ttl = ttl_seconds
193
+ self._heartbeat_dir = Path(heartbeats_dir) if heartbeats_dir else home / "heartbeats"
194
+ self._start_time = datetime.now(timezone.utc)
195
+
196
+ def initialize(self) -> None:
197
+ """Create the heartbeat directory."""
198
+ self._heartbeat_dir.mkdir(parents=True, exist_ok=True)
199
+
200
+ def pulse(
201
+ self,
202
+ status: str = "alive",
203
+ claimed_tasks: Optional[list[str]] = None,
204
+ loaded_model: str = "",
205
+ capabilities: Optional[list[AgentCapability]] = None,
206
+ metadata: Optional[dict[str, Any]] = None,
207
+ services: Optional[list[HeartbeatService]] = None,
208
+ tailscale_ip: Optional[str] = None,
209
+ consciousness_active: bool = False,
210
+ active_conversations: int = 0,
211
+ messages_processed_24h: int = 0,
212
+ ) -> Heartbeat:
213
+ """Publish a heartbeat beacon.
214
+
215
+ Writes the agent's current state to its heartbeat file.
216
+ Only writes to its own file — never touches peer files.
217
+
218
+ Args:
219
+ status: Agent status (alive, busy, draining, offline).
220
+ claimed_tasks: Currently claimed task IDs.
221
+ loaded_model: Currently loaded AI model.
222
+ capabilities: Agent capabilities list.
223
+ metadata: Additional metadata.
224
+ services: Backend services to advertise. Auto-detected if None.
225
+ tailscale_ip: Tailscale IP address. Auto-detected if None.
226
+
227
+ Returns:
228
+ The published Heartbeat.
229
+ """
230
+ self.initialize()
231
+
232
+ uptime_secs = (datetime.now(timezone.utc) - self._start_time).total_seconds()
233
+ uptime_hours = uptime_secs / 3600
234
+ cpu_load, mem_used_mb = self._detect_cpu_and_mem()
235
+
236
+ heartbeat = Heartbeat(
237
+ agent_name=self._agent,
238
+ status=status,
239
+ hostname=platform.node(),
240
+ platform=f"{platform.system()} {platform.machine()}",
241
+ ttl_seconds=self._ttl,
242
+ uptime_hours=round(uptime_hours, 2),
243
+ claimed_tasks=claimed_tasks or [],
244
+ loaded_model=loaded_model,
245
+ session_active=True,
246
+ consciousness_active=consciousness_active,
247
+ uptime_seconds=round(uptime_secs, 1),
248
+ cpu_load_1min=cpu_load,
249
+ memory_used_mb=mem_used_mb,
250
+ active_conversations=active_conversations,
251
+ messages_processed_24h=messages_processed_24h,
252
+ capacity=self._detect_capacity(),
253
+ capabilities=capabilities or self._detect_capabilities(),
254
+ version=self._detect_version(),
255
+ fingerprint=self._detect_fingerprint(),
256
+ soul_active=self._detect_soul(),
257
+ metadata=metadata or {},
258
+ services=services if services is not None else self._detect_services(),
259
+ tailscale_ip=tailscale_ip if tailscale_ip is not None else self._detect_tailscale_ip(),
260
+ )
261
+
262
+ path = self._heartbeat_dir / f"{self._agent.lower()}.json"
263
+ tmp_path = path.with_suffix(".json.tmp")
264
+ tmp_path.write_text(
265
+ heartbeat.model_dump_json(indent=2),
266
+ encoding="utf-8",
267
+ )
268
+ tmp_path.rename(path)
269
+
270
+ logger.debug("Heartbeat pulse: %s status=%s", self._agent, status)
271
+ return heartbeat
272
+
273
+ def read_heartbeat(self, agent_name: str) -> Optional[Heartbeat]:
274
+ """Read a specific agent's heartbeat.
275
+
276
+ Args:
277
+ agent_name: The agent to read.
278
+
279
+ Returns:
280
+ Heartbeat or None if not found.
281
+ """
282
+ path = self._heartbeat_dir / f"{agent_name}.json"
283
+ if not path.exists():
284
+ return None
285
+ try:
286
+ return Heartbeat.model_validate_json(
287
+ path.read_text(encoding="utf-8")
288
+ )
289
+ except Exception as exc:
290
+ logger.warning("Cannot read heartbeat for %s: %s", agent_name, exc)
291
+ return None
292
+
293
+ def discover_peers(self, include_self: bool = False) -> list[PeerInfo]:
294
+ """Discover all peers from heartbeat files.
295
+
296
+ Args:
297
+ include_self: Whether to include own heartbeat.
298
+
299
+ Returns:
300
+ List of PeerInfo for all discovered agents.
301
+ """
302
+ self.initialize()
303
+ peers: list[PeerInfo] = []
304
+
305
+ for f in sorted(self._heartbeat_dir.glob("*.json")):
306
+ if f.name.endswith(".tmp"):
307
+ continue
308
+
309
+ agent_name = f.stem
310
+ if not include_self and agent_name == self._agent:
311
+ continue
312
+
313
+ try:
314
+ hb = Heartbeat.model_validate_json(
315
+ f.read_text(encoding="utf-8")
316
+ )
317
+ peers.append(PeerInfo(
318
+ agent_name=hb.agent_name,
319
+ status=hb.status if hb.is_alive else "offline",
320
+ alive=hb.is_alive,
321
+ age_seconds=round(hb.age_seconds, 1),
322
+ hostname=hb.hostname,
323
+ capabilities=[c.name for c in hb.capabilities if c.enabled],
324
+ soul_active=hb.soul_active,
325
+ claimed_tasks=len(hb.claimed_tasks),
326
+ services=[s.name for s in hb.services],
327
+ tailscale_ip=hb.tailscale_ip,
328
+ ))
329
+ except Exception as exc:
330
+ logger.warning("Cannot parse heartbeat %s: %s", f.name, exc)
331
+
332
+ return peers
333
+
334
+ def mesh_health(self) -> MeshHealth:
335
+ """Get overall mesh health summary.
336
+
337
+ Returns:
338
+ MeshHealth with peer counts and capability overview.
339
+ """
340
+ peers = self.discover_peers(include_self=True)
341
+ alive = [p for p in peers if p.alive]
342
+ offline = [p for p in peers if not p.alive]
343
+ busy = [p for p in peers if p.status == "busy"]
344
+
345
+ all_caps: set[str] = set()
346
+ for p in alive:
347
+ all_caps.update(p.capabilities)
348
+
349
+ return MeshHealth(
350
+ total_peers=len(peers),
351
+ alive_peers=len(alive),
352
+ offline_peers=len(offline),
353
+ busy_peers=len(busy),
354
+ total_capabilities=sorted(all_caps),
355
+ peers=peers,
356
+ )
357
+
358
+ def find_capable(self, capability: str) -> list[PeerInfo]:
359
+ """Find alive peers with a specific capability.
360
+
361
+ Args:
362
+ capability: The capability name to search for.
363
+
364
+ Returns:
365
+ List of alive peers with the capability.
366
+ """
367
+ peers = self.discover_peers(include_self=True)
368
+ return [
369
+ p for p in peers
370
+ if p.alive and capability in p.capabilities
371
+ ]
372
+
373
+ def mark_offline(self) -> None:
374
+ """Mark this agent as going offline.
375
+
376
+ Publishes a final heartbeat with status "offline" and
377
+ TTL of 0 so peers know immediately.
378
+ """
379
+ self.pulse(status="offline")
380
+
381
+ # -------------------------------------------------------------------
382
+ # Internal helpers
383
+ # -------------------------------------------------------------------
384
+
385
+ def _detect_cpu_and_mem(self) -> tuple[float, int]:
386
+ """Return (cpu_load_1min, memory_used_mb) using psutil or /proc fallback.
387
+
388
+ Returns:
389
+ Tuple of (1-minute CPU load average, memory used in MB).
390
+ """
391
+ cpu_load = 0.0
392
+ mem_used_mb = 0
393
+
394
+ try:
395
+ import psutil
396
+ loads = psutil.getloadavg()
397
+ cpu_load = round(loads[0], 2)
398
+ mem = psutil.virtual_memory()
399
+ mem_used_mb = (mem.total - mem.available) // (1024 * 1024)
400
+ except ImportError:
401
+ # Fallback: /proc on Linux
402
+ try:
403
+ loadavg = Path("/proc/loadavg")
404
+ if loadavg.exists():
405
+ parts = loadavg.read_text().split()
406
+ cpu_load = round(float(parts[0]), 2)
407
+ except Exception as exc:
408
+ logger.debug("CPU load fallback failed: %s", exc)
409
+ try:
410
+ meminfo = Path("/proc/meminfo")
411
+ if meminfo.exists():
412
+ info: dict[str, int] = {}
413
+ for line in meminfo.read_text().splitlines():
414
+ parts = line.split()
415
+ if len(parts) >= 2:
416
+ info[parts[0].rstrip(":")] = int(parts[1])
417
+ total_kb = info.get("MemTotal", 0)
418
+ avail_kb = info.get("MemAvailable", 0)
419
+ mem_used_mb = (total_kb - avail_kb) // 1024
420
+ except Exception as exc:
421
+ logger.debug("Memory fallback failed: %s", exc)
422
+ except Exception as exc:
423
+ logger.debug("CPU/mem detection failed: %s", exc)
424
+
425
+ return cpu_load, mem_used_mb
426
+
427
+ def _detect_capacity(self) -> NodeCapacity:
428
+ """Detect current node resource capacity."""
429
+ try:
430
+ cpu_count = os.cpu_count() or 0
431
+ disk = shutil.disk_usage(self._home)
432
+ disk_free_gb = round(disk.free / (1024 ** 3), 1)
433
+
434
+ mem_total = 0
435
+ mem_avail = 0
436
+ try:
437
+ import psutil
438
+ mem = psutil.virtual_memory()
439
+ mem_total = mem.total // (1024 * 1024)
440
+ mem_avail = mem.available // (1024 * 1024)
441
+ except ImportError:
442
+ # Fallback: read from /proc/meminfo on Linux
443
+ meminfo = Path("/proc/meminfo")
444
+ if meminfo.exists():
445
+ for line in meminfo.read_text().splitlines():
446
+ if line.startswith("MemTotal:"):
447
+ mem_total = int(line.split()[1]) // 1024
448
+ elif line.startswith("MemAvailable:"):
449
+ mem_avail = int(line.split()[1]) // 1024
450
+
451
+ gpu_available = False
452
+ gpu_name = ""
453
+ if shutil.which("nvidia-smi"):
454
+ gpu_available = True
455
+ gpu_name = "nvidia"
456
+
457
+ return NodeCapacity(
458
+ cpu_count=cpu_count,
459
+ memory_total_mb=mem_total,
460
+ memory_available_mb=mem_avail,
461
+ disk_free_gb=disk_free_gb,
462
+ gpu_available=gpu_available,
463
+ gpu_name=gpu_name,
464
+ )
465
+ except Exception as exc:
466
+ logger.warning("Failed to detect node capacity: %s", exc)
467
+ return NodeCapacity()
468
+
469
+ def _load_config_capabilities(self) -> list[str]:
470
+ """Load capability names from the agent config file.
471
+
472
+ Reads ``{home}/config/config.yaml`` and returns the ``capabilities``
473
+ list. Falls back to the ``AgentConfig`` defaults if the file is
474
+ absent or unparseable.
475
+
476
+ Returns:
477
+ List of capability name strings from config.
478
+ """
479
+ config_path = self._home / "config" / "config.yaml"
480
+ try:
481
+ if config_path.exists():
482
+ import yaml as _yaml
483
+ data = _yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
484
+ caps = data.get("capabilities")
485
+ if isinstance(caps, list):
486
+ return [str(c) for c in caps if c]
487
+ except Exception as exc:
488
+ logger.debug("Cannot load capabilities from config: %s", exc)
489
+ # Fall back to AgentConfig defaults
490
+ from .models import AgentConfig
491
+ return AgentConfig().capabilities
492
+
493
+ def _detect_capabilities(self) -> list[AgentCapability]:
494
+ """Build capabilities list from config, merging with detected packages.
495
+
496
+ Config capabilities (from ``{home}/config/config.yaml``) are always
497
+ included. Additionally, known optional packages are probed via
498
+ ``import``; any that are present but not already listed by config are
499
+ appended.
500
+ """
501
+ config_caps = self._load_config_capabilities()
502
+ cap_names: list[str] = list(config_caps)
503
+
504
+ # Probe optional packages and add if not already declared in config
505
+ package_checks = [
506
+ ("skcapstone", "skcapstone"),
507
+ ("skmemory", "skmemory"),
508
+ ("skchat", "skchat"),
509
+ ("skcomm", "skcomm"),
510
+ ("capauth", "capauth"),
511
+ ("cloud9", "cloud9"),
512
+ ]
513
+ for name, module in package_checks:
514
+ if name not in cap_names:
515
+ try:
516
+ __import__(module)
517
+ cap_names.append(name)
518
+ except ImportError:
519
+ pass
520
+
521
+ return [AgentCapability(name=n) for n in cap_names]
522
+
523
+ def _detect_version(self) -> str:
524
+ """Detect skcapstone version."""
525
+ try:
526
+ from . import __version__
527
+ return __version__
528
+ except Exception as exc:
529
+ logger.debug("Version detection failed: %s", exc)
530
+ return "unknown"
531
+
532
+ def _detect_fingerprint(self) -> str:
533
+ """Detect agent identity fingerprint."""
534
+ identity_path = self._home / "identity" / "identity.json"
535
+ if identity_path.exists():
536
+ try:
537
+ data = json.loads(identity_path.read_text(encoding="utf-8"))
538
+ return data.get("fingerprint", "")[:16]
539
+ except (json.JSONDecodeError, OSError) as exc:
540
+ logger.debug("Cannot read identity for fingerprint: %s", exc)
541
+ return ""
542
+
543
+ def _detect_soul(self) -> str:
544
+ """Detect active soul overlay."""
545
+ active_path = self._home / "soul" / "active.json"
546
+ if active_path.exists():
547
+ try:
548
+ data = json.loads(active_path.read_text(encoding="utf-8"))
549
+ return data.get("active_soul", "")
550
+ except (json.JSONDecodeError, OSError) as exc:
551
+ logger.debug("Cannot read soul overlay: %s", exc)
552
+ return ""
553
+
554
+ def _detect_services(self) -> list[HeartbeatService]:
555
+ """Auto-detect locally running backend services.
556
+
557
+ Checks if well-known ports are listening on localhost.
558
+
559
+ Returns:
560
+ List of detected HeartbeatService entries.
561
+ """
562
+ import socket as _socket
563
+
564
+ services: list[HeartbeatService] = []
565
+ checks = [
566
+ ("skvector", 6333, "http"),
567
+ ("skgraph", 6379, "redis"),
568
+ ]
569
+
570
+ for name, port, protocol in checks:
571
+ try:
572
+ s = _socket.create_connection(("127.0.0.1", port), timeout=1)
573
+ s.close()
574
+ services.append(HeartbeatService(
575
+ name=name, port=port, protocol=protocol,
576
+ ))
577
+ except (OSError, _socket.timeout):
578
+ pass
579
+
580
+ return services
581
+
582
+ def _detect_tailscale_ip(self) -> str:
583
+ """Best-effort Tailscale IP detection.
584
+
585
+ Runs ``tailscale status --json`` and extracts the self IP.
586
+ Fails silently if Tailscale is not installed or not running.
587
+
588
+ Returns:
589
+ Tailscale IPv4 address or empty string.
590
+ """
591
+ import subprocess
592
+
593
+ try:
594
+ result = subprocess.run(
595
+ ["tailscale", "status", "--json"],
596
+ capture_output=True,
597
+ text=True,
598
+ timeout=3,
599
+ )
600
+ if result.returncode == 0:
601
+ data = json.loads(result.stdout)
602
+ ts_ips = data.get("Self", {}).get("TailscaleIPs", [])
603
+ # Prefer IPv4
604
+ for ip in ts_ips:
605
+ if "." in ip:
606
+ return ip
607
+ if ts_ips:
608
+ return ts_ips[0]
609
+ except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError, OSError):
610
+ pass
611
+ return ""