@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,1061 @@
1
+ """
2
+ Cloud Provider — deploy agents on Hetzner, AWS, GCP, or any cloud.
3
+
4
+ This is the abstraction layer that makes blueprints truly portable.
5
+ Each cloud gets a thin adapter; the provider interface stays the same.
6
+
7
+ Currently supports:
8
+ - Hetzner Cloud (via hcloud API)
9
+ - AWS EC2 (via boto3)
10
+ - GCP Compute (via google-cloud-compute)
11
+
12
+ The pattern: provision a VM/container, install the agent runtime,
13
+ configure via cloud-init or SSH, register on the Tailscale mesh.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ import os
21
+ import time
22
+ from typing import Any, Dict, Optional
23
+
24
+ from ..blueprints.schema import AgentSpec, ProviderType
25
+ from ..team_engine import AgentStatus, ProviderBackend
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Cloud adapter registry
32
+ # ---------------------------------------------------------------------------
33
+
34
+ _CLOUD_ADAPTERS: Dict[str, type] = {}
35
+
36
+
37
+ def register_cloud_adapter(name: str):
38
+ """Decorator to register a cloud adapter class.
39
+
40
+ Args:
41
+ name: Cloud provider name (e.g. 'hetzner', 'aws', 'gcp').
42
+ """
43
+ def wrapper(cls):
44
+ _CLOUD_ADAPTERS[name] = cls
45
+ return cls
46
+ return wrapper
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Cloud Provider
51
+ # ---------------------------------------------------------------------------
52
+
53
+ class CloudProvider(ProviderBackend):
54
+ """Generic cloud provider that delegates to cloud-specific adapters.
55
+
56
+ Args:
57
+ cloud: Which cloud to use ('hetzner', 'aws', 'gcp').
58
+ config: Cloud-specific configuration dict.
59
+ """
60
+
61
+ _CLOUD_TO_PROVIDER_TYPE = {
62
+ "hetzner": ProviderType.HETZNER,
63
+ "aws": ProviderType.AWS,
64
+ "gcp": ProviderType.GCP,
65
+ }
66
+
67
+ @property
68
+ def provider_type(self) -> ProviderType:
69
+ """Return the ProviderType matching the selected cloud adapter."""
70
+ return self._CLOUD_TO_PROVIDER_TYPE.get(self._cloud, ProviderType.HETZNER)
71
+
72
+ def __init__(
73
+ self,
74
+ cloud: str = "hetzner",
75
+ config: Optional[Dict[str, Any]] = None,
76
+ ) -> None:
77
+ self._cloud = cloud
78
+ self._config = config or {}
79
+ self._adapter = self._get_adapter()
80
+
81
+ def _get_adapter(self) -> Any:
82
+ """Instantiate the cloud-specific adapter.
83
+
84
+ Returns:
85
+ Cloud adapter instance.
86
+
87
+ Raises:
88
+ RuntimeError: If the cloud adapter is not available.
89
+ """
90
+ adapter_cls = _CLOUD_ADAPTERS.get(self._cloud)
91
+ if adapter_cls:
92
+ return adapter_cls(**self._config)
93
+
94
+ if self._cloud == "hetzner":
95
+ return HetznerAdapter(**self._config)
96
+ elif self._cloud == "aws":
97
+ return AWSAdapter(**self._config)
98
+ elif self._cloud == "gcp":
99
+ return GCPAdapter(**self._config)
100
+ else:
101
+ raise RuntimeError(f"Unknown cloud provider: {self._cloud}")
102
+
103
+ def provision(
104
+ self, agent_name: str, spec: AgentSpec, team_name: str,
105
+ ) -> Dict[str, Any]:
106
+ """Delegate to cloud adapter."""
107
+ return self._adapter.provision(agent_name, spec, team_name)
108
+
109
+ def configure(
110
+ self, agent_name: str, spec: AgentSpec, provision_result: Dict[str, Any],
111
+ ) -> bool:
112
+ """Delegate to cloud adapter."""
113
+ return self._adapter.configure(agent_name, spec, provision_result)
114
+
115
+ def start(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
116
+ """Delegate to cloud adapter."""
117
+ return self._adapter.start(agent_name, provision_result)
118
+
119
+ def stop(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
120
+ """Delegate to cloud adapter."""
121
+ return self._adapter.stop(agent_name, provision_result)
122
+
123
+ def destroy(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
124
+ """Delegate to cloud adapter."""
125
+ return self._adapter.destroy(agent_name, provision_result)
126
+
127
+ def health_check(
128
+ self, agent_name: str, provision_result: Dict[str, Any],
129
+ ) -> AgentStatus:
130
+ """Delegate to cloud adapter."""
131
+ return self._adapter.health_check(agent_name, provision_result)
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Hetzner Adapter
136
+ # ---------------------------------------------------------------------------
137
+
138
+ def _memory_to_hetzner_type(memory_str: str, cores: int) -> str:
139
+ """Map resource spec to closest Hetzner server type.
140
+
141
+ Args:
142
+ memory_str: Memory allocation string (e.g. '4g').
143
+ cores: Number of CPU cores.
144
+
145
+ Returns:
146
+ Hetzner server type name (e.g. 'cx22', 'cx32').
147
+ """
148
+ mem_str = memory_str.strip().lower()
149
+ if mem_str.endswith("g"):
150
+ mem_gb = float(mem_str[:-1])
151
+ elif mem_str.endswith("m"):
152
+ mem_gb = float(mem_str[:-1]) / 1024
153
+ else:
154
+ mem_gb = float(mem_str) / 1024
155
+
156
+ # Hetzner CX line: cx22=4GB/2c, cx32=8GB/4c, cx42=16GB/8c, cx52=32GB/16c
157
+ if mem_gb <= 2 and cores <= 2:
158
+ return "cx22"
159
+ elif mem_gb <= 4 and cores <= 2:
160
+ return "cx22"
161
+ elif mem_gb <= 8 and cores <= 4:
162
+ return "cx32"
163
+ elif mem_gb <= 16 and cores <= 8:
164
+ return "cx42"
165
+ else:
166
+ return "cx52"
167
+
168
+
169
+ @register_cloud_adapter("hetzner")
170
+ class HetznerAdapter:
171
+ """Hetzner Cloud adapter using the hcloud API.
172
+
173
+ Expects HETZNER_API_TOKEN environment variable or token in config.
174
+ """
175
+
176
+ def __init__(self, api_token: Optional[str] = None, **kwargs: Any) -> None:
177
+ self._token = api_token or os.environ.get("HETZNER_API_TOKEN", "")
178
+
179
+ def _api_call(
180
+ self,
181
+ method: str,
182
+ endpoint: str,
183
+ data: Optional[Dict] = None,
184
+ ) -> Dict[str, Any]:
185
+ """Make an authenticated Hetzner API call.
186
+
187
+ Args:
188
+ method: HTTP method.
189
+ endpoint: API endpoint.
190
+ data: Request body.
191
+
192
+ Returns:
193
+ Parsed JSON response.
194
+
195
+ Raises:
196
+ RuntimeError: On API failure.
197
+ """
198
+ try:
199
+ import requests
200
+ except ImportError:
201
+ raise RuntimeError(
202
+ "Hetzner adapter requires 'requests': pip install requests"
203
+ )
204
+
205
+ if not self._token:
206
+ raise RuntimeError(
207
+ "Hetzner not configured. Set HETZNER_API_TOKEN."
208
+ )
209
+
210
+ url = f"https://api.hetzner.cloud/v1{endpoint}"
211
+ headers = {
212
+ "Authorization": f"Bearer {self._token}",
213
+ "Content-Type": "application/json",
214
+ }
215
+
216
+ resp = requests.request(
217
+ method, url, headers=headers, json=data, timeout=30,
218
+ )
219
+
220
+ if resp.status_code >= 400:
221
+ raise RuntimeError(
222
+ f"Hetzner API {method} {endpoint}: "
223
+ f"{resp.status_code} {resp.text}"
224
+ )
225
+
226
+ return resp.json()
227
+
228
+ def provision(
229
+ self, agent_name: str, spec: AgentSpec, team_name: str,
230
+ ) -> Dict[str, Any]:
231
+ """Create a Hetzner Cloud server.
232
+
233
+ Args:
234
+ agent_name: Agent instance name.
235
+ spec: Agent specification.
236
+ team_name: Parent team name.
237
+
238
+ Returns:
239
+ Dict with server details.
240
+ """
241
+ server_type = _memory_to_hetzner_type(
242
+ spec.resources.memory, spec.resources.cores,
243
+ )
244
+
245
+ cloud_init = _build_cloud_init(agent_name, spec)
246
+
247
+ create_data = {
248
+ "name": agent_name.replace("_", "-")[:63],
249
+ "server_type": server_type,
250
+ "image": "debian-12",
251
+ "location": "fsn1",
252
+ "start_after_create": True,
253
+ "user_data": cloud_init,
254
+ "labels": {
255
+ "team": team_name.replace(" ", "-").lower()[:63],
256
+ "agent": agent_name[:63],
257
+ "role": spec.role.value,
258
+ "managed-by": "skcapstone",
259
+ },
260
+ }
261
+
262
+ logger.info(
263
+ "Creating Hetzner server %s (type=%s)",
264
+ agent_name, server_type,
265
+ )
266
+
267
+ result = self._api_call("POST", "/servers", data=create_data)
268
+ server = result.get("server", {})
269
+
270
+ return {
271
+ "server_id": server.get("id"),
272
+ "host": server.get("public_net", {}).get("ipv4", {}).get("ip", ""),
273
+ "container_id": str(server.get("id", "")),
274
+ }
275
+
276
+ def configure(
277
+ self, agent_name: str, spec: AgentSpec, provision_result: Dict[str, Any],
278
+ ) -> bool:
279
+ """Cloud-init handles configuration at boot."""
280
+ return True
281
+
282
+ def start(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
283
+ """Power on the server."""
284
+ server_id = provision_result.get("server_id")
285
+ if not server_id:
286
+ return False
287
+ try:
288
+ self._api_call("POST", f"/servers/{server_id}/actions/poweron")
289
+ return True
290
+ except RuntimeError:
291
+ return False
292
+
293
+ def stop(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
294
+ """Power off the server."""
295
+ server_id = provision_result.get("server_id")
296
+ if not server_id:
297
+ return False
298
+ try:
299
+ self._api_call("POST", f"/servers/{server_id}/actions/poweroff")
300
+ return True
301
+ except RuntimeError:
302
+ return False
303
+
304
+ def destroy(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
305
+ """Delete the server."""
306
+ server_id = provision_result.get("server_id")
307
+ if not server_id:
308
+ return False
309
+ try:
310
+ self._api_call("DELETE", f"/servers/{server_id}")
311
+ return True
312
+ except RuntimeError as exc:
313
+ logger.error("Failed to destroy Hetzner server %s: %s", server_id, exc)
314
+ return False
315
+
316
+ def health_check(
317
+ self, agent_name: str, provision_result: Dict[str, Any],
318
+ ) -> AgentStatus:
319
+ """Check server status via API."""
320
+ server_id = provision_result.get("server_id")
321
+ if not server_id:
322
+ return AgentStatus.STOPPED
323
+ try:
324
+ result = self._api_call("GET", f"/servers/{server_id}")
325
+ status = result.get("server", {}).get("status", "off")
326
+ if status == "running":
327
+ return AgentStatus.RUNNING
328
+ elif status in ("off", "deleting"):
329
+ return AgentStatus.STOPPED
330
+ else:
331
+ return AgentStatus.DEGRADED
332
+ except RuntimeError:
333
+ return AgentStatus.FAILED
334
+
335
+
336
+ # ---------------------------------------------------------------------------
337
+ # Cloud-init template
338
+ # ---------------------------------------------------------------------------
339
+
340
+ def _build_cloud_init(
341
+ agent_name: str,
342
+ spec: AgentSpec,
343
+ tailscale_authkey: Optional[str] = None,
344
+ ) -> str:
345
+ """Generate cloud-init user data to bootstrap an agent VM.
346
+
347
+ Args:
348
+ agent_name: Agent instance name.
349
+ spec: Agent specification.
350
+ tailscale_authkey: Optional Tailscale auth key for mesh auto-join.
351
+ Falls back to TAILSCALE_AUTHKEY environment variable at build time.
352
+
353
+ Returns:
354
+ Cloud-init YAML string.
355
+ """
356
+ ts_key = tailscale_authkey or os.environ.get("TAILSCALE_AUTHKEY", "")
357
+ ts_join = ""
358
+ if ts_key:
359
+ ts_join = f' - tailscale up --authkey="{ts_key}" --hostname="{agent_name}"\n'
360
+
361
+ return f"""#cloud-config
362
+ package_update: true
363
+ packages:
364
+ - python3
365
+ - python3-pip
366
+ - python3-venv
367
+ - curl
368
+ - gnupg
369
+
370
+ runcmd:
371
+ - pip3 install skcapstone
372
+ - mkdir -p /opt/agent
373
+ - |
374
+ cat > /opt/agent/config.json << 'AGENT_EOF'
375
+ {{
376
+ "agent_name": "{agent_name}",
377
+ "role": "{spec.role.value}",
378
+ "model": "{spec.model_name or spec.model.value}",
379
+ "skills": {json.dumps(spec.skills)}
380
+ }}
381
+ AGENT_EOF
382
+ - curl -fsSL https://tailscale.com/install.sh | sh
383
+ {ts_join} - echo "Agent {agent_name} provisioned by skcapstone"
384
+ """
385
+
386
+
387
+ # ---------------------------------------------------------------------------
388
+ # AWS EC2 Adapter
389
+ # ---------------------------------------------------------------------------
390
+
391
+ def _memory_to_ec2_instance_type(memory_str: str, cores: int) -> str:
392
+ """Map resource spec to closest AWS EC2 instance type.
393
+
394
+ Args:
395
+ memory_str: Memory allocation string (e.g. '4g').
396
+ cores: Number of CPU cores.
397
+
398
+ Returns:
399
+ EC2 instance type name (e.g. 't3.small', 't3.medium').
400
+ """
401
+ mem_str = memory_str.strip().lower()
402
+ if mem_str.endswith("g"):
403
+ mem_gb = float(mem_str[:-1])
404
+ elif mem_str.endswith("m"):
405
+ mem_gb = float(mem_str[:-1]) / 1024
406
+ else:
407
+ mem_gb = float(mem_str) / 1024
408
+
409
+ # t3 line: micro=1GB/2c, small=2GB/2c, medium=4GB/2c,
410
+ # large=8GB/2c, xlarge=16GB/4c, 2xlarge=32GB/8c
411
+ if mem_gb <= 1 and cores <= 2:
412
+ return "t3.micro"
413
+ elif mem_gb <= 2 and cores <= 2:
414
+ return "t3.small"
415
+ elif mem_gb <= 4 and cores <= 2:
416
+ return "t3.medium"
417
+ elif mem_gb <= 8 and cores <= 2:
418
+ return "t3.large"
419
+ elif mem_gb <= 16 and cores <= 4:
420
+ return "t3.xlarge"
421
+ else:
422
+ return "t3.2xlarge"
423
+
424
+
425
+ @register_cloud_adapter("aws")
426
+ class AWSAdapter:
427
+ """AWS EC2 adapter using boto3.
428
+
429
+ Launches EC2 instances with cloud-init user data for agent bootstrap
430
+ and Tailscale mesh auto-join.
431
+
432
+ Expects AWS credentials via environment variables, ~/.aws/credentials,
433
+ or IAM instance profile:
434
+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION
435
+
436
+ Args:
437
+ region: AWS region (e.g. 'us-east-1'). Falls back to
438
+ AWS_DEFAULT_REGION.
439
+ ami_id: Base AMI ID. Defaults to Debian 12 lookup via SSM.
440
+ security_group_id: Security group for instances. If not provided,
441
+ uses the VPC default.
442
+ subnet_id: Subnet to launch into. If not provided, uses default.
443
+ key_name: EC2 key pair name for SSH access (optional).
444
+ """
445
+
446
+ # Default Debian 12 AMIs per region (amd64, hvm, ebs).
447
+ _DEFAULT_AMIS = {
448
+ "us-east-1": "ami-0fec2c2e2017f4e7b",
449
+ "us-west-2": "ami-0b6edd8449255b799",
450
+ "eu-central-1": "ami-042e6fdb154c830c5",
451
+ "eu-west-1": "ami-0694d931cee176e7d",
452
+ }
453
+
454
+ def __init__(
455
+ self,
456
+ region: Optional[str] = None,
457
+ ami_id: Optional[str] = None,
458
+ security_group_id: Optional[str] = None,
459
+ subnet_id: Optional[str] = None,
460
+ key_name: Optional[str] = None,
461
+ **kwargs: Any,
462
+ ) -> None:
463
+ self._region = region or os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
464
+ self._ami_id = ami_id
465
+ self._security_group_id = security_group_id
466
+ self._subnet_id = subnet_id
467
+ self._key_name = key_name
468
+
469
+ def _ec2_client(self) -> Any:
470
+ """Create a boto3 EC2 client.
471
+
472
+ Returns:
473
+ boto3 EC2 client.
474
+
475
+ Raises:
476
+ RuntimeError: If boto3 is not installed.
477
+ """
478
+ try:
479
+ import boto3
480
+ except ImportError:
481
+ raise RuntimeError(
482
+ "AWS adapter requires boto3: pip install boto3"
483
+ )
484
+ return boto3.client("ec2", region_name=self._region)
485
+
486
+ def _resolve_ami(self) -> str:
487
+ """Return the AMI ID for the target region.
488
+
489
+ Uses the explicit ami_id if configured, otherwise falls back to a
490
+ built-in map of Debian 12 AMIs.
491
+
492
+ Returns:
493
+ AMI ID string.
494
+
495
+ Raises:
496
+ RuntimeError: If no AMI can be resolved for the region.
497
+ """
498
+ if self._ami_id:
499
+ return self._ami_id
500
+ ami = self._DEFAULT_AMIS.get(self._region)
501
+ if ami:
502
+ return ami
503
+ raise RuntimeError(
504
+ f"No default AMI for region {self._region}. "
505
+ "Pass ami_id= explicitly."
506
+ )
507
+
508
+ def provision(
509
+ self, agent_name: str, spec: AgentSpec, team_name: str,
510
+ ) -> Dict[str, Any]:
511
+ """Launch an EC2 instance.
512
+
513
+ Args:
514
+ agent_name: Agent instance name.
515
+ spec: Agent specification.
516
+ team_name: Parent team name.
517
+
518
+ Returns:
519
+ Dict with instance_id, host (public IP), container_id.
520
+ """
521
+ ec2 = self._ec2_client()
522
+ instance_type = _memory_to_ec2_instance_type(
523
+ spec.resources.memory, spec.resources.cores,
524
+ )
525
+ ami = self._resolve_ami()
526
+ cloud_init = _build_cloud_init(agent_name, spec)
527
+
528
+ run_kwargs: Dict[str, Any] = {
529
+ "ImageId": ami,
530
+ "InstanceType": instance_type,
531
+ "MinCount": 1,
532
+ "MaxCount": 1,
533
+ "UserData": cloud_init,
534
+ "TagSpecifications": [
535
+ {
536
+ "ResourceType": "instance",
537
+ "Tags": [
538
+ {"Key": "Name", "Value": agent_name[:255]},
539
+ {"Key": "Team", "Value": team_name[:255]},
540
+ {"Key": "Role", "Value": spec.role.value},
541
+ {"Key": "ManagedBy", "Value": "skcapstone"},
542
+ ],
543
+ }
544
+ ],
545
+ }
546
+ if self._key_name:
547
+ run_kwargs["KeyName"] = self._key_name
548
+ if self._security_group_id:
549
+ run_kwargs["SecurityGroupIds"] = [self._security_group_id]
550
+ if self._subnet_id:
551
+ run_kwargs["SubnetId"] = self._subnet_id
552
+
553
+ logger.info(
554
+ "Launching EC2 instance %s (type=%s ami=%s region=%s)",
555
+ agent_name, instance_type, ami, self._region,
556
+ )
557
+
558
+ result = ec2.run_instances(**run_kwargs)
559
+ instance = result["Instances"][0]
560
+ instance_id = instance["InstanceId"]
561
+
562
+ # Wait briefly for public IP assignment.
563
+ public_ip = instance.get("PublicIpAddress", "")
564
+ if not public_ip:
565
+ try:
566
+ waiter = ec2.get_waiter("instance_running")
567
+ waiter.wait(
568
+ InstanceIds=[instance_id],
569
+ WaiterConfig={"Delay": 5, "MaxAttempts": 12},
570
+ )
571
+ desc = ec2.describe_instances(InstanceIds=[instance_id])
572
+ reservations = desc.get("Reservations", [])
573
+ if reservations:
574
+ inst = reservations[0]["Instances"][0]
575
+ public_ip = inst.get("PublicIpAddress", "")
576
+ except Exception as exc:
577
+ logger.warning("Could not get public IP for %s: %s", instance_id, exc)
578
+
579
+ return {
580
+ "instance_id": instance_id,
581
+ "host": public_ip,
582
+ "container_id": instance_id,
583
+ "region": self._region,
584
+ }
585
+
586
+ def configure(
587
+ self, agent_name: str, spec: AgentSpec, provision_result: Dict[str, Any],
588
+ ) -> bool:
589
+ """Cloud-init handles configuration at boot."""
590
+ return True
591
+
592
+ def start(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
593
+ """Start a stopped EC2 instance.
594
+
595
+ Args:
596
+ agent_name: Agent instance name.
597
+ provision_result: Output from provision().
598
+
599
+ Returns:
600
+ True if the start request succeeded.
601
+ """
602
+ instance_id = provision_result.get("instance_id")
603
+ if not instance_id:
604
+ return False
605
+ try:
606
+ ec2 = self._ec2_client()
607
+ ec2.start_instances(InstanceIds=[instance_id])
608
+ return True
609
+ except Exception as exc:
610
+ logger.error("Failed to start EC2 %s: %s", instance_id, exc)
611
+ return False
612
+
613
+ def stop(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
614
+ """Stop a running EC2 instance.
615
+
616
+ Args:
617
+ agent_name: Agent instance name.
618
+ provision_result: Output from provision().
619
+
620
+ Returns:
621
+ True if the stop request succeeded.
622
+ """
623
+ instance_id = provision_result.get("instance_id")
624
+ if not instance_id:
625
+ return False
626
+ try:
627
+ ec2 = self._ec2_client()
628
+ ec2.stop_instances(InstanceIds=[instance_id])
629
+ return True
630
+ except Exception as exc:
631
+ logger.error("Failed to stop EC2 %s: %s", instance_id, exc)
632
+ return False
633
+
634
+ def destroy(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
635
+ """Terminate an EC2 instance.
636
+
637
+ Args:
638
+ agent_name: Agent instance name.
639
+ provision_result: Output from provision().
640
+
641
+ Returns:
642
+ True if the termination request succeeded.
643
+ """
644
+ instance_id = provision_result.get("instance_id")
645
+ if not instance_id:
646
+ return False
647
+ try:
648
+ ec2 = self._ec2_client()
649
+ ec2.terminate_instances(InstanceIds=[instance_id])
650
+ logger.info("Terminated EC2 instance %s", instance_id)
651
+ return True
652
+ except Exception as exc:
653
+ logger.error("Failed to terminate EC2 %s: %s", instance_id, exc)
654
+ return False
655
+
656
+ def health_check(
657
+ self, agent_name: str, provision_result: Dict[str, Any],
658
+ ) -> AgentStatus:
659
+ """Check EC2 instance status.
660
+
661
+ Args:
662
+ agent_name: Agent instance name.
663
+ provision_result: Output from provision().
664
+
665
+ Returns:
666
+ AgentStatus based on instance state.
667
+ """
668
+ instance_id = provision_result.get("instance_id")
669
+ if not instance_id:
670
+ return AgentStatus.STOPPED
671
+ try:
672
+ ec2 = self._ec2_client()
673
+ desc = ec2.describe_instances(InstanceIds=[instance_id])
674
+ reservations = desc.get("Reservations", [])
675
+ if not reservations:
676
+ return AgentStatus.STOPPED
677
+ state = reservations[0]["Instances"][0]["State"]["Name"]
678
+ if state == "running":
679
+ return AgentStatus.RUNNING
680
+ elif state in ("stopped", "terminated"):
681
+ return AgentStatus.STOPPED
682
+ elif state in ("pending", "stopping", "shutting-down"):
683
+ return AgentStatus.DEGRADED
684
+ else:
685
+ return AgentStatus.DEGRADED
686
+ except Exception:
687
+ return AgentStatus.FAILED
688
+
689
+
690
+ # ---------------------------------------------------------------------------
691
+ # GCP Compute Adapter
692
+ # ---------------------------------------------------------------------------
693
+
694
+ def _memory_to_gcp_machine_type(memory_str: str, cores: int) -> str:
695
+ """Map resource spec to closest GCP machine type.
696
+
697
+ Args:
698
+ memory_str: Memory allocation string (e.g. '4g').
699
+ cores: Number of CPU cores.
700
+
701
+ Returns:
702
+ GCP machine type name (e.g. 'e2-small', 'e2-medium').
703
+ """
704
+ mem_str = memory_str.strip().lower()
705
+ if mem_str.endswith("g"):
706
+ mem_gb = float(mem_str[:-1])
707
+ elif mem_str.endswith("m"):
708
+ mem_gb = float(mem_str[:-1]) / 1024
709
+ else:
710
+ mem_gb = float(mem_str) / 1024
711
+
712
+ # e2 line: micro=1GB/0.25c, small=2GB/0.5c, medium=4GB/1c,
713
+ # standard-2=8GB/2c, standard-4=16GB/4c, standard-8=32GB/8c
714
+ if mem_gb <= 1 and cores <= 1:
715
+ return "e2-micro"
716
+ elif mem_gb <= 2 and cores <= 1:
717
+ return "e2-small"
718
+ elif mem_gb <= 4 and cores <= 2:
719
+ return "e2-medium"
720
+ elif mem_gb <= 8 and cores <= 2:
721
+ return "e2-standard-2"
722
+ elif mem_gb <= 16 and cores <= 4:
723
+ return "e2-standard-4"
724
+ else:
725
+ return "e2-standard-8"
726
+
727
+
728
+ @register_cloud_adapter("gcp")
729
+ class GCPAdapter:
730
+ """GCP Compute Engine adapter using the google-cloud-compute library.
731
+
732
+ Launches Compute Engine instances with startup-script metadata for
733
+ agent bootstrap and Tailscale mesh auto-join.
734
+
735
+ Expects GCP credentials via:
736
+ GOOGLE_APPLICATION_CREDENTIALS (service account JSON) or
737
+ Application Default Credentials (gcloud auth application-default login).
738
+
739
+ Args:
740
+ project: GCP project ID. Falls back to GOOGLE_CLOUD_PROJECT or
741
+ GCLOUD_PROJECT.
742
+ zone: Compute zone (e.g. 'us-central1-a'). Falls back to
743
+ CLOUDSDK_COMPUTE_ZONE.
744
+ network: VPC network name (default: 'default').
745
+ subnet: Subnetwork name (optional).
746
+ service_account_email: Service account for the instance (optional).
747
+ """
748
+
749
+ # Debian 12 image family on GCP.
750
+ _IMAGE_PROJECT = "debian-cloud"
751
+ _IMAGE_FAMILY = "debian-12"
752
+
753
+ def __init__(
754
+ self,
755
+ project: Optional[str] = None,
756
+ zone: Optional[str] = None,
757
+ network: str = "default",
758
+ subnet: Optional[str] = None,
759
+ service_account_email: Optional[str] = None,
760
+ **kwargs: Any,
761
+ ) -> None:
762
+ self._project = (
763
+ project
764
+ or os.environ.get("GOOGLE_CLOUD_PROJECT")
765
+ or os.environ.get("GCLOUD_PROJECT", "")
766
+ )
767
+ self._zone = zone or os.environ.get("CLOUDSDK_COMPUTE_ZONE", "us-central1-a")
768
+ self._network = network
769
+ self._subnet = subnet
770
+ self._service_account_email = service_account_email
771
+
772
+ def _compute_client(self) -> Any:
773
+ """Create a GCP Compute instances client.
774
+
775
+ Returns:
776
+ google.cloud.compute_v1.InstancesClient.
777
+
778
+ Raises:
779
+ RuntimeError: If google-cloud-compute is not installed.
780
+ """
781
+ try:
782
+ from google.cloud import compute_v1
783
+ except ImportError:
784
+ raise RuntimeError(
785
+ "GCP adapter requires google-cloud-compute: "
786
+ "pip install google-cloud-compute"
787
+ )
788
+ return compute_v1.InstancesClient()
789
+
790
+ def _get_source_image(self) -> str:
791
+ """Resolve the latest Debian 12 image URI from GCP.
792
+
793
+ Returns:
794
+ Fully-qualified image self-link.
795
+
796
+ Raises:
797
+ RuntimeError: If the image cannot be resolved.
798
+ """
799
+ try:
800
+ from google.cloud import compute_v1
801
+
802
+ images_client = compute_v1.ImagesClient()
803
+ image = images_client.get_from_family(
804
+ project=self._IMAGE_PROJECT, family=self._IMAGE_FAMILY,
805
+ )
806
+ return image.self_link
807
+ except ImportError:
808
+ # Fallback to well-known URI pattern.
809
+ return (
810
+ f"projects/{self._IMAGE_PROJECT}/global/images/family/"
811
+ f"{self._IMAGE_FAMILY}"
812
+ )
813
+ except Exception as exc:
814
+ raise RuntimeError(f"Failed to resolve GCP image: {exc}")
815
+
816
+ def provision(
817
+ self, agent_name: str, spec: AgentSpec, team_name: str,
818
+ ) -> Dict[str, Any]:
819
+ """Create a GCP Compute Engine instance.
820
+
821
+ Args:
822
+ agent_name: Agent instance name.
823
+ spec: Agent specification.
824
+ team_name: Parent team name.
825
+
826
+ Returns:
827
+ Dict with instance_name, zone, host (external IP).
828
+ """
829
+ try:
830
+ from google.cloud import compute_v1
831
+ except ImportError:
832
+ raise RuntimeError(
833
+ "GCP adapter requires google-cloud-compute: "
834
+ "pip install google-cloud-compute"
835
+ )
836
+
837
+ if not self._project:
838
+ raise RuntimeError(
839
+ "GCP project not configured. Set GOOGLE_CLOUD_PROJECT "
840
+ "or pass project= to GCPAdapter."
841
+ )
842
+
843
+ machine_type = _memory_to_gcp_machine_type(
844
+ spec.resources.memory, spec.resources.cores,
845
+ )
846
+ instance_name = agent_name.replace("_", "-").lower()[:63]
847
+ cloud_init = _build_cloud_init(agent_name, spec)
848
+ source_image = self._get_source_image()
849
+
850
+ # Build instance resource.
851
+ instance = compute_v1.Instance()
852
+ instance.name = instance_name
853
+ instance.machine_type = (
854
+ f"zones/{self._zone}/machineTypes/{machine_type}"
855
+ )
856
+
857
+ # Boot disk.
858
+ disk = compute_v1.AttachedDisk()
859
+ disk.auto_delete = True
860
+ disk.boot = True
861
+ init_params = compute_v1.AttachedDiskInitializeParams()
862
+ init_params.source_image = source_image
863
+ init_params.disk_size_gb = 20
864
+ disk.initialize_params = init_params
865
+ instance.disks = [disk]
866
+
867
+ # Network.
868
+ net_iface = compute_v1.NetworkInterface()
869
+ net_iface.network = f"global/networks/{self._network}"
870
+ if self._subnet:
871
+ net_iface.subnetwork = (
872
+ f"regions/{self._zone.rsplit('-', 1)[0]}/subnetworks/{self._subnet}"
873
+ )
874
+ # External IP for SSH/Tailscale.
875
+ access_config = compute_v1.AccessConfig()
876
+ access_config.name = "External NAT"
877
+ access_config.type_ = "ONE_TO_ONE_NAT"
878
+ net_iface.access_configs = [access_config]
879
+ instance.network_interfaces = [net_iface]
880
+
881
+ # Startup script (cloud-init equivalent).
882
+ instance.metadata = compute_v1.Metadata()
883
+ instance.metadata.items = [
884
+ compute_v1.Items(key="startup-script", value=cloud_init),
885
+ ]
886
+
887
+ # Labels.
888
+ instance.labels = {
889
+ "team": team_name.replace(" ", "-").lower()[:63],
890
+ "agent": instance_name[:63],
891
+ "role": spec.role.value,
892
+ "managed-by": "skcapstone",
893
+ }
894
+
895
+ # Service account.
896
+ if self._service_account_email:
897
+ sa = compute_v1.ServiceAccount()
898
+ sa.email = self._service_account_email
899
+ sa.scopes = ["https://www.googleapis.com/auth/cloud-platform"]
900
+ instance.service_accounts = [sa]
901
+
902
+ logger.info(
903
+ "Creating GCP instance %s (type=%s zone=%s project=%s)",
904
+ instance_name, machine_type, self._zone, self._project,
905
+ )
906
+
907
+ client = self._compute_client()
908
+ operation = client.insert(
909
+ project=self._project,
910
+ zone=self._zone,
911
+ instance_resource=instance,
912
+ )
913
+
914
+ # Wait for the operation to complete.
915
+ try:
916
+ operation.result(timeout=120)
917
+ except Exception as exc:
918
+ logger.warning("GCP insert wait: %s", exc)
919
+
920
+ # Fetch instance details for external IP.
921
+ host = ""
922
+ try:
923
+ inst = client.get(
924
+ project=self._project,
925
+ zone=self._zone,
926
+ instance=instance_name,
927
+ )
928
+ for iface in inst.network_interfaces:
929
+ for ac in iface.access_configs:
930
+ if ac.nat_i_p:
931
+ host = ac.nat_i_p
932
+ break
933
+ except Exception as exc:
934
+ logger.warning("Could not fetch GCP instance IP: %s", exc)
935
+
936
+ return {
937
+ "instance_name": instance_name,
938
+ "host": host,
939
+ "zone": self._zone,
940
+ "project": self._project,
941
+ "container_id": instance_name,
942
+ }
943
+
944
+ def configure(
945
+ self, agent_name: str, spec: AgentSpec, provision_result: Dict[str, Any],
946
+ ) -> bool:
947
+ """Startup script handles configuration at boot."""
948
+ return True
949
+
950
+ def start(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
951
+ """Start a stopped GCP instance.
952
+
953
+ Args:
954
+ agent_name: Agent instance name.
955
+ provision_result: Output from provision().
956
+
957
+ Returns:
958
+ True if the start request succeeded.
959
+ """
960
+ instance_name = provision_result.get("instance_name")
961
+ if not instance_name:
962
+ return False
963
+ try:
964
+ client = self._compute_client()
965
+ operation = client.start(
966
+ project=provision_result.get("project", self._project),
967
+ zone=provision_result.get("zone", self._zone),
968
+ instance=instance_name,
969
+ )
970
+ operation.result(timeout=60)
971
+ return True
972
+ except Exception as exc:
973
+ logger.error("Failed to start GCP instance %s: %s", instance_name, exc)
974
+ return False
975
+
976
+ def stop(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
977
+ """Stop a running GCP instance.
978
+
979
+ Args:
980
+ agent_name: Agent instance name.
981
+ provision_result: Output from provision().
982
+
983
+ Returns:
984
+ True if the stop request succeeded.
985
+ """
986
+ instance_name = provision_result.get("instance_name")
987
+ if not instance_name:
988
+ return False
989
+ try:
990
+ client = self._compute_client()
991
+ operation = client.stop(
992
+ project=provision_result.get("project", self._project),
993
+ zone=provision_result.get("zone", self._zone),
994
+ instance=instance_name,
995
+ )
996
+ operation.result(timeout=60)
997
+ return True
998
+ except Exception as exc:
999
+ logger.error("Failed to stop GCP instance %s: %s", instance_name, exc)
1000
+ return False
1001
+
1002
+ def destroy(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
1003
+ """Delete a GCP instance.
1004
+
1005
+ Args:
1006
+ agent_name: Agent instance name.
1007
+ provision_result: Output from provision().
1008
+
1009
+ Returns:
1010
+ True if the deletion request succeeded.
1011
+ """
1012
+ instance_name = provision_result.get("instance_name")
1013
+ if not instance_name:
1014
+ return False
1015
+ try:
1016
+ client = self._compute_client()
1017
+ operation = client.delete(
1018
+ project=provision_result.get("project", self._project),
1019
+ zone=provision_result.get("zone", self._zone),
1020
+ instance=instance_name,
1021
+ )
1022
+ operation.result(timeout=120)
1023
+ logger.info("Deleted GCP instance %s", instance_name)
1024
+ return True
1025
+ except Exception as exc:
1026
+ logger.error("Failed to delete GCP instance %s: %s", instance_name, exc)
1027
+ return False
1028
+
1029
+ def health_check(
1030
+ self, agent_name: str, provision_result: Dict[str, Any],
1031
+ ) -> AgentStatus:
1032
+ """Check GCP instance status.
1033
+
1034
+ Args:
1035
+ agent_name: Agent instance name.
1036
+ provision_result: Output from provision().
1037
+
1038
+ Returns:
1039
+ AgentStatus based on instance state.
1040
+ """
1041
+ instance_name = provision_result.get("instance_name")
1042
+ if not instance_name:
1043
+ return AgentStatus.STOPPED
1044
+ try:
1045
+ client = self._compute_client()
1046
+ inst = client.get(
1047
+ project=provision_result.get("project", self._project),
1048
+ zone=provision_result.get("zone", self._zone),
1049
+ instance=instance_name,
1050
+ )
1051
+ status = inst.status
1052
+ if status == "RUNNING":
1053
+ return AgentStatus.RUNNING
1054
+ elif status in ("TERMINATED", "STOPPED"):
1055
+ return AgentStatus.STOPPED
1056
+ elif status in ("STAGING", "STOPPING", "SUSPENDING", "SUSPENDED"):
1057
+ return AgentStatus.DEGRADED
1058
+ else:
1059
+ return AgentStatus.DEGRADED
1060
+ except Exception:
1061
+ return AgentStatus.FAILED