@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,966 @@
1
+ """Tests for the Docker provider backend.
2
+
3
+ All Docker SDK calls are mocked so no real daemon is required.
4
+ Covers provision, configure, start, stop, destroy, rotate,
5
+ health_check, and generate_compose (including SKComm/MCP wiring).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from types import SimpleNamespace
12
+ from typing import Any, Dict
13
+ from unittest.mock import MagicMock, call, patch
14
+
15
+ import pytest
16
+ import yaml
17
+
18
+ from skcapstone.blueprints.schema import (
19
+ AgentRole,
20
+ AgentSpec,
21
+ BlueprintManifest,
22
+ ModelTier,
23
+ ProviderType,
24
+ ResourceSpec,
25
+ )
26
+ from skcapstone.providers.docker import (
27
+ DockerProvider,
28
+ _DEFAULT_IMAGE,
29
+ _nano_cpus,
30
+ _parse_memory_bytes,
31
+ )
32
+ from skcapstone.team_engine import AgentStatus
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Helpers / fixtures
37
+ # ---------------------------------------------------------------------------
38
+
39
+
40
+ def _make_spec(
41
+ role: str = "worker",
42
+ model: str = "fast",
43
+ memory: str = "2g",
44
+ cores: int = 1,
45
+ skills: list | None = None,
46
+ soul_blueprint: str | None = None,
47
+ env: dict | None = None,
48
+ ) -> AgentSpec:
49
+ """Build a minimal AgentSpec for testing."""
50
+ return AgentSpec(
51
+ role=AgentRole(role),
52
+ model=ModelTier(model),
53
+ resources=ResourceSpec(memory=memory, cores=cores),
54
+ skills=skills or [],
55
+ soul_blueprint=soul_blueprint,
56
+ env=env or {},
57
+ )
58
+
59
+
60
+ def _make_blueprint(agent_count: int = 1) -> BlueprintManifest:
61
+ """Build a minimal BlueprintManifest for testing."""
62
+ agents = {
63
+ f"agent{i}": _make_spec() for i in range(agent_count)
64
+ }
65
+ return BlueprintManifest(
66
+ name="Test Team",
67
+ slug="test-team",
68
+ description="Unit-test blueprint",
69
+ agents=agents,
70
+ default_provider=ProviderType.DOCKER,
71
+ )
72
+
73
+
74
+ def _provision_result(
75
+ container_name: str = "test-agent",
76
+ container_id: str = "abc123def456",
77
+ volume_name: str = "skcapstone-agent-test-agent",
78
+ ) -> Dict[str, Any]:
79
+ """Return a typical provision_result dict."""
80
+ return {
81
+ "container_id": container_id,
82
+ "container_name": container_name,
83
+ "host": container_name,
84
+ "volume_name": volume_name,
85
+ }
86
+
87
+
88
+ @pytest.fixture()
89
+ def provider() -> DockerProvider:
90
+ """Return a DockerProvider with default settings."""
91
+ return DockerProvider(
92
+ base_image="python:3.12-slim",
93
+ network_name="skcapstone",
94
+ volume_prefix="skcapstone-agent",
95
+ )
96
+
97
+
98
+ @pytest.fixture()
99
+ def mock_docker_client():
100
+ """Return a MagicMock simulating docker.DockerClient."""
101
+ client = MagicMock()
102
+ client.ping.return_value = True
103
+
104
+ # Simulate network not existing initially
105
+ client.networks.get.side_effect = Exception("not found")
106
+
107
+ # Simulate containers.get raising when looking for stale container
108
+ client.containers.get.side_effect = Exception("not found")
109
+
110
+ # Simulate volume not existing
111
+ client.volumes.get.side_effect = Exception("not found")
112
+ client.volumes.create.return_value = MagicMock()
113
+
114
+ # Container mock
115
+ mock_container = MagicMock()
116
+ mock_container.id = "abc123def456"
117
+ mock_container.status = "created"
118
+ client.containers.create.return_value = mock_container
119
+
120
+ return client, mock_container
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Unit helpers
125
+ # ---------------------------------------------------------------------------
126
+
127
+
128
+ class TestParseMemoryBytes:
129
+ """Tests for _parse_memory_bytes helper."""
130
+
131
+ def test_gigabytes(self):
132
+ assert _parse_memory_bytes("2g") == 2 * 1024 ** 3
133
+
134
+ def test_megabytes(self):
135
+ assert _parse_memory_bytes("512m") == 512 * 1024 ** 2
136
+
137
+ def test_uppercase_suffix(self):
138
+ assert _parse_memory_bytes("1G") == 1 * 1024 ** 3
139
+
140
+ def test_numeric_only(self):
141
+ assert _parse_memory_bytes("1073741824") == 1073741824
142
+
143
+ def test_fractional_gigabytes(self):
144
+ assert _parse_memory_bytes("0.5g") == int(0.5 * 1024 ** 3)
145
+
146
+
147
+ class TestNanoCpus:
148
+ """Tests for _nano_cpus helper."""
149
+
150
+ def test_single_core(self):
151
+ assert _nano_cpus(1) == 1_000_000_000
152
+
153
+ def test_four_cores(self):
154
+ assert _nano_cpus(4) == 4_000_000_000
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # DockerProvider._client
159
+ # ---------------------------------------------------------------------------
160
+
161
+
162
+ class TestDockerProviderClient:
163
+ """Tests for _client() connection logic."""
164
+
165
+ def test_raises_if_sdk_missing(self, provider: DockerProvider):
166
+ with patch.dict("sys.modules", {"docker": None}):
167
+ with pytest.raises(RuntimeError, match="pip install docker"):
168
+ provider._client()
169
+
170
+ def test_raises_if_daemon_unreachable(self, provider: DockerProvider):
171
+ mock_docker = MagicMock()
172
+ mock_client_instance = MagicMock()
173
+ mock_client_instance.ping.side_effect = Exception("connection refused")
174
+ mock_docker.from_env.return_value = mock_client_instance
175
+
176
+ with patch.dict("sys.modules", {"docker": mock_docker}):
177
+ with pytest.raises(RuntimeError, match="Cannot connect"):
178
+ provider._client()
179
+
180
+ def test_returns_client_on_success(self, provider: DockerProvider):
181
+ mock_docker = MagicMock()
182
+ mock_client_instance = MagicMock()
183
+ mock_client_instance.ping.return_value = True
184
+ mock_docker.from_env.return_value = mock_client_instance
185
+
186
+ with patch.dict("sys.modules", {"docker": mock_docker}):
187
+ result = provider._client()
188
+
189
+ assert result is mock_client_instance
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # provision()
194
+ # ---------------------------------------------------------------------------
195
+
196
+
197
+ class TestProvision:
198
+ """Tests for DockerProvider.provision()."""
199
+
200
+ def _run_provision(self, provider, mock_client, mock_container):
201
+ spec = _make_spec(memory="1g", cores=2)
202
+ with patch.object(provider, "_client", return_value=mock_client):
203
+ result = provider.provision("my-agent", spec, "my-team")
204
+ return result
205
+
206
+ def test_returns_expected_keys(self, provider, mock_docker_client):
207
+ mock_client, mock_container = mock_docker_client
208
+ result = self._run_provision(provider, mock_client, mock_container)
209
+
210
+ assert "container_id" in result
211
+ assert "container_name" in result
212
+ assert "host" in result
213
+ assert "volume_name" in result
214
+
215
+ def test_container_name_derived_from_agent_name(self, provider, mock_docker_client):
216
+ mock_client, mock_container = mock_docker_client
217
+ result = self._run_provision(provider, mock_client, mock_container)
218
+ assert result["container_name"] == "my-agent"
219
+
220
+ def test_network_created_if_missing(self, provider, mock_docker_client):
221
+ mock_client, mock_container = mock_docker_client
222
+ self._run_provision(provider, mock_client, mock_container)
223
+ mock_client.networks.create.assert_called_once()
224
+
225
+ def test_network_not_created_if_exists(self, provider, mock_docker_client):
226
+ mock_client, mock_container = mock_docker_client
227
+ mock_client.networks.get.side_effect = None # network exists
228
+ mock_client.networks.get.return_value = MagicMock()
229
+ self._run_provision(provider, mock_client, mock_container)
230
+ mock_client.networks.create.assert_not_called()
231
+
232
+ def test_volume_created(self, provider, mock_docker_client):
233
+ mock_client, mock_container = mock_docker_client
234
+ self._run_provision(provider, mock_client, mock_container)
235
+ mock_client.volumes.create.assert_called_once()
236
+
237
+ def test_memory_limit_applied(self, provider, mock_docker_client):
238
+ mock_client, mock_container = mock_docker_client
239
+ spec = _make_spec(memory="2g", cores=1)
240
+ with patch.object(provider, "_client", return_value=mock_client):
241
+ provider.provision("agent-x", spec, "team-y")
242
+
243
+ kwargs = mock_client.containers.create.call_args[1]
244
+ assert kwargs["mem_limit"] == 2 * 1024 ** 3
245
+
246
+ def test_cpu_limit_applied(self, provider, mock_docker_client):
247
+ mock_client, mock_container = mock_docker_client
248
+ spec = _make_spec(memory="512m", cores=4)
249
+ with patch.object(provider, "_client", return_value=mock_client):
250
+ provider.provision("agent-x", spec, "team-y")
251
+
252
+ kwargs = mock_client.containers.create.call_args[1]
253
+ assert kwargs["nano_cpus"] == 4_000_000_000
254
+
255
+ def test_environment_vars_set(self, provider, mock_docker_client):
256
+ mock_client, mock_container = mock_docker_client
257
+ spec = _make_spec(env={"MY_KEY": "my_value"})
258
+ with patch.object(provider, "_client", return_value=mock_client):
259
+ provider.provision("agent-x", spec, "team-y")
260
+
261
+ kwargs = mock_client.containers.create.call_args[1]
262
+ env = kwargs["environment"]
263
+ assert env["AGENT_NAME"] == "agent-x"
264
+ assert env["TEAM_NAME"] == "team-y"
265
+ assert env["MY_KEY"] == "my_value"
266
+
267
+ def test_stale_container_removed(self, provider, mock_docker_client):
268
+ mock_client, mock_container = mock_docker_client
269
+ stale = MagicMock()
270
+ # First call returns stale container; subsequent return nothing
271
+ mock_client.containers.get.side_effect = [stale, Exception("not found")]
272
+ spec = _make_spec()
273
+ with patch.object(provider, "_client", return_value=mock_client):
274
+ provider.provision("my-agent", spec, "team")
275
+
276
+ stale.remove.assert_called_once_with(force=True)
277
+
278
+ def test_edge_underscores_in_name_normalised(self, provider, mock_docker_client):
279
+ mock_client, mock_container = mock_docker_client
280
+ spec = _make_spec()
281
+ with patch.object(provider, "_client", return_value=mock_client):
282
+ result = provider.provision("my_agent_name", spec, "team")
283
+ assert result["container_name"] == "my-agent-name"
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # configure()
288
+ # ---------------------------------------------------------------------------
289
+
290
+
291
+ class TestConfigure:
292
+ """Tests for DockerProvider.configure()."""
293
+
294
+ def test_returns_true_on_success(self, provider):
295
+ mock_client = MagicMock()
296
+ mock_container = MagicMock()
297
+ mock_container.status = "running"
298
+ mock_container.exec_run.return_value = (0, b"")
299
+ mock_client.containers.get.return_value = mock_container
300
+
301
+ with patch.object(provider, "_client", return_value=mock_client):
302
+ result = provider.configure(
303
+ "my-agent",
304
+ _make_spec(),
305
+ _provision_result("my-agent"),
306
+ )
307
+
308
+ assert result is True
309
+
310
+ def test_starts_stopped_container_before_config(self, provider):
311
+ mock_client = MagicMock()
312
+ mock_container = MagicMock()
313
+ mock_container.status = "created"
314
+ mock_container.exec_run.return_value = (0, b"")
315
+ mock_client.containers.get.return_value = mock_container
316
+
317
+ with patch.object(provider, "_client", return_value=mock_client):
318
+ provider.configure("my-agent", _make_spec(), _provision_result("my-agent"))
319
+
320
+ mock_container.start.assert_called_once()
321
+
322
+ def test_returns_false_if_container_missing(self, provider):
323
+ mock_client = MagicMock()
324
+ mock_client.containers.get.side_effect = Exception("not found")
325
+
326
+ with patch.object(provider, "_client", return_value=mock_client):
327
+ result = provider.configure(
328
+ "ghost-agent",
329
+ _make_spec(),
330
+ _provision_result("ghost-agent"),
331
+ )
332
+
333
+ assert result is False
334
+
335
+ def test_returns_false_if_exec_fails(self, provider):
336
+ mock_client = MagicMock()
337
+ mock_container = MagicMock()
338
+ mock_container.status = "running"
339
+ mock_container.exec_run.return_value = (1, b"error")
340
+ mock_client.containers.get.return_value = mock_container
341
+
342
+ with patch.object(provider, "_client", return_value=mock_client):
343
+ result = provider.configure(
344
+ "fail-agent",
345
+ _make_spec(),
346
+ _provision_result("fail-agent"),
347
+ )
348
+
349
+ assert result is False
350
+
351
+ def test_empty_container_name_returns_false(self, provider):
352
+ result = provider.configure("x", _make_spec(), {})
353
+ assert result is False
354
+
355
+
356
+ # ---------------------------------------------------------------------------
357
+ # start()
358
+ # ---------------------------------------------------------------------------
359
+
360
+
361
+ class TestStart:
362
+ """Tests for DockerProvider.start()."""
363
+
364
+ def test_returns_true_on_success(self, provider):
365
+ mock_client = MagicMock()
366
+ mock_container = MagicMock()
367
+ mock_container.id = "abc"
368
+ mock_client.containers.get.return_value = mock_container
369
+
370
+ with patch.object(provider, "_client", return_value=mock_client):
371
+ result = provider.start("agent", _provision_result())
372
+
373
+ assert result is True
374
+ mock_container.start.assert_called_once()
375
+
376
+ def test_returns_false_on_docker_error(self, provider):
377
+ mock_client = MagicMock()
378
+ mock_client.containers.get.side_effect = Exception("not found")
379
+
380
+ with patch.object(provider, "_client", return_value=mock_client):
381
+ result = provider.start("ghost", _provision_result("ghost"))
382
+
383
+ assert result is False
384
+
385
+ def test_empty_container_name_returns_false(self, provider):
386
+ mock_client = MagicMock()
387
+ with patch.object(provider, "_client", return_value=mock_client):
388
+ result = provider.start("x", {})
389
+ assert result is False
390
+
391
+
392
+ # ---------------------------------------------------------------------------
393
+ # stop()
394
+ # ---------------------------------------------------------------------------
395
+
396
+
397
+ class TestStop:
398
+ """Tests for DockerProvider.stop()."""
399
+
400
+ def test_returns_true_on_success(self, provider):
401
+ mock_client = MagicMock()
402
+ mock_container = MagicMock()
403
+ mock_client.containers.get.return_value = mock_container
404
+
405
+ with patch.object(provider, "_client", return_value=mock_client):
406
+ result = provider.stop("agent", _provision_result())
407
+
408
+ assert result is True
409
+ mock_container.stop.assert_called_once_with(timeout=15)
410
+
411
+ def test_returns_false_on_docker_error(self, provider):
412
+ mock_client = MagicMock()
413
+ mock_client.containers.get.return_value = MagicMock()
414
+ mock_client.containers.get.return_value.stop.side_effect = Exception("err")
415
+
416
+ with patch.object(provider, "_client", return_value=mock_client):
417
+ result = provider.stop("agent", _provision_result())
418
+
419
+ assert result is False
420
+
421
+ def test_empty_container_name_returns_true(self, provider):
422
+ mock_client = MagicMock()
423
+ with patch.object(provider, "_client", return_value=mock_client):
424
+ result = provider.stop("x", {})
425
+ assert result is True
426
+
427
+
428
+ # ---------------------------------------------------------------------------
429
+ # destroy()
430
+ # ---------------------------------------------------------------------------
431
+
432
+
433
+ class TestDestroy:
434
+ """Tests for DockerProvider.destroy()."""
435
+
436
+ def test_removes_container_and_volume(self, provider):
437
+ mock_client = MagicMock()
438
+ mock_container = MagicMock()
439
+ mock_volume = MagicMock()
440
+ mock_client.containers.get.return_value = mock_container
441
+ mock_client.volumes.get.return_value = mock_volume
442
+
443
+ pr = _provision_result()
444
+ with patch.object(provider, "_client", return_value=mock_client):
445
+ with patch.object(provider, "stop", return_value=True):
446
+ result = provider.destroy("agent", pr)
447
+
448
+ assert result is True
449
+ mock_container.remove.assert_called_once_with(v=True, force=True)
450
+ mock_volume.remove.assert_called_once_with(force=True)
451
+
452
+ def test_returns_false_if_container_remove_fails(self, provider):
453
+ mock_client = MagicMock()
454
+ mock_container = MagicMock()
455
+ mock_container.remove.side_effect = Exception("locked")
456
+ mock_client.containers.get.return_value = mock_container
457
+ mock_client.volumes.get.side_effect = Exception("no vol")
458
+
459
+ pr = _provision_result()
460
+ with patch.object(provider, "_client", return_value=mock_client):
461
+ with patch.object(provider, "stop", return_value=True):
462
+ result = provider.destroy("agent", pr)
463
+
464
+ assert result is False
465
+
466
+ def test_tolerates_missing_volume(self, provider):
467
+ mock_client = MagicMock()
468
+ mock_container = MagicMock()
469
+ mock_client.containers.get.return_value = mock_container
470
+ mock_client.volumes.get.side_effect = Exception("not found")
471
+
472
+ pr = _provision_result()
473
+ with patch.object(provider, "_client", return_value=mock_client):
474
+ with patch.object(provider, "stop", return_value=True):
475
+ result = provider.destroy("agent", pr)
476
+
477
+ assert result is True
478
+
479
+
480
+ # ---------------------------------------------------------------------------
481
+ # health_check()
482
+ # ---------------------------------------------------------------------------
483
+
484
+
485
+ class TestHealthCheck:
486
+ """Tests for DockerProvider.health_check()."""
487
+
488
+ def _make_container(self, status: str) -> MagicMock:
489
+ c = MagicMock()
490
+ c.status = status
491
+ return c
492
+
493
+ def test_running_returns_running(self, provider):
494
+ mock_client = MagicMock()
495
+ mock_client.containers.get.return_value = self._make_container("running")
496
+
497
+ with patch.object(provider, "_client", return_value=mock_client):
498
+ result = provider.health_check("agent", _provision_result())
499
+
500
+ assert result == AgentStatus.RUNNING
501
+
502
+ def test_exited_returns_stopped(self, provider):
503
+ mock_client = MagicMock()
504
+ mock_client.containers.get.return_value = self._make_container("exited")
505
+
506
+ with patch.object(provider, "_client", return_value=mock_client):
507
+ result = provider.health_check("agent", _provision_result())
508
+
509
+ assert result == AgentStatus.STOPPED
510
+
511
+ def test_paused_returns_degraded(self, provider):
512
+ mock_client = MagicMock()
513
+ mock_client.containers.get.return_value = self._make_container("paused")
514
+
515
+ with patch.object(provider, "_client", return_value=mock_client):
516
+ result = provider.health_check("agent", _provision_result())
517
+
518
+ assert result == AgentStatus.DEGRADED
519
+
520
+ def test_dead_returns_stopped(self, provider):
521
+ mock_client = MagicMock()
522
+ mock_client.containers.get.return_value = self._make_container("dead")
523
+
524
+ with patch.object(provider, "_client", return_value=mock_client):
525
+ result = provider.health_check("agent", _provision_result())
526
+
527
+ assert result == AgentStatus.STOPPED
528
+
529
+ def test_unknown_state_returns_degraded(self, provider):
530
+ mock_client = MagicMock()
531
+ mock_client.containers.get.return_value = self._make_container("restarting")
532
+
533
+ with patch.object(provider, "_client", return_value=mock_client):
534
+ result = provider.health_check("agent", _provision_result())
535
+
536
+ assert result == AgentStatus.DEGRADED
537
+
538
+ def test_missing_container_returns_failed(self, provider):
539
+ mock_client = MagicMock()
540
+ mock_client.containers.get.side_effect = Exception("not found")
541
+
542
+ with patch.object(provider, "_client", return_value=mock_client):
543
+ result = provider.health_check("ghost", _provision_result("ghost"))
544
+
545
+ assert result == AgentStatus.FAILED
546
+
547
+ def test_empty_container_name_returns_stopped(self, provider):
548
+ mock_client = MagicMock()
549
+ with patch.object(provider, "_client", return_value=mock_client):
550
+ result = provider.health_check("x", {})
551
+ assert result == AgentStatus.STOPPED
552
+
553
+
554
+ # ---------------------------------------------------------------------------
555
+ # generate_compose()
556
+ # ---------------------------------------------------------------------------
557
+
558
+
559
+ class TestGenerateCompose:
560
+ """Tests for DockerProvider.generate_compose()."""
561
+
562
+ def test_returns_valid_yaml(self, provider):
563
+ bp = _make_blueprint(agent_count=2)
564
+ output = provider.generate_compose(bp)
565
+ parsed = yaml.safe_load(output)
566
+ assert isinstance(parsed, dict)
567
+ assert "services" in parsed
568
+
569
+ def test_services_match_agent_count(self, provider):
570
+ bp = _make_blueprint(agent_count=3)
571
+ output = provider.generate_compose(bp)
572
+ parsed = yaml.safe_load(output)
573
+ assert len(parsed["services"]) == 3
574
+
575
+ def test_volumes_section_present(self, provider):
576
+ bp = _make_blueprint(agent_count=1)
577
+ output = provider.generate_compose(bp)
578
+ parsed = yaml.safe_load(output)
579
+ assert "volumes" in parsed
580
+
581
+ def test_networks_section_present(self, provider):
582
+ bp = _make_blueprint(agent_count=1)
583
+ output = provider.generate_compose(bp)
584
+ parsed = yaml.safe_load(output)
585
+ assert "networks" in parsed
586
+ assert "skcapstone" in parsed["networks"]
587
+
588
+ def test_memory_in_deploy_limits(self, provider):
589
+ bp = BlueprintManifest(
590
+ name="Mem Team",
591
+ slug="mem-team",
592
+ description="test",
593
+ agents={"alpha": _make_spec(memory="4g", cores=2)},
594
+ default_provider=ProviderType.DOCKER,
595
+ )
596
+ output = provider.generate_compose(bp)
597
+ parsed = yaml.safe_load(output)
598
+ svc = list(parsed["services"].values())[0]
599
+ mem = svc["deploy"]["resources"]["limits"]["memory"]
600
+ assert "4G" in mem.upper()
601
+
602
+ def test_cpu_in_deploy_limits(self, provider):
603
+ bp = BlueprintManifest(
604
+ name="Cpu Team",
605
+ slug="cpu-team",
606
+ description="test",
607
+ agents={"alpha": _make_spec(cores=4)},
608
+ default_provider=ProviderType.DOCKER,
609
+ )
610
+ output = provider.generate_compose(bp)
611
+ parsed = yaml.safe_load(output)
612
+ svc = list(parsed["services"].values())[0]
613
+ cpus = svc["deploy"]["resources"]["limits"]["cpus"]
614
+ assert cpus == "4"
615
+
616
+ def test_soul_blueprint_in_env_when_set(self, provider):
617
+ bp = BlueprintManifest(
618
+ name="Soul Team",
619
+ slug="soul-team",
620
+ description="test",
621
+ agents={"alpha": _make_spec(soul_blueprint="souls/sentinel.yaml")},
622
+ default_provider=ProviderType.DOCKER,
623
+ )
624
+ output = provider.generate_compose(bp)
625
+ parsed = yaml.safe_load(output)
626
+ svc = list(parsed["services"].values())[0]
627
+ assert svc["environment"].get("SOUL_BLUEPRINT") == "souls/sentinel.yaml"
628
+
629
+ def test_count_expands_to_multiple_services(self, provider):
630
+ spec = AgentSpec(
631
+ role=AgentRole.WORKER,
632
+ model=ModelTier.FAST,
633
+ resources=ResourceSpec(),
634
+ count=3,
635
+ )
636
+ bp = BlueprintManifest(
637
+ name="Scale Team",
638
+ slug="scale-team",
639
+ description="test",
640
+ agents={"worker": spec},
641
+ default_provider=ProviderType.DOCKER,
642
+ )
643
+ output = provider.generate_compose(bp)
644
+ parsed = yaml.safe_load(output)
645
+ assert len(parsed["services"]) == 3
646
+
647
+ def test_writes_to_file_when_output_path_provided(self, provider, tmp_path):
648
+ bp = _make_blueprint()
649
+ out = tmp_path / "docker-compose.yml"
650
+ provider.generate_compose(bp, output_path=out)
651
+ assert out.exists()
652
+ content = yaml.safe_load(out.read_text())
653
+ assert "services" in content
654
+
655
+ def test_edge_empty_agents_produces_no_services(self, provider):
656
+ """Edge case: blueprint with no agents should yield empty services."""
657
+ bp = BlueprintManifest(
658
+ name="Empty Team",
659
+ slug="empty-team",
660
+ description="no agents",
661
+ agents={},
662
+ default_provider=ProviderType.DOCKER,
663
+ )
664
+ output = provider.generate_compose(bp)
665
+ parsed = yaml.safe_load(output)
666
+ assert parsed["services"] == {} or parsed["services"] is None
667
+
668
+
669
+ # ---------------------------------------------------------------------------
670
+ # provision() — team_name fix
671
+ # ---------------------------------------------------------------------------
672
+
673
+
674
+ class TestProvisionTeamName:
675
+ """Verify that team_name is included in the provision result."""
676
+
677
+ def test_team_name_in_result(self, provider, mock_docker_client):
678
+ mock_client, mock_container = mock_docker_client
679
+ spec = _make_spec()
680
+ with patch.object(provider, "_client", return_value=mock_client):
681
+ result = provider.provision("my-agent", spec, "my-team")
682
+
683
+ assert result.get("team_name") == "my-team"
684
+
685
+ def test_configure_uses_team_name_from_provision_result(self, provider):
686
+ """configure() should not produce empty team_name in config.json."""
687
+ mock_client = MagicMock()
688
+ mock_container = MagicMock()
689
+ mock_container.status = "running"
690
+ # Capture the exec_run cmd to inspect the config JSON written
691
+ written_json: list[str] = []
692
+
693
+ def capture_exec(cmd, **kwargs):
694
+ # The sh -c command contains the JSON payload
695
+ written_json.append(cmd[2] if len(cmd) > 2 else "")
696
+ return (0, b"")
697
+
698
+ mock_container.exec_run.side_effect = capture_exec
699
+ mock_client.containers.get.return_value = mock_container
700
+
701
+ spec = _make_spec()
702
+ pr = _provision_result("my-agent")
703
+ pr["team_name"] = "alpha-team"
704
+
705
+ with patch.object(provider, "_client", return_value=mock_client):
706
+ provider.configure("my-agent", spec, pr)
707
+
708
+ assert written_json, "exec_run was never called"
709
+ assert "alpha-team" in written_json[0]
710
+
711
+
712
+ # ---------------------------------------------------------------------------
713
+ # SKComm / MCP sovereign wiring
714
+ # ---------------------------------------------------------------------------
715
+
716
+
717
+ class TestSovereignWiring:
718
+ """Verify SKComm and MCP env vars are injected correctly."""
719
+
720
+ def test_mcp_host_injected_in_env(self, mock_docker_client):
721
+ mock_client, mock_container = mock_docker_client
722
+ provider = DockerProvider(
723
+ base_image="python:3.12-slim",
724
+ network_name="skcapstone",
725
+ mcp_host="host-gateway:8765",
726
+ )
727
+ spec = _make_spec()
728
+ with patch.object(provider, "_client", return_value=mock_client):
729
+ provider.provision("agent-x", spec, "team-y")
730
+
731
+ kwargs = mock_client.containers.create.call_args[1]
732
+ env = kwargs["environment"]
733
+ assert env.get("SKCAPSTONE_MCP_HOST") == "host-gateway:8765"
734
+
735
+ def test_skcomm_home_env_injected_when_dir_exists(
736
+ self, mock_docker_client, tmp_path
737
+ ):
738
+ skcomm_dir = tmp_path / "skcomm"
739
+ skcomm_dir.mkdir()
740
+
741
+ mock_client, mock_container = mock_docker_client
742
+ provider = DockerProvider(
743
+ base_image="python:3.12-slim",
744
+ network_name="skcapstone",
745
+ skcomm_home=str(skcomm_dir),
746
+ )
747
+ spec = _make_spec()
748
+ with patch.object(provider, "_client", return_value=mock_client):
749
+ provider.provision("agent-x", spec, "team-y")
750
+
751
+ kwargs = mock_client.containers.create.call_args[1]
752
+ env = kwargs["environment"]
753
+ assert env.get("SKCOMM_HOME") == "/skcomm"
754
+
755
+ def test_skcomm_volume_mounted_when_dir_exists(
756
+ self, mock_docker_client, tmp_path
757
+ ):
758
+ skcomm_dir = tmp_path / "skcomm"
759
+ skcomm_dir.mkdir()
760
+
761
+ mock_client, mock_container = mock_docker_client
762
+ provider = DockerProvider(
763
+ base_image="python:3.12-slim",
764
+ network_name="skcapstone",
765
+ skcomm_home=str(skcomm_dir),
766
+ )
767
+ spec = _make_spec()
768
+ with patch.object(provider, "_client", return_value=mock_client):
769
+ provider.provision("agent-x", spec, "team-y")
770
+
771
+ kwargs = mock_client.containers.create.call_args[1]
772
+ volumes = kwargs["volumes"]
773
+ assert str(skcomm_dir) in volumes
774
+ assert volumes[str(skcomm_dir)]["bind"] == "/skcomm"
775
+
776
+ def test_no_skcomm_mount_when_dir_missing(self, mock_docker_client):
777
+ mock_client, mock_container = mock_docker_client
778
+ provider = DockerProvider(
779
+ base_image="python:3.12-slim",
780
+ network_name="skcapstone",
781
+ skcomm_home="/nonexistent/skcomm",
782
+ )
783
+ spec = _make_spec()
784
+ with patch.object(provider, "_client", return_value=mock_client):
785
+ provider.provision("agent-x", spec, "team-y")
786
+
787
+ kwargs = mock_client.containers.create.call_args[1]
788
+ volumes = kwargs["volumes"]
789
+ assert "/nonexistent/skcomm" not in volumes
790
+
791
+ def test_mcp_socket_env_injected_when_socket_missing(self, mock_docker_client):
792
+ """SKCAPSTONE_MCP_SOCKET env is set regardless; socket mounted only if exists."""
793
+ mock_client, mock_container = mock_docker_client
794
+ provider = DockerProvider(
795
+ base_image="python:3.12-slim",
796
+ network_name="skcapstone",
797
+ mcp_socket_path="/run/skcapstone/mcp.sock",
798
+ )
799
+ spec = _make_spec()
800
+ with patch.object(provider, "_client", return_value=mock_client):
801
+ provider.provision("agent-x", spec, "team-y")
802
+
803
+ kwargs = mock_client.containers.create.call_args[1]
804
+ env = kwargs["environment"]
805
+ # Socket path env always set; actual mount conditional on file existence
806
+ assert "SKCAPSTONE_MCP_SOCKET" in env
807
+
808
+ def test_soul_blueprint_in_env_on_provision(self, mock_docker_client):
809
+ mock_client, mock_container = mock_docker_client
810
+ provider = DockerProvider(
811
+ base_image="python:3.12-slim",
812
+ network_name="skcapstone",
813
+ )
814
+ spec = _make_spec(soul_blueprint="souls/sentinel.yaml")
815
+ with patch.object(provider, "_client", return_value=mock_client):
816
+ provider.provision("sentinel-1", spec, "ops-team")
817
+
818
+ kwargs = mock_client.containers.create.call_args[1]
819
+ env = kwargs["environment"]
820
+ assert env.get("SOUL_BLUEPRINT") == "souls/sentinel.yaml"
821
+
822
+
823
+ # ---------------------------------------------------------------------------
824
+ # rotate()
825
+ # ---------------------------------------------------------------------------
826
+
827
+
828
+ class TestRotate:
829
+ """Tests for DockerProvider.rotate()."""
830
+
831
+ def test_rotate_calls_destroy_then_provision_configure_start(self, provider):
832
+ spec = _make_spec()
833
+ old_pr = _provision_result("my-agent")
834
+ old_pr["team_name"] = "my-team"
835
+
836
+ new_pr = {
837
+ "container_id": "new-id",
838
+ "container_name": "my-agent",
839
+ "host": "my-agent",
840
+ "volume_name": "skcapstone-agent-my-agent",
841
+ "team_name": "my-team",
842
+ }
843
+
844
+ with (
845
+ patch.object(provider, "destroy", return_value=True) as mock_destroy,
846
+ patch.object(provider, "provision", return_value=new_pr) as mock_provision,
847
+ patch.object(provider, "configure", return_value=True) as mock_configure,
848
+ patch.object(provider, "start", return_value=True) as mock_start,
849
+ ):
850
+ result = provider.rotate("my-agent", spec, old_pr)
851
+
852
+ mock_destroy.assert_called_once_with("my-agent", old_pr)
853
+ mock_provision.assert_called_once_with("my-agent", spec, "my-team")
854
+ mock_configure.assert_called_once_with("my-agent", spec, new_pr)
855
+ mock_start.assert_called_once_with("my-agent", new_pr)
856
+ assert result == new_pr
857
+
858
+ def test_rotate_preserves_team_name(self, provider):
859
+ spec = _make_spec()
860
+ old_pr = _provision_result("agent-x")
861
+ old_pr["team_name"] = "research-team"
862
+
863
+ captured: dict = {}
864
+
865
+ def fake_provision(name, s, team):
866
+ captured["team"] = team
867
+ return {**old_pr, "container_id": "new-id"}
868
+
869
+ with (
870
+ patch.object(provider, "destroy", return_value=True),
871
+ patch.object(provider, "provision", side_effect=fake_provision),
872
+ patch.object(provider, "configure", return_value=True),
873
+ patch.object(provider, "start", return_value=True),
874
+ ):
875
+ provider.rotate("agent-x", spec, old_pr)
876
+
877
+ assert captured["team"] == "research-team"
878
+
879
+ def test_rotate_returns_new_provision_result(self, provider):
880
+ spec = _make_spec()
881
+ old_pr = _provision_result("agent-z")
882
+ old_pr["team_name"] = "t"
883
+ new_pr = {**old_pr, "container_id": "brand-new"}
884
+
885
+ with (
886
+ patch.object(provider, "destroy", return_value=True),
887
+ patch.object(provider, "provision", return_value=new_pr),
888
+ patch.object(provider, "configure", return_value=True),
889
+ patch.object(provider, "start", return_value=True),
890
+ ):
891
+ result = provider.rotate("agent-z", spec, old_pr)
892
+
893
+ assert result["container_id"] == "brand-new"
894
+
895
+
896
+ # ---------------------------------------------------------------------------
897
+ # generate_compose() — MCP service + SKComm volume
898
+ # ---------------------------------------------------------------------------
899
+
900
+
901
+ class TestGenerateComposeSovereignExtensions:
902
+ """Tests for SKComm/MCP extensions in generate_compose()."""
903
+
904
+ def test_mcp_service_added_when_requested(self, provider):
905
+ bp = _make_blueprint(agent_count=1)
906
+ output = provider.generate_compose(bp, include_mcp_service=True)
907
+ parsed = yaml.safe_load(output)
908
+ assert "skcapstone-mcp" in parsed["services"]
909
+
910
+ def test_agents_depend_on_mcp_service_when_included(self, provider):
911
+ bp = _make_blueprint(agent_count=1)
912
+ output = provider.generate_compose(bp, include_mcp_service=True)
913
+ parsed = yaml.safe_load(output)
914
+ agent_svcs = [k for k in parsed["services"] if k != "skcapstone-mcp"]
915
+ for svc_name in agent_svcs:
916
+ assert "skcapstone-mcp" in parsed["services"][svc_name].get(
917
+ "depends_on", []
918
+ )
919
+
920
+ def test_mcp_host_env_set_on_agents_when_mcp_service_included(self, provider):
921
+ bp = _make_blueprint(agent_count=1)
922
+ output = provider.generate_compose(bp, include_mcp_service=True)
923
+ parsed = yaml.safe_load(output)
924
+ agent_svcs = [k for k in parsed["services"] if k != "skcapstone-mcp"]
925
+ for svc_name in agent_svcs:
926
+ env = parsed["services"][svc_name]["environment"]
927
+ assert "SKCAPSTONE_MCP_HOST" in env
928
+
929
+ def test_no_mcp_service_by_default(self, provider):
930
+ bp = _make_blueprint(agent_count=1)
931
+ output = provider.generate_compose(bp)
932
+ parsed = yaml.safe_load(output)
933
+ assert "skcapstone-mcp" not in parsed["services"]
934
+
935
+ def test_skcomm_volume_in_compose_when_configured(self, tmp_path):
936
+ skcomm_dir = tmp_path / "skcomm"
937
+ skcomm_dir.mkdir()
938
+ provider = DockerProvider(
939
+ base_image="python:3.12-slim",
940
+ network_name="skcapstone",
941
+ skcomm_home=str(skcomm_dir),
942
+ )
943
+ bp = _make_blueprint(agent_count=1)
944
+ output = provider.generate_compose(bp)
945
+ parsed = yaml.safe_load(output)
946
+ assert "skcomm-data" in parsed.get("volumes", {})
947
+
948
+ def test_skcomm_env_on_agents_when_configured(self, tmp_path):
949
+ skcomm_dir = tmp_path / "skcomm"
950
+ skcomm_dir.mkdir()
951
+ provider = DockerProvider(
952
+ base_image="python:3.12-slim",
953
+ network_name="skcapstone",
954
+ skcomm_home=str(skcomm_dir),
955
+ )
956
+ bp = _make_blueprint(agent_count=1)
957
+ output = provider.generate_compose(bp)
958
+ parsed = yaml.safe_load(output)
959
+ for svc in parsed["services"].values():
960
+ assert svc["environment"].get("SKCOMM_HOME") == "/skcomm"
961
+
962
+ def test_mcp_service_volume_included(self, provider):
963
+ bp = _make_blueprint(agent_count=1)
964
+ output = provider.generate_compose(bp, include_mcp_service=True)
965
+ parsed = yaml.safe_load(output)
966
+ assert "skcapstone-mcp-data" in parsed["volumes"]