@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,345 @@
1
+ """Trustee operations tools (health, restart, scale, rotate, monitor, logs, deployments)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from mcp.types import TextContent, Tool
6
+
7
+ from ._helpers import _error_response, _home, _json_response
8
+
9
+ TOOLS: list[Tool] = [
10
+ Tool(
11
+ name="trustee_health",
12
+ description=(
13
+ "Run health checks on all agents in a deployment. "
14
+ "Returns per-agent status, heartbeat, and error info."
15
+ ),
16
+ inputSchema={
17
+ "type": "object",
18
+ "properties": {
19
+ "deployment_id": {
20
+ "type": "string",
21
+ "description": "The deployment ID to check",
22
+ },
23
+ },
24
+ "required": ["deployment_id"],
25
+ },
26
+ ),
27
+ Tool(
28
+ name="trustee_restart",
29
+ description=(
30
+ "Restart a failed agent or all agents in a deployment. "
31
+ "Calls provider stop/start and updates deployment state."
32
+ ),
33
+ inputSchema={
34
+ "type": "object",
35
+ "properties": {
36
+ "deployment_id": {
37
+ "type": "string",
38
+ "description": "The deployment ID",
39
+ },
40
+ "agent_name": {
41
+ "type": "string",
42
+ "description": "Agent to restart (omit for all agents)",
43
+ },
44
+ },
45
+ "required": ["deployment_id"],
46
+ },
47
+ ),
48
+ Tool(
49
+ name="trustee_scale",
50
+ description=(
51
+ "Scale the number of instances for an agent type up or down. "
52
+ "Adds or removes instances while updating deployment state."
53
+ ),
54
+ inputSchema={
55
+ "type": "object",
56
+ "properties": {
57
+ "deployment_id": {
58
+ "type": "string",
59
+ "description": "The deployment ID",
60
+ },
61
+ "agent_spec_key": {
62
+ "type": "string",
63
+ "description": "The agent spec key (role) to scale",
64
+ },
65
+ "count": {
66
+ "type": "integer",
67
+ "description": "Desired total instance count (>= 1)",
68
+ },
69
+ },
70
+ "required": ["deployment_id", "agent_spec_key", "count"],
71
+ },
72
+ ),
73
+ Tool(
74
+ name="trustee_rotate",
75
+ description=(
76
+ "Snapshot context, destroy, and redeploy an agent fresh. "
77
+ "Used when an agent shows context degradation."
78
+ ),
79
+ inputSchema={
80
+ "type": "object",
81
+ "properties": {
82
+ "deployment_id": {
83
+ "type": "string",
84
+ "description": "The deployment ID",
85
+ },
86
+ "agent_name": {
87
+ "type": "string",
88
+ "description": "Agent to rotate",
89
+ },
90
+ },
91
+ "required": ["deployment_id", "agent_name"],
92
+ },
93
+ ),
94
+ Tool(
95
+ name="trustee_monitor",
96
+ description=(
97
+ "Run a single autonomous monitoring pass over all deployments "
98
+ "or a specific one. Detects stale heartbeats, triggers "
99
+ "auto-restart/rotate, and escalates on critical degradation."
100
+ ),
101
+ inputSchema={
102
+ "type": "object",
103
+ "properties": {
104
+ "deployment_id": {
105
+ "type": "string",
106
+ "description": "Specific deployment to check (omit for all)",
107
+ },
108
+ "heartbeat_timeout": {
109
+ "type": "number",
110
+ "description": "Seconds before heartbeat is stale (default: 120)",
111
+ },
112
+ "auto_restart": {
113
+ "type": "boolean",
114
+ "description": "Enable auto-restart on failure (default: true)",
115
+ },
116
+ "auto_rotate": {
117
+ "type": "boolean",
118
+ "description": "Enable auto-rotate after repeated failures (default: true)",
119
+ },
120
+ },
121
+ "required": [],
122
+ },
123
+ ),
124
+ Tool(
125
+ name="trustee_logs",
126
+ description=(
127
+ "Get recent log lines for agents in a deployment. "
128
+ "Reads agent log files or falls back to audit log entries."
129
+ ),
130
+ inputSchema={
131
+ "type": "object",
132
+ "properties": {
133
+ "deployment_id": {
134
+ "type": "string",
135
+ "description": "The deployment ID",
136
+ },
137
+ "agent_name": {
138
+ "type": "string",
139
+ "description": "Specific agent (omit for all)",
140
+ },
141
+ "tail": {
142
+ "type": "integer",
143
+ "description": "Max lines per agent (default: 50)",
144
+ },
145
+ },
146
+ "required": ["deployment_id"],
147
+ },
148
+ ),
149
+ Tool(
150
+ name="trustee_deployments",
151
+ description=(
152
+ "List all active deployments with agent counts and status. "
153
+ "Overview of the entire team fleet."
154
+ ),
155
+ inputSchema={"type": "object", "properties": {}, "required": []},
156
+ ),
157
+ ]
158
+
159
+
160
+ # ── Helpers ──────────────────────────────────────────────────
161
+
162
+
163
+ def _get_trustee_ops():
164
+ """Build TrusteeOps and TeamEngine from agent home."""
165
+ from ..team_engine import TeamEngine
166
+ from ..trustee_ops import TrusteeOps
167
+
168
+ home = _home()
169
+ engine = TeamEngine(home=home, provider=None, comms_root=None)
170
+ ops = TrusteeOps(engine=engine, home=home)
171
+ return ops, engine
172
+
173
+
174
+ # ── Handlers ─────────────────────────────────────────────────
175
+
176
+
177
+ async def _handle_trustee_health(args: dict) -> list[TextContent]:
178
+ """Run health checks on a deployment."""
179
+ deployment_id = args.get("deployment_id", "")
180
+ if not deployment_id:
181
+ return _error_response("deployment_id is required")
182
+
183
+ ops, _ = _get_trustee_ops()
184
+ try:
185
+ report = ops.health_report(deployment_id)
186
+ healthy = sum(1 for r in report if r["healthy"])
187
+ return _json_response({
188
+ "deployment_id": deployment_id,
189
+ "agents": report,
190
+ "summary": {
191
+ "total": len(report),
192
+ "healthy": healthy,
193
+ "degraded": len(report) - healthy,
194
+ },
195
+ })
196
+ except ValueError as exc:
197
+ return _error_response(str(exc))
198
+
199
+
200
+ async def _handle_trustee_restart(args: dict) -> list[TextContent]:
201
+ """Restart agents in a deployment."""
202
+ deployment_id = args.get("deployment_id", "")
203
+ if not deployment_id:
204
+ return _error_response("deployment_id is required")
205
+
206
+ agent_name = args.get("agent_name")
207
+ ops, _ = _get_trustee_ops()
208
+ try:
209
+ results = ops.restart_agent(deployment_id, agent_name)
210
+ return _json_response({
211
+ "deployment_id": deployment_id,
212
+ "results": results,
213
+ "all_restarted": all(v == "restarted" for v in results.values()),
214
+ })
215
+ except ValueError as exc:
216
+ return _error_response(str(exc))
217
+
218
+
219
+ async def _handle_trustee_scale(args: dict) -> list[TextContent]:
220
+ """Scale agent instances in a deployment."""
221
+ deployment_id = args.get("deployment_id", "")
222
+ agent_spec_key = args.get("agent_spec_key", "")
223
+ count = args.get("count", 0)
224
+ if not deployment_id or not agent_spec_key or not count:
225
+ return _error_response("deployment_id, agent_spec_key, and count are required")
226
+
227
+ ops, _ = _get_trustee_ops()
228
+ try:
229
+ result = ops.scale_agent(deployment_id, agent_spec_key, count)
230
+ return _json_response({
231
+ "deployment_id": deployment_id,
232
+ "agent_spec_key": agent_spec_key,
233
+ **result,
234
+ })
235
+ except ValueError as exc:
236
+ return _error_response(str(exc))
237
+
238
+
239
+ async def _handle_trustee_rotate(args: dict) -> list[TextContent]:
240
+ """Rotate an agent (snapshot + fresh deploy)."""
241
+ deployment_id = args.get("deployment_id", "")
242
+ agent_name = args.get("agent_name", "")
243
+ if not deployment_id or not agent_name:
244
+ return _error_response("deployment_id and agent_name are required")
245
+
246
+ ops, _ = _get_trustee_ops()
247
+ try:
248
+ result = ops.rotate_agent(deployment_id, agent_name)
249
+ return _json_response({
250
+ "deployment_id": deployment_id,
251
+ "agent_name": agent_name,
252
+ **result,
253
+ })
254
+ except ValueError as exc:
255
+ return _error_response(str(exc))
256
+
257
+
258
+ async def _handle_trustee_monitor(args: dict) -> list[TextContent]:
259
+ """Run a single monitoring pass."""
260
+ from ..trustee_monitor import MonitorConfig, TrusteeMonitor
261
+
262
+ ops, engine = _get_trustee_ops()
263
+ config = MonitorConfig(
264
+ heartbeat_timeout=args.get("heartbeat_timeout", 120.0),
265
+ auto_restart=args.get("auto_restart", True),
266
+ auto_rotate=args.get("auto_rotate", True),
267
+ )
268
+ monitor = TrusteeMonitor(ops, engine, config)
269
+
270
+ deployment_id = args.get("deployment_id")
271
+ if deployment_id:
272
+ deployment = engine.get_deployment(deployment_id)
273
+ if not deployment:
274
+ return _error_response(f"Deployment '{deployment_id}' not found")
275
+ report = monitor.check_deployment(deployment)
276
+ else:
277
+ report = monitor.check_all()
278
+
279
+ return _json_response({
280
+ "timestamp": report.timestamp,
281
+ "deployments_checked": report.deployments_checked,
282
+ "agents_healthy": report.agents_healthy,
283
+ "agents_degraded": report.agents_degraded,
284
+ "restarts_triggered": report.restarts_triggered,
285
+ "rotations_triggered": report.rotations_triggered,
286
+ "escalations_sent": report.escalations_sent,
287
+ })
288
+
289
+
290
+ async def _handle_trustee_logs(args: dict) -> list[TextContent]:
291
+ """Get agent logs from a deployment."""
292
+ deployment_id = args.get("deployment_id", "")
293
+ if not deployment_id:
294
+ return _error_response("deployment_id is required")
295
+
296
+ agent_name = args.get("agent_name")
297
+ tail = args.get("tail", 50)
298
+ ops, _ = _get_trustee_ops()
299
+ try:
300
+ logs = ops.get_logs(deployment_id, agent_name, tail=tail)
301
+ return _json_response({
302
+ "deployment_id": deployment_id,
303
+ "agents": {name: lines for name, lines in logs.items()},
304
+ })
305
+ except ValueError as exc:
306
+ return _error_response(str(exc))
307
+
308
+
309
+ async def _handle_trustee_deployments(_args: dict) -> list[TextContent]:
310
+ """List all active deployments."""
311
+ _, engine = _get_trustee_ops()
312
+ deployments = engine.list_deployments()
313
+ return _json_response({
314
+ "count": len(deployments),
315
+ "deployments": [
316
+ {
317
+ "deployment_id": d.deployment_id,
318
+ "blueprint_slug": d.blueprint_slug,
319
+ "team_name": d.team_name,
320
+ "provider": d.provider,
321
+ "status": d.status,
322
+ "agent_count": len(d.agents),
323
+ "agents": {
324
+ name: {
325
+ "status": a.status.value if hasattr(a.status, "value") else str(a.status),
326
+ "host": a.host or "\u2014",
327
+ "last_heartbeat": a.last_heartbeat or "\u2014",
328
+ }
329
+ for name, a in d.agents.items()
330
+ },
331
+ }
332
+ for d in deployments
333
+ ],
334
+ })
335
+
336
+
337
+ HANDLERS: dict = {
338
+ "trustee_health": _handle_trustee_health,
339
+ "trustee_restart": _handle_trustee_restart,
340
+ "trustee_scale": _handle_trustee_scale,
341
+ "trustee_rotate": _handle_trustee_rotate,
342
+ "trustee_monitor": _handle_trustee_monitor,
343
+ "trustee_logs": _handle_trustee_logs,
344
+ "trustee_deployments": _handle_trustee_deployments,
345
+ }
@@ -0,0 +1,313 @@
1
+ """
2
+ mDNS peer discovery for SKCapstone — Zeroconf-based LAN peer detection.
3
+
4
+ Registers ``_skcapstone._tcp`` on daemon start and browses for other
5
+ instances on the local network. Discovered peers are written as synthetic
6
+ heartbeat files (``metadata.source = "mdns"``) so
7
+ ``HeartbeatBeacon.discover_peers()`` picks them up through the normal flow.
8
+
9
+ Gracefully disabled at import time if the ``zeroconf`` package is not
10
+ installed — no hard dependency.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ import platform as _platform
18
+ import socket
19
+ import threading
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Optional
23
+
24
+ logger = logging.getLogger("skcapstone.mdns_discovery")
25
+
26
+ MDNS_SERVICE_TYPE = "_skcapstone._tcp.local."
27
+
28
+ # Short TTL so a stale mDNS peer auto-expires within 2 minutes if the
29
+ # browse callback never fires ``remove_service``.
30
+ MDNS_TTL = 120
31
+
32
+ try:
33
+ from zeroconf import ServiceBrowser, ServiceInfo, Zeroconf # type: ignore[import-untyped]
34
+
35
+ _ZEROCONF_AVAILABLE = True
36
+ except ImportError:
37
+ _ZEROCONF_AVAILABLE = False
38
+
39
+
40
+ class MDNSDiscovery:
41
+ """mDNS peer discovery for the SKCapstone agent mesh.
42
+
43
+ Registers the local agent as a ``_skcapstone._tcp`` Zeroconf service and
44
+ browses for other instances on the same LAN segment. When a peer is
45
+ found, a synthetic heartbeat JSON file is written to *heartbeats_dir* with
46
+ ``metadata.source = "mdns"`` so ``HeartbeatBeacon.discover_peers()``
47
+ transparently includes it. When the service disappears the heartbeat is
48
+ marked offline (TTL=0) so it immediately ages out.
49
+
50
+ Args:
51
+ agent_name: Local agent name — used as the mDNS service instance name
52
+ and written into the synthetic heartbeat ``agent_name`` field.
53
+ port: Local HTTP API port advertised in the TXT record.
54
+ heartbeats_dir: Directory that ``HeartbeatBeacon`` reads heartbeat
55
+ files from (``~/.skcapstone/heartbeats/`` by default).
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ agent_name: str,
61
+ port: int,
62
+ heartbeats_dir: Path,
63
+ ) -> None:
64
+ self._agent_name = agent_name
65
+ self._port = port
66
+ self._heartbeats_dir = heartbeats_dir
67
+
68
+ self._zc: Optional[object] = None # Zeroconf instance
69
+ self._browser: Optional[object] = None # ServiceBrowser
70
+ self._info: Optional[object] = None # ServiceInfo
71
+
72
+ self._lock = threading.Lock()
73
+ # Maps raw mDNS service name → agent_name for peers we track
74
+ self._mdns_peers: dict[str, str] = {}
75
+
76
+ # ------------------------------------------------------------------
77
+ # Public API
78
+ # ------------------------------------------------------------------
79
+
80
+ def start(self) -> None:
81
+ """Register the local service and start browsing for peers.
82
+
83
+ Does nothing (logs a warning) when ``zeroconf`` is not installed.
84
+ """
85
+ if not _ZEROCONF_AVAILABLE:
86
+ logger.warning(
87
+ "zeroconf not installed — mDNS peer discovery disabled. "
88
+ "Install with: pip install 'skcapstone[mdns]'"
89
+ )
90
+ return
91
+
92
+ self._heartbeats_dir.mkdir(parents=True, exist_ok=True)
93
+
94
+ zc = Zeroconf()
95
+ self._zc = zc
96
+
97
+ addresses = self._local_addresses()
98
+ props: dict[str, str] = {
99
+ "agent": self._agent_name,
100
+ "platform": f"{_platform.system()} {_platform.machine()}",
101
+ }
102
+ instance_name = f"{self._agent_name}.{MDNS_SERVICE_TYPE}"
103
+ self._info = ServiceInfo(
104
+ type_=MDNS_SERVICE_TYPE,
105
+ name=instance_name,
106
+ addresses=addresses,
107
+ port=self._port,
108
+ properties=props,
109
+ server=f"{socket.gethostname()}.local.",
110
+ )
111
+
112
+ try:
113
+ zc.register_service(self._info)
114
+ logger.info(
115
+ "mDNS: registered '%s' on port %d", instance_name, self._port
116
+ )
117
+ except Exception as exc:
118
+ logger.warning("mDNS: service registration failed: %s", exc)
119
+
120
+ self._browser = ServiceBrowser(zc, MDNS_SERVICE_TYPE, self._make_listener())
121
+ logger.info("mDNS: browsing for %s", MDNS_SERVICE_TYPE)
122
+
123
+ def stop(self) -> None:
124
+ """Unregister the local service and close the Zeroconf socket."""
125
+ if self._zc is None:
126
+ return
127
+
128
+ try:
129
+ if self._info is not None:
130
+ self._zc.unregister_service(self._info) # type: ignore[attr-defined]
131
+ except Exception as exc:
132
+ logger.debug("mDNS: unregister error: %s", exc)
133
+
134
+ try:
135
+ self._zc.close() # type: ignore[attr-defined]
136
+ except Exception as exc:
137
+ logger.debug("mDNS: close error: %s", exc)
138
+
139
+ self._zc = None
140
+ logger.info("mDNS: stopped")
141
+
142
+ # ------------------------------------------------------------------
143
+ # Internal
144
+ # ------------------------------------------------------------------
145
+
146
+ def _make_listener(self):
147
+ """Build a zeroconf ServiceListener that delegates to this instance."""
148
+ discovery = self
149
+
150
+ class _Listener:
151
+ def add_service(self, zc, type_: str, name: str) -> None:
152
+ discovery._on_add(zc, type_, name)
153
+
154
+ def remove_service(self, zc, type_: str, name: str) -> None:
155
+ discovery._on_remove(zc, type_, name)
156
+
157
+ def update_service(self, zc, type_: str, name: str) -> None:
158
+ discovery._on_add(zc, type_, name)
159
+
160
+ return _Listener()
161
+
162
+ def _on_add(self, zc, type_: str, name: str) -> None:
163
+ """Handle a newly discovered or updated ``_skcapstone._tcp`` service."""
164
+ try:
165
+ info = zc.get_service_info(type_, name)
166
+ if info is None:
167
+ return
168
+
169
+ props: dict[str, str] = {}
170
+ for k, v in (info.properties or {}).items():
171
+ key = k.decode() if isinstance(k, bytes) else str(k)
172
+ val = v.decode() if isinstance(v, bytes) else str(v)
173
+ props[key] = val
174
+
175
+ agent_name = props.get("agent", name.split(".")[0])
176
+
177
+ # Skip ourselves — our own registration fires the browser too
178
+ if agent_name == self._agent_name:
179
+ return
180
+
181
+ addresses = [
182
+ socket.inet_ntoa(a) if len(a) == 4 else a.hex()
183
+ for a in (info.addresses or [])
184
+ ]
185
+
186
+ logger.info(
187
+ "mDNS: discovered peer '%s' at %s:%d",
188
+ agent_name,
189
+ addresses,
190
+ info.port,
191
+ )
192
+ self._write_mdns_heartbeat(agent_name, addresses, info.port, props)
193
+
194
+ with self._lock:
195
+ self._mdns_peers[name] = agent_name
196
+
197
+ except Exception as exc:
198
+ logger.warning("mDNS: error handling add for %s: %s", name, exc)
199
+
200
+ def _on_remove(self, zc, type_: str, name: str) -> None:
201
+ """Handle a ``_skcapstone._tcp`` service going offline."""
202
+ with self._lock:
203
+ agent_name = self._mdns_peers.pop(name, None)
204
+
205
+ if agent_name is None:
206
+ return
207
+
208
+ logger.info("mDNS: peer '%s' left the LAN", agent_name)
209
+ self._write_mdns_heartbeat(agent_name, [], 0, {}, offline=True)
210
+
211
+ def _write_mdns_heartbeat(
212
+ self,
213
+ agent_name: str,
214
+ addresses: list[str],
215
+ port: int,
216
+ props: dict[str, str],
217
+ offline: bool = False,
218
+ ) -> None:
219
+ """Write (or update) a synthetic heartbeat JSON for an mDNS peer.
220
+
221
+ Existing heartbeat files whose ``metadata.source`` is **not** ``mdns``
222
+ are left untouched so a real Syncthing-synced heartbeat is never
223
+ overwritten by a weaker mDNS-sourced one.
224
+ """
225
+ self._heartbeats_dir.mkdir(parents=True, exist_ok=True)
226
+
227
+ safe_name = agent_name.lower().replace(" ", "-")
228
+ path = self._heartbeats_dir / f"{safe_name}.json"
229
+
230
+ # Guard: do not overwrite a real (non-mDNS) heartbeat
231
+ if path.exists() and not offline:
232
+ try:
233
+ existing = json.loads(path.read_text(encoding="utf-8"))
234
+ if existing.get("metadata", {}).get("source") != "mdns":
235
+ logger.debug(
236
+ "mDNS: skipping heartbeat write for '%s' — "
237
+ "a non-mDNS heartbeat already exists",
238
+ agent_name,
239
+ )
240
+ return
241
+ except Exception:
242
+ pass
243
+
244
+ heartbeat = {
245
+ "agent_name": agent_name,
246
+ "status": "offline" if offline else "alive",
247
+ "hostname": props.get("hostname", ""),
248
+ "platform": props.get("platform", ""),
249
+ "timestamp": datetime.now(timezone.utc).isoformat(),
250
+ "ttl_seconds": 0 if offline else MDNS_TTL,
251
+ "uptime_hours": 0.0,
252
+ "soul_active": "",
253
+ "claimed_tasks": [],
254
+ "loaded_model": "",
255
+ "session_active": False,
256
+ "consciousness_active": False,
257
+ "uptime_seconds": 0.0,
258
+ "cpu_load_1min": 0.0,
259
+ "memory_used_mb": 0,
260
+ "active_conversations": 0,
261
+ "messages_processed_24h": 0,
262
+ "capacity": {
263
+ "cpu_count": 0,
264
+ "memory_total_mb": 0,
265
+ "memory_available_mb": 0,
266
+ "disk_free_gb": 0.0,
267
+ "gpu_available": False,
268
+ "gpu_name": "",
269
+ },
270
+ "capabilities": [],
271
+ "version": "",
272
+ "fingerprint": "",
273
+ "metadata": {
274
+ "source": "mdns",
275
+ "addresses": addresses,
276
+ "port": port,
277
+ },
278
+ "services": [],
279
+ "tailscale_ip": "",
280
+ }
281
+
282
+ tmp = path.with_suffix(".json.tmp")
283
+ tmp.write_text(json.dumps(heartbeat, indent=2), encoding="utf-8")
284
+ tmp.rename(path)
285
+ logger.debug(
286
+ "mDNS: wrote heartbeat for '%s' (offline=%s)", agent_name, offline
287
+ )
288
+
289
+ @staticmethod
290
+ def _local_addresses() -> list[bytes]:
291
+ """Return non-loopback local IPv4 addresses as packed 4-byte values.
292
+
293
+ Falls back to ``127.0.0.1`` if no suitable address is found so that
294
+ the service can still be registered (useful for loopback-only testing).
295
+ """
296
+ addrs: list[bytes] = []
297
+ try:
298
+ hostname = socket.gethostname()
299
+ for _family, _type, _proto, _canon, sockaddr in socket.getaddrinfo(
300
+ hostname, None, socket.AF_INET
301
+ ):
302
+ ip: str = sockaddr[0]
303
+ if not ip.startswith("127."):
304
+ packed = socket.inet_aton(ip)
305
+ if packed not in addrs:
306
+ addrs.append(packed)
307
+ except Exception as exc:
308
+ logger.debug("mDNS: address detection failed: %s", exc)
309
+
310
+ if not addrs:
311
+ addrs = [socket.inet_aton("127.0.0.1")]
312
+
313
+ return addrs