@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,942 @@
1
+ """
2
+ SKSecurity KMS — Sovereign Key Management Service.
3
+
4
+ Wraps sksecurity.kms.KMS for cryptographic operations while providing
5
+ agent-specific features: service key derivation, team member ACLs,
6
+ label-based lookup, and key expiry management.
7
+
8
+ Every operation is logged to the security audit trail.
9
+
10
+ Key hierarchy:
11
+ Agent identity key (PGP, managed by CapAuth)
12
+ └── Master KMS key (derived via HKDF from identity fingerprint)
13
+ ├── Service keys (per-service HKDF derivation)
14
+ ├── Team keys (shared keys with member ACL)
15
+ └── Subkeys (delegatable, revocable)
16
+
17
+ Crypto backend: sksecurity.kms — AES-256-GCM key wrapping,
18
+ HKDF-SHA256 derivation, scrypt master key sealing.
19
+
20
+ Storage layout:
21
+ ~/.skcapstone/security/kms/
22
+ ├── keystore.json # Key metadata (KeyRecord list)
23
+ ├── keys/ # Encrypted key material
24
+ │ └── <key_id>.key.enc # AES-256-GCM encrypted raw key bytes
25
+ ├── rotation-log.json # Key rotation history
26
+ └── backend/ # sksecurity KMS 4-tier hierarchy
27
+ ├── keys/ # Wrapped keys (master→team→agent→DEK)
28
+ └── audit.log # Backend audit trail
29
+
30
+ Usage:
31
+ store = KeyStore(home)
32
+ key = store.derive_service_key("api-gateway")
33
+ team_key = store.create_team_key("dev-team", members=["opus", "lumina"])
34
+ store.rotate_key(key.key_id)
35
+
36
+ # Access the full sksecurity 4-tier KMS backend:
37
+ backend = store.backend
38
+ backend.create_team_key("deployment-alpha")
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import hashlib
44
+ import json
45
+ import logging
46
+ import os
47
+ import secrets
48
+ from datetime import datetime, timedelta, timezone
49
+ from enum import Enum
50
+ from pathlib import Path
51
+ from typing import Any, Optional
52
+
53
+ from pydantic import BaseModel, Field
54
+
55
+ logger = logging.getLogger("skcapstone.kms")
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # sksecurity backend integration
60
+ # ---------------------------------------------------------------------------
61
+
62
+ try:
63
+ from sksecurity.kms import (
64
+ KMS as BackendKMS,
65
+ FileKeyStore as BackendFileKeyStore,
66
+ _hkdf_derive as _backend_hkdf,
67
+ _aes_gcm_encrypt as _backend_encrypt,
68
+ _aes_gcm_decrypt as _backend_decrypt,
69
+ )
70
+ _HAS_BACKEND = True
71
+ except ImportError:
72
+ _HAS_BACKEND = False
73
+ BackendKMS = None # type: ignore[assignment,misc]
74
+ BackendFileKeyStore = None # type: ignore[assignment,misc]
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Models
79
+ # ---------------------------------------------------------------------------
80
+
81
+ class KeyType(str, Enum):
82
+ """Types of managed keys."""
83
+
84
+ MASTER = "master"
85
+ SERVICE = "service"
86
+ TEAM = "team"
87
+ SUBKEY = "subkey"
88
+
89
+
90
+ class KeyStatus(str, Enum):
91
+ """Lifecycle status of a key."""
92
+
93
+ ACTIVE = "active"
94
+ ROTATED = "rotated"
95
+ REVOKED = "revoked"
96
+ EXPIRED = "expired"
97
+
98
+
99
+ class KeyRecord(BaseModel):
100
+ """Metadata for a managed key (the actual key material is stored separately)."""
101
+
102
+ key_id: str = Field(description="Unique key identifier (SHA-256 hash)")
103
+ key_type: KeyType
104
+ algorithm: str = Field(default="HKDF-SHA256+AES-256-GCM")
105
+ label: str = Field(description="Human-readable label (e.g., 'api-gateway', 'dev-team')")
106
+ parent_key_id: Optional[str] = Field(default=None, description="Parent key for derivations")
107
+ fingerprint: str = Field(description="SHA-256 of the raw key material")
108
+ status: KeyStatus = KeyStatus.ACTIVE
109
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
110
+ rotated_at: Optional[datetime] = None
111
+ expires_at: Optional[datetime] = None
112
+ version: int = Field(default=1, description="Key version (incremented on rotation)")
113
+ members: list[str] = Field(default_factory=list, description="Team key members (agent names)")
114
+ metadata: dict[str, Any] = Field(default_factory=dict)
115
+
116
+
117
+ class RotationEntry(BaseModel):
118
+ """Audit record for a key rotation event."""
119
+
120
+ key_id: str
121
+ old_fingerprint: str
122
+ new_fingerprint: str
123
+ old_version: int
124
+ new_version: int
125
+ rotated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
126
+ reason: str = ""
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Cryptographic helpers — delegates to sksecurity when available
131
+ # ---------------------------------------------------------------------------
132
+
133
+ def _derive_key(master_material: bytes, info: bytes, length: int = 32) -> bytes:
134
+ """Derive a key using HKDF-SHA256.
135
+
136
+ Delegates to sksecurity.kms._hkdf_derive when available, otherwise
137
+ uses the cryptography library directly.
138
+
139
+ Args:
140
+ master_material: Input keying material.
141
+ info: Context and application-specific info string.
142
+ length: Desired output key length in bytes.
143
+
144
+ Returns:
145
+ Derived key bytes.
146
+ """
147
+ if _HAS_BACKEND:
148
+ info_str = info.decode("utf-8") if isinstance(info, bytes) else info
149
+ return _backend_hkdf(master_material, info_str, length)
150
+
151
+ from cryptography.hazmat.primitives.hashes import SHA256
152
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
153
+
154
+ hkdf = HKDF(
155
+ algorithm=SHA256(),
156
+ length=length,
157
+ salt=None,
158
+ info=info,
159
+ )
160
+ return hkdf.derive(master_material)
161
+
162
+
163
+ def _encrypt_at_rest(data: bytes, key_material: bytes) -> bytes:
164
+ """Encrypt data for at-rest storage using AES-256-GCM.
165
+
166
+ Returns nonce (12 bytes) || ciphertext || tag (16 bytes).
167
+ Delegates to sksecurity.kms._aes_gcm_encrypt when available.
168
+
169
+ Args:
170
+ data: Plaintext bytes.
171
+ key_material: 32-byte AES key.
172
+
173
+ Returns:
174
+ Ciphertext bytes (nonce || ciphertext || tag).
175
+ """
176
+ if _HAS_BACKEND:
177
+ return _backend_encrypt(key_material, data)
178
+
179
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
180
+
181
+ nonce = os.urandom(12)
182
+ aesgcm = AESGCM(key_material[:32])
183
+ ct = aesgcm.encrypt(nonce, data, None)
184
+ return nonce + ct
185
+
186
+
187
+ def _decrypt_at_rest(token: bytes, key_material: bytes) -> bytes:
188
+ """Decrypt data from at-rest AES-256-GCM storage.
189
+
190
+ Expects nonce (12 bytes) || ciphertext || tag (16 bytes).
191
+ Delegates to sksecurity.kms._aes_gcm_decrypt when available.
192
+
193
+ Args:
194
+ token: Ciphertext bytes (nonce || ciphertext || tag).
195
+ key_material: 32-byte AES key.
196
+
197
+ Returns:
198
+ Plaintext bytes.
199
+ """
200
+ if _HAS_BACKEND:
201
+ return _backend_decrypt(key_material, token)
202
+
203
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
204
+
205
+ nonce, ct = token[:12], token[12:]
206
+ aesgcm = AESGCM(key_material[:32])
207
+ return aesgcm.decrypt(nonce, ct, None)
208
+
209
+
210
+ def _fernet_encrypt(data: bytes, key: bytes) -> bytes:
211
+ """Encrypt data using Fernet (AES-128-CBC + HMAC-SHA256).
212
+
213
+ Derives a 32-byte Fernet key from the provided key material using SHA-256,
214
+ then encodes it as URL-safe base64 as Fernet requires.
215
+
216
+ Args:
217
+ data: Plaintext bytes.
218
+ key: Raw key material (any length).
219
+
220
+ Returns:
221
+ Fernet token bytes.
222
+ """
223
+ import base64
224
+
225
+ from cryptography.fernet import Fernet
226
+
227
+ key32 = hashlib.sha256(key).digest()
228
+ fernet_key = base64.urlsafe_b64encode(key32)
229
+ return Fernet(fernet_key).encrypt(data)
230
+
231
+
232
+ def _fernet_decrypt(token: bytes, key: bytes) -> bytes:
233
+ """Decrypt a Fernet token.
234
+
235
+ Args:
236
+ token: Fernet token bytes produced by :func:`_fernet_encrypt`.
237
+ key: Raw key material (any length) — must match the key used to encrypt.
238
+
239
+ Returns:
240
+ Decrypted plaintext bytes.
241
+ """
242
+ import base64
243
+
244
+ from cryptography.fernet import Fernet
245
+
246
+ key32 = hashlib.sha256(key).digest()
247
+ fernet_key = base64.urlsafe_b64encode(key32)
248
+ return Fernet(fernet_key).decrypt(token)
249
+
250
+
251
+ def _key_fingerprint(raw: bytes) -> str:
252
+ """Compute SHA-256 fingerprint of raw key material."""
253
+ return hashlib.sha256(raw).hexdigest()
254
+
255
+
256
+ def _key_id(label: str, key_type: KeyType, version: int = 1) -> str:
257
+ """Deterministic key ID from label + type + version."""
258
+ data = f"skcapstone:kms:{key_type.value}:{label}:v{version}"
259
+ return hashlib.sha256(data.encode()).hexdigest()[:16]
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # KeyStore
264
+ # ---------------------------------------------------------------------------
265
+
266
+ class KeyStore:
267
+ """Sovereign key management store.
268
+
269
+ Wraps sksecurity.kms.KMS for cryptographic operations while
270
+ providing agent-specific key lifecycle management: derivation,
271
+ storage, rotation, team membership, and revocation.
272
+
273
+ When sksecurity is installed, the full 4-tier key hierarchy
274
+ (master -> team -> agent -> DEK) is available via the ``backend``
275
+ property. The agent-level API (service keys, team ACLs, subkeys)
276
+ is always available regardless of sksecurity availability.
277
+
278
+ Args:
279
+ home: Agent home directory (~/.skcapstone).
280
+ """
281
+
282
+ def __init__(self, home: Path) -> None:
283
+ self._home = home
284
+ self._kms_dir = home / "security" / "kms"
285
+ self._keys_dir = self._kms_dir / "keys"
286
+ self._keystore_file = self._kms_dir / "keystore.json"
287
+ self._rotation_log = self._kms_dir / "rotation-log.json"
288
+ self._master_material: Optional[bytes] = None
289
+ self._backend_kms: Optional[Any] = None
290
+
291
+ @property
292
+ def backend(self) -> Optional[Any]:
293
+ """Access the sksecurity KMS backend for 4-tier operations.
294
+
295
+ Returns the underlying sksecurity.kms.KMS instance (unsealed)
296
+ for direct team/agent/DEK key management. Returns None if
297
+ sksecurity is not installed or backend initialization failed.
298
+ """
299
+ return self._backend_kms
300
+
301
+ def initialize(self) -> KeyRecord:
302
+ """Initialize the KMS and derive the master key.
303
+
304
+ The master key is derived from the agent's identity fingerprint
305
+ via HKDF. If no identity exists, a random master is generated.
306
+ When sksecurity is available, also initializes the 4-tier
307
+ backend KMS.
308
+
309
+ Returns:
310
+ KeyRecord for the master key.
311
+ """
312
+ self._kms_dir.mkdir(parents=True, exist_ok=True)
313
+ self._keys_dir.mkdir(exist_ok=True)
314
+
315
+ existing = self._load_records()
316
+ master = next((r for r in existing if r.key_type == KeyType.MASTER), None)
317
+ if master and master.status == KeyStatus.ACTIVE:
318
+ self._master_material = self._load_key_material(master.key_id)
319
+ self._init_backend()
320
+ return master
321
+
322
+ identity_material = self._get_identity_material()
323
+ raw_master = _derive_key(identity_material, b"skcapstone:kms:master", length=32)
324
+ self._master_material = raw_master
325
+
326
+ record = KeyRecord(
327
+ key_id=_key_id("master", KeyType.MASTER),
328
+ key_type=KeyType.MASTER,
329
+ label="master",
330
+ fingerprint=_key_fingerprint(raw_master),
331
+ )
332
+
333
+ self._save_key_material(record.key_id, raw_master)
334
+ self._append_record(record)
335
+ self._init_backend()
336
+ self._audit("KMS_INIT", f"KMS initialized, master key {record.key_id}")
337
+
338
+ return record
339
+
340
+ def derive_service_key(
341
+ self,
342
+ service_name: str,
343
+ ttl_days: Optional[int] = None,
344
+ ) -> KeyRecord:
345
+ """Derive a service-specific key from the master.
346
+
347
+ Args:
348
+ service_name: Service identifier (e.g., 'api-gateway', 'skchat').
349
+ ttl_days: Optional key expiry in days.
350
+
351
+ Returns:
352
+ KeyRecord for the new service key.
353
+ """
354
+ master = self._ensure_master()
355
+
356
+ existing = self.get_key(service_name, KeyType.SERVICE)
357
+ if existing and existing.status == KeyStatus.ACTIVE:
358
+ return existing
359
+
360
+ info = f"skcapstone:kms:service:{service_name}".encode()
361
+ raw = _derive_key(self._master_material, info, length=32)
362
+
363
+ expires = None
364
+ if ttl_days:
365
+ expires = datetime.now(timezone.utc) + timedelta(days=ttl_days)
366
+
367
+ record = KeyRecord(
368
+ key_id=_key_id(service_name, KeyType.SERVICE),
369
+ key_type=KeyType.SERVICE,
370
+ label=service_name,
371
+ parent_key_id=master.key_id,
372
+ fingerprint=_key_fingerprint(raw),
373
+ expires_at=expires,
374
+ )
375
+
376
+ self._save_key_material(record.key_id, raw)
377
+ self._append_record(record)
378
+ self._audit(
379
+ "KEY_DERIVE",
380
+ f"Derived service key '{service_name}' ({record.key_id})",
381
+ metadata={"key_type": "service", "label": service_name},
382
+ )
383
+
384
+ return record
385
+
386
+ def derive_subkey(
387
+ self,
388
+ label: str,
389
+ parent_label: Optional[str] = None,
390
+ ) -> KeyRecord:
391
+ """Derive a subkey for delegation.
392
+
393
+ Args:
394
+ label: Subkey label.
395
+ parent_label: Parent service key label (defaults to master).
396
+
397
+ Returns:
398
+ KeyRecord for the new subkey.
399
+ """
400
+ if parent_label:
401
+ parent = self.get_key(parent_label)
402
+ if not parent:
403
+ raise ValueError(f"Parent key '{parent_label}' not found")
404
+ parent_material = self._load_key_material(parent.key_id)
405
+ parent_id = parent.key_id
406
+ else:
407
+ self._ensure_master()
408
+ parent_material = self._master_material
409
+ parent_id = _key_id("master", KeyType.MASTER)
410
+
411
+ info = f"skcapstone:kms:subkey:{label}".encode()
412
+ raw = _derive_key(parent_material, info, length=32)
413
+
414
+ record = KeyRecord(
415
+ key_id=_key_id(label, KeyType.SUBKEY),
416
+ key_type=KeyType.SUBKEY,
417
+ label=label,
418
+ parent_key_id=parent_id,
419
+ fingerprint=_key_fingerprint(raw),
420
+ )
421
+
422
+ self._save_key_material(record.key_id, raw)
423
+ self._append_record(record)
424
+ self._audit(
425
+ "KEY_DERIVE",
426
+ f"Derived subkey '{label}' ({record.key_id})",
427
+ metadata={"key_type": "subkey", "parent": parent_id},
428
+ )
429
+
430
+ return record
431
+
432
+ def create_team_key(
433
+ self,
434
+ team_name: str,
435
+ members: Optional[list[str]] = None,
436
+ ) -> KeyRecord:
437
+ """Create a shared team key.
438
+
439
+ Team keys are random (not derived from master) so they can be
440
+ independently rotated without affecting the key hierarchy.
441
+
442
+ Args:
443
+ team_name: Team identifier.
444
+ members: Initial list of agent names with access.
445
+
446
+ Returns:
447
+ KeyRecord for the new team key.
448
+ """
449
+ self._ensure_master()
450
+
451
+ existing = self.get_key(team_name, KeyType.TEAM)
452
+ if existing and existing.status == KeyStatus.ACTIVE:
453
+ return existing
454
+
455
+ raw = secrets.token_bytes(32)
456
+
457
+ record = KeyRecord(
458
+ key_id=_key_id(team_name, KeyType.TEAM),
459
+ key_type=KeyType.TEAM,
460
+ label=team_name,
461
+ fingerprint=_key_fingerprint(raw),
462
+ members=members or [],
463
+ algorithm="random+AES-256-GCM",
464
+ )
465
+
466
+ self._save_key_material(record.key_id, raw)
467
+ self._append_record(record)
468
+ self._audit(
469
+ "TEAM_KEY_CREATE",
470
+ f"Created team key '{team_name}' ({record.key_id}) with {len(record.members)} members",
471
+ metadata={"team": team_name, "members": record.members},
472
+ )
473
+
474
+ return record
475
+
476
+ def add_team_member(self, team_name: str, agent_name: str) -> KeyRecord:
477
+ """Add a member to a team key's ACL.
478
+
479
+ Args:
480
+ team_name: Team key label.
481
+ agent_name: Agent name to add.
482
+
483
+ Returns:
484
+ Updated KeyRecord.
485
+
486
+ Raises:
487
+ ValueError: If team key not found.
488
+ """
489
+ record = self.get_key(team_name, KeyType.TEAM)
490
+ if not record:
491
+ raise ValueError(f"Team key '{team_name}' not found")
492
+
493
+ if agent_name in record.members:
494
+ return record
495
+
496
+ record.members.append(agent_name)
497
+ self._update_record(record)
498
+ self._audit(
499
+ "TEAM_MEMBER_ADD",
500
+ f"Added '{agent_name}' to team '{team_name}'",
501
+ metadata={"team": team_name, "agent": agent_name},
502
+ )
503
+
504
+ return record
505
+
506
+ def remove_team_member(self, team_name: str, agent_name: str) -> KeyRecord:
507
+ """Remove a member from a team key's ACL.
508
+
509
+ Args:
510
+ team_name: Team key label.
511
+ agent_name: Agent name to remove.
512
+
513
+ Returns:
514
+ Updated KeyRecord.
515
+
516
+ Raises:
517
+ ValueError: If team key not found.
518
+ """
519
+ record = self.get_key(team_name, KeyType.TEAM)
520
+ if not record:
521
+ raise ValueError(f"Team key '{team_name}' not found")
522
+
523
+ if agent_name not in record.members:
524
+ return record
525
+
526
+ record.members.remove(agent_name)
527
+ self._update_record(record)
528
+ self._audit(
529
+ "TEAM_MEMBER_REMOVE",
530
+ f"Removed '{agent_name}' from team '{team_name}'",
531
+ metadata={"team": team_name, "agent": agent_name},
532
+ )
533
+
534
+ return record
535
+
536
+ def rotate_key(self, key_id: str, reason: str = "") -> KeyRecord:
537
+ """Rotate a key — generate new material, increment version.
538
+
539
+ The old key is marked ROTATED and a new active key replaces it.
540
+
541
+ Args:
542
+ key_id: Key to rotate.
543
+ reason: Optional reason for the rotation.
544
+
545
+ Returns:
546
+ New KeyRecord for the rotated key.
547
+
548
+ Raises:
549
+ ValueError: If key not found.
550
+ """
551
+ old = self._get_record_by_id(key_id)
552
+ if not old:
553
+ raise ValueError(f"Key '{key_id}' not found")
554
+
555
+ if old.key_type == KeyType.MASTER:
556
+ return self._rotate_master(old, reason)
557
+
558
+ old_fingerprint = old.fingerprint
559
+ old_version = old.version
560
+
561
+ if old.key_type == KeyType.TEAM:
562
+ new_raw = secrets.token_bytes(32)
563
+ else:
564
+ self._ensure_master()
565
+ info = f"skcapstone:kms:{old.key_type.value}:{old.label}:v{old.version + 1}".encode()
566
+ new_raw = _derive_key(self._master_material, info, length=32)
567
+
568
+ old.status = KeyStatus.ROTATED
569
+ old.rotated_at = datetime.now(timezone.utc)
570
+ self._update_record(old)
571
+
572
+ new_record = KeyRecord(
573
+ key_id=_key_id(old.label, old.key_type, old.version + 1),
574
+ key_type=old.key_type,
575
+ label=old.label,
576
+ parent_key_id=old.parent_key_id,
577
+ fingerprint=_key_fingerprint(new_raw),
578
+ version=old.version + 1,
579
+ members=old.members.copy(),
580
+ algorithm=old.algorithm,
581
+ )
582
+
583
+ self._save_key_material(new_record.key_id, new_raw)
584
+ self._append_record(new_record)
585
+
586
+ rotation = RotationEntry(
587
+ key_id=old.key_id,
588
+ old_fingerprint=old_fingerprint,
589
+ new_fingerprint=new_record.fingerprint,
590
+ old_version=old_version,
591
+ new_version=new_record.version,
592
+ reason=reason,
593
+ )
594
+ self._append_rotation(rotation)
595
+
596
+ self._audit(
597
+ "KEY_ROTATE",
598
+ f"Rotated key '{old.label}' v{old_version} -> v{new_record.version}",
599
+ metadata={
600
+ "key_id": old.key_id,
601
+ "new_key_id": new_record.key_id,
602
+ "reason": reason,
603
+ },
604
+ )
605
+
606
+ return new_record
607
+
608
+ def revoke_key(self, key_id: str, reason: str = "") -> KeyRecord:
609
+ """Revoke a key — mark it unusable.
610
+
611
+ Args:
612
+ key_id: Key to revoke.
613
+ reason: Optional reason.
614
+
615
+ Returns:
616
+ Updated KeyRecord.
617
+
618
+ Raises:
619
+ ValueError: If key not found.
620
+ """
621
+ record = self._get_record_by_id(key_id)
622
+ if not record:
623
+ raise ValueError(f"Key '{key_id}' not found")
624
+
625
+ record.status = KeyStatus.REVOKED
626
+ self._update_record(record)
627
+
628
+ key_file = self._keys_dir / f"{key_id}.key.enc"
629
+ if key_file.exists():
630
+ key_file.unlink()
631
+
632
+ self._audit(
633
+ "KEY_REVOKE",
634
+ f"Revoked key '{record.label}' ({key_id})",
635
+ metadata={"key_id": key_id, "reason": reason},
636
+ )
637
+
638
+ return record
639
+
640
+ def get_key(
641
+ self,
642
+ label: str,
643
+ key_type: Optional[KeyType] = None,
644
+ ) -> Optional[KeyRecord]:
645
+ """Look up the latest active key by label.
646
+
647
+ Args:
648
+ label: Key label.
649
+ key_type: Optional filter by key type.
650
+
651
+ Returns:
652
+ KeyRecord if found, None otherwise.
653
+ """
654
+ records = self._load_records()
655
+ matches = [
656
+ r for r in records
657
+ if r.label == label and r.status == KeyStatus.ACTIVE
658
+ and (key_type is None or r.key_type == key_type)
659
+ ]
660
+ if not matches:
661
+ return None
662
+ return max(matches, key=lambda r: r.version)
663
+
664
+ def list_keys(
665
+ self,
666
+ key_type: Optional[KeyType] = None,
667
+ include_inactive: bool = False,
668
+ ) -> list[KeyRecord]:
669
+ """List all managed keys.
670
+
671
+ Args:
672
+ key_type: Optional filter.
673
+ include_inactive: Include rotated/revoked keys.
674
+
675
+ Returns:
676
+ List of KeyRecords.
677
+ """
678
+ records = self._load_records()
679
+ if key_type:
680
+ records = [r for r in records if r.key_type == key_type]
681
+ if not include_inactive:
682
+ records = [r for r in records if r.status == KeyStatus.ACTIVE]
683
+ return records
684
+
685
+ def get_key_material(self, key_id: str, agent_name: Optional[str] = None) -> bytes:
686
+ """Retrieve raw key material (access-controlled for team keys).
687
+
688
+ Args:
689
+ key_id: Key to retrieve.
690
+ agent_name: Requesting agent (checked against team ACL).
691
+
692
+ Returns:
693
+ Raw key bytes.
694
+
695
+ Raises:
696
+ ValueError: If key not found.
697
+ PermissionError: If agent not in team ACL.
698
+ """
699
+ record = self._get_record_by_id(key_id)
700
+ if not record:
701
+ raise ValueError(f"Key '{key_id}' not found")
702
+
703
+ if record.status != KeyStatus.ACTIVE:
704
+ raise ValueError(f"Key '{key_id}' is {record.status.value}")
705
+
706
+ if record.key_type == KeyType.TEAM and record.members and agent_name:
707
+ if agent_name not in record.members:
708
+ self._audit(
709
+ "KEY_ACCESS_DENIED",
710
+ f"Agent '{agent_name}' denied access to team key '{record.label}'",
711
+ metadata={"key_id": key_id, "agent": agent_name},
712
+ )
713
+ raise PermissionError(
714
+ f"Agent '{agent_name}' not in team '{record.label}' members"
715
+ )
716
+
717
+ material = self._load_key_material(key_id)
718
+ self._audit(
719
+ "KEY_ACCESS",
720
+ f"Key material accessed: '{record.label}' ({key_id})",
721
+ metadata={"key_id": key_id, "agent": agent_name},
722
+ )
723
+ return material
724
+
725
+ def status(self) -> dict[str, Any]:
726
+ """Return KMS status summary.
727
+
728
+ Returns:
729
+ Dict with key counts, health, and statistics.
730
+ """
731
+ records = self._load_records()
732
+ active = [r for r in records if r.status == KeyStatus.ACTIVE]
733
+ rotated = [r for r in records if r.status == KeyStatus.ROTATED]
734
+ revoked = [r for r in records if r.status == KeyStatus.REVOKED]
735
+
736
+ by_type: dict[str, int] = {}
737
+ for r in active:
738
+ by_type[r.key_type.value] = by_type.get(r.key_type.value, 0) + 1
739
+
740
+ expiring_soon = [
741
+ r for r in active
742
+ if r.expires_at and r.expires_at < datetime.now(timezone.utc) + timedelta(days=7)
743
+ ]
744
+
745
+ return {
746
+ "initialized": bool(active),
747
+ "total_keys": len(records),
748
+ "active": len(active),
749
+ "rotated": len(rotated),
750
+ "revoked": len(revoked),
751
+ "by_type": by_type,
752
+ "expiring_soon": [r.label for r in expiring_soon],
753
+ "kms_dir": str(self._kms_dir),
754
+ "backend_available": _HAS_BACKEND,
755
+ "backend_unsealed": (
756
+ self._backend_kms.is_unsealed if self._backend_kms else False
757
+ ),
758
+ }
759
+
760
+ # -------------------------------------------------------------------
761
+ # Internal helpers
762
+ # -------------------------------------------------------------------
763
+
764
+ def _init_backend(self) -> None:
765
+ """Initialize the sksecurity KMS backend if available."""
766
+ if not _HAS_BACKEND or self._backend_kms is not None:
767
+ return
768
+
769
+ try:
770
+ backend_dir = self._kms_dir / "backend"
771
+ backend_dir.mkdir(parents=True, exist_ok=True)
772
+ store = BackendFileKeyStore(store_dir=backend_dir / "keys")
773
+ self._backend_kms = BackendKMS(
774
+ store=store,
775
+ audit_path=backend_dir / "audit.log",
776
+ )
777
+ passphrase = hashlib.sha256(
778
+ self._get_identity_material()
779
+ ).hexdigest()
780
+ self._backend_kms.unseal(passphrase)
781
+ logger.debug("sksecurity KMS backend initialized and unsealed")
782
+ except Exception as exc:
783
+ logger.warning("Failed to initialize sksecurity KMS backend: %s", exc)
784
+ self._backend_kms = None
785
+
786
+ def _ensure_master(self) -> KeyRecord:
787
+ """Ensure the master key is loaded."""
788
+ if self._master_material is None:
789
+ return self.initialize()
790
+ records = self._load_records()
791
+ master = next((r for r in records if r.key_type == KeyType.MASTER and r.status == KeyStatus.ACTIVE), None)
792
+ if master is None:
793
+ return self.initialize()
794
+ return master
795
+
796
+ def _rotate_master(self, old: KeyRecord, reason: str) -> KeyRecord:
797
+ """Rotate the master key (re-derives from fresh identity material)."""
798
+ identity_material = self._get_identity_material()
799
+ salt = secrets.token_bytes(16)
800
+ info = f"skcapstone:kms:master:v{old.version + 1}".encode()
801
+
802
+ from cryptography.hazmat.primitives.hashes import SHA256
803
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
804
+
805
+ hkdf = HKDF(algorithm=SHA256(), length=32, salt=salt, info=info)
806
+ new_raw = hkdf.derive(identity_material)
807
+ self._master_material = new_raw
808
+
809
+ old.status = KeyStatus.ROTATED
810
+ old.rotated_at = datetime.now(timezone.utc)
811
+ self._update_record(old)
812
+
813
+ new_record = KeyRecord(
814
+ key_id=_key_id("master", KeyType.MASTER, old.version + 1),
815
+ key_type=KeyType.MASTER,
816
+ label="master",
817
+ fingerprint=_key_fingerprint(new_raw),
818
+ version=old.version + 1,
819
+ )
820
+
821
+ self._save_key_material(new_record.key_id, new_raw)
822
+ self._append_record(new_record)
823
+
824
+ rotation = RotationEntry(
825
+ key_id=old.key_id,
826
+ old_fingerprint=old.fingerprint,
827
+ new_fingerprint=new_record.fingerprint,
828
+ old_version=old.version,
829
+ new_version=new_record.version,
830
+ reason=reason,
831
+ )
832
+ self._append_rotation(rotation)
833
+
834
+ self._audit(
835
+ "MASTER_KEY_ROTATE",
836
+ f"Master key rotated v{old.version} -> v{new_record.version}",
837
+ metadata={"reason": reason},
838
+ )
839
+
840
+ return new_record
841
+
842
+ def _get_identity_material(self) -> bytes:
843
+ """Get identity keying material from the agent's CapAuth profile."""
844
+ identity_file = self._home / "identity" / "identity.json"
845
+ if identity_file.exists():
846
+ try:
847
+ data = json.loads(identity_file.read_text(encoding="utf-8"))
848
+ fingerprint = data.get("fingerprint", "")
849
+ if fingerprint:
850
+ return f"skcapstone:identity:{fingerprint}".encode()
851
+ except (json.JSONDecodeError, OSError) as exc:
852
+ logger.warning("Failed to read identity file %s: %s", identity_file, exc)
853
+
854
+ logger.warning("No identity found for KMS — using random master seed")
855
+ return secrets.token_bytes(64)
856
+
857
+ def _load_records(self) -> list[KeyRecord]:
858
+ """Load all key records from disk."""
859
+ if not self._keystore_file.exists():
860
+ return []
861
+ try:
862
+ data = json.loads(self._keystore_file.read_text(encoding="utf-8"))
863
+ return [KeyRecord.model_validate(r) for r in data]
864
+ except (json.JSONDecodeError, Exception) as exc:
865
+ logger.warning("Failed to load keystore: %s", exc)
866
+ return []
867
+
868
+ def _save_records(self, records: list[KeyRecord]) -> None:
869
+ """Write all key records to disk."""
870
+ self._kms_dir.mkdir(parents=True, exist_ok=True)
871
+ data = [r.model_dump(mode="json") for r in records]
872
+ self._keystore_file.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
873
+
874
+ def _append_record(self, record: KeyRecord) -> None:
875
+ """Add a new record to the keystore."""
876
+ records = self._load_records()
877
+ records.append(record)
878
+ self._save_records(records)
879
+
880
+ def _update_record(self, updated: KeyRecord) -> None:
881
+ """Update an existing record in the keystore."""
882
+ records = self._load_records()
883
+ for i, r in enumerate(records):
884
+ if r.key_id == updated.key_id and r.version == updated.version:
885
+ records[i] = updated
886
+ break
887
+ self._save_records(records)
888
+
889
+ def _get_record_by_id(self, key_id: str) -> Optional[KeyRecord]:
890
+ """Find a record by key_id."""
891
+ records = self._load_records()
892
+ matches = [r for r in records if r.key_id == key_id]
893
+ return matches[-1] if matches else None
894
+
895
+ def _save_key_material(self, key_id: str, raw: bytes) -> None:
896
+ """Encrypt and save raw key material to disk using AES-256-GCM."""
897
+ self._keys_dir.mkdir(parents=True, exist_ok=True)
898
+ enc_key = self._get_encryption_key()
899
+ encrypted = _encrypt_at_rest(raw, enc_key)
900
+ (self._keys_dir / f"{key_id}.key.enc").write_bytes(encrypted)
901
+
902
+ def _load_key_material(self, key_id: str) -> bytes:
903
+ """Load and decrypt key material from disk."""
904
+ key_file = self._keys_dir / f"{key_id}.key.enc"
905
+ if not key_file.exists():
906
+ raise ValueError(f"Key material not found for '{key_id}'")
907
+ enc_key = self._get_encryption_key()
908
+ return _decrypt_at_rest(key_file.read_bytes(), enc_key)
909
+
910
+ def _get_encryption_key(self) -> bytes:
911
+ """Get the encryption key for at-rest key storage.
912
+
913
+ Uses a deterministic derivation from the agent's identity
914
+ fingerprint so keys can be decrypted without storing a
915
+ separate passphrase.
916
+ """
917
+ identity_material = self._get_identity_material()
918
+ return _derive_key(identity_material, b"skcapstone:kms:storage-encryption", length=32)
919
+
920
+ def _append_rotation(self, entry: RotationEntry) -> None:
921
+ """Append a rotation event to the rotation log."""
922
+ log: list[dict] = []
923
+ if self._rotation_log.exists():
924
+ try:
925
+ log = json.loads(self._rotation_log.read_text(encoding="utf-8"))
926
+ except (json.JSONDecodeError, OSError) as exc:
927
+ logger.warning("Failed to read rotation log, starting fresh: %s", exc)
928
+ log.append(entry.model_dump(mode="json"))
929
+ self._rotation_log.write_text(json.dumps(log, indent=2, default=str), encoding="utf-8")
930
+
931
+ def _audit(
932
+ self,
933
+ event_type: str,
934
+ detail: str,
935
+ metadata: Optional[dict] = None,
936
+ ) -> None:
937
+ """Log a KMS event to the security audit trail."""
938
+ try:
939
+ from .pillars.security import audit_event
940
+ audit_event(self._home, event_type, detail, agent="kms", metadata=metadata)
941
+ except Exception:
942
+ logger.debug("Audit log unavailable: %s — %s", event_type, detail)