@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,420 @@
1
+ """Tests for the two-factor memory verification gate (memory_verifier.py).
2
+
3
+ These tests verify that:
4
+ - Aligned memories are promoted normally.
5
+ - Contradicting memories are tagged=conflicting, a conflict report is stored,
6
+ a critical notification fires, and promotion is skipped.
7
+ - Fail-open behaviour when skseed is unavailable.
8
+ - Short-term → mid-term gate in memory_engine._promote().
9
+ - Short-term → mid-term gate in PromotionEngine._promote().
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from unittest.mock import MagicMock, patch
18
+
19
+ import pytest
20
+
21
+ from skcapstone.memory_verifier import (
22
+ VerificationResult,
23
+ verify_before_promotion,
24
+ )
25
+ from skcapstone.models import MemoryEntry, MemoryLayer
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Helpers
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def _make_entry(
34
+ memory_id: str = "abc123",
35
+ content: str = "The sky is blue",
36
+ layer: MemoryLayer = MemoryLayer.SHORT_TERM,
37
+ importance: float = 0.5,
38
+ tags: list[str] | None = None,
39
+ ) -> MemoryEntry:
40
+ return MemoryEntry(
41
+ memory_id=memory_id,
42
+ content=content,
43
+ tags=tags or [],
44
+ source="test",
45
+ layer=layer,
46
+ importance=importance,
47
+ created_at=datetime.now(timezone.utc),
48
+ )
49
+
50
+
51
+ def _aligned_result(coherence: float = 0.85) -> dict:
52
+ """Simulate a truth_check return that passes alignment."""
53
+ return {
54
+ "is_aligned": True,
55
+ "collider_result": {
56
+ "coherence_score": coherence,
57
+ "truth_grade": "strong",
58
+ "collision_fragments": [],
59
+ },
60
+ "alignment_record": {},
61
+ "belief": {},
62
+ }
63
+
64
+
65
+ def _contradicting_result(
66
+ coherence: float = 0.3,
67
+ fragments: list[str] | None = None,
68
+ ) -> dict:
69
+ """Simulate a truth_check return that fails alignment."""
70
+ return {
71
+ "is_aligned": False,
72
+ "collider_result": {
73
+ "coherence_score": coherence,
74
+ "truth_grade": "weak",
75
+ "collision_fragments": fragments or ["fragment A contradicts earlier claim"],
76
+ },
77
+ "alignment_record": {},
78
+ "belief": {},
79
+ }
80
+
81
+
82
+ @pytest.fixture
83
+ def home(tmp_path: Path) -> Path:
84
+ """Minimal agent home directory."""
85
+ mem = tmp_path / "memory"
86
+ for layer in ("short-term", "mid-term", "long-term"):
87
+ (mem / layer).mkdir(parents=True)
88
+ return tmp_path
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # verify_before_promotion — unit tests
93
+ # ---------------------------------------------------------------------------
94
+
95
+
96
+ class TestVerifyBeforePromotion:
97
+
98
+ def test_fail_open_when_skseed_missing(self, home: Path) -> None:
99
+ """Should allow promotion when skseed cannot be imported."""
100
+ entry = _make_entry()
101
+ with patch.dict(sys.modules, {"skseed": None, "skseed.skill": None}):
102
+ result = verify_before_promotion(home, entry)
103
+ assert result.should_promote is True
104
+ assert result.is_conflicting is False
105
+
106
+ def test_fail_open_when_truth_check_raises(self, home: Path) -> None:
107
+ """Should allow promotion when truth_check raises unexpectedly."""
108
+ import skcapstone.memory_verifier as mv
109
+
110
+ entry = _make_entry()
111
+ skill_mock = MagicMock()
112
+ skill_mock.truth_check = MagicMock(side_effect=RuntimeError("LLM timeout"))
113
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
114
+ result = mv.verify_before_promotion(home, entry)
115
+ assert result.should_promote is True
116
+
117
+ def test_skip_gate_for_mid_term_entries(self, home: Path) -> None:
118
+ """Gate only fires for SHORT_TERM; mid-term entries pass through."""
119
+ entry = _make_entry(layer=MemoryLayer.MID_TERM)
120
+ result = verify_before_promotion(home, entry)
121
+ assert result.should_promote is True
122
+ assert result.is_conflicting is False
123
+
124
+ def test_aligned_memory_promotes(self, home: Path) -> None:
125
+ """Aligned truth-check result → should_promote=True, no conflict."""
126
+ entry = _make_entry()
127
+ skill_mock = MagicMock()
128
+ skill_mock.truth_check = MagicMock(return_value=_aligned_result(0.9))
129
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
130
+ result = verify_before_promotion(home, entry)
131
+ assert result.should_promote is True
132
+ assert result.is_conflicting is False
133
+ assert result.coherence_score == pytest.approx(0.9)
134
+ assert result.truth_grade == "strong"
135
+
136
+ def test_contradicting_memory_blocked(self, home: Path) -> None:
137
+ """Contradiction found → should_promote=False, is_conflicting=True."""
138
+ entry = _make_entry()
139
+ skill_mock = MagicMock()
140
+ skill_mock.truth_check = MagicMock(return_value=_contradicting_result())
141
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
142
+ with patch("skcapstone.memory_verifier._fire_conflict_notification"):
143
+ result = verify_before_promotion(home, entry)
144
+ assert result.should_promote is False
145
+ assert result.is_conflicting is True
146
+ assert result.collision_fragments != []
147
+
148
+ def test_conflicting_tag_added_to_candidate(self, home: Path) -> None:
149
+ """Candidate entry gets tag=conflicting when promotion is blocked."""
150
+ entry = _make_entry(memory_id="cand01")
151
+ # Write the entry to disk so _save_entry works
152
+ path = home / "memory" / "short-term" / "cand01.json"
153
+ path.write_text(entry.model_dump_json(), encoding="utf-8")
154
+
155
+ skill_mock = MagicMock()
156
+ skill_mock.truth_check = MagicMock(return_value=_contradicting_result())
157
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
158
+ with patch("skcapstone.memory_verifier._fire_conflict_notification"):
159
+ verify_before_promotion(home, entry)
160
+
161
+ # Re-read from disk
162
+ import json
163
+ raw = json.loads(path.read_text())
164
+ assert "conflicting" in raw["tags"]
165
+
166
+ def test_conflict_report_stored(self, home: Path) -> None:
167
+ """A conflict-report memory is stored in short-term when blocked."""
168
+ entry = _make_entry(memory_id="cand02", content="contradictory claim")
169
+ path = home / "memory" / "short-term" / "cand02.json"
170
+ path.write_text(entry.model_dump_json(), encoding="utf-8")
171
+
172
+ skill_mock = MagicMock()
173
+ skill_mock.truth_check = MagicMock(return_value=_contradicting_result())
174
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
175
+ with patch("skcapstone.memory_verifier._fire_conflict_notification"):
176
+ result = verify_before_promotion(home, entry)
177
+
178
+ assert result.conflict_report_id is not None
179
+ # The report file should exist in short-term
180
+ report_path = home / "memory" / "short-term" / f"{result.conflict_report_id}.json"
181
+ assert report_path.exists()
182
+ import json
183
+ report = json.loads(report_path.read_text())
184
+ assert "conflicting" in report["tags"]
185
+ assert "CONFLICT REPORT" in report["content"]
186
+ assert "cand02" in report["content"]
187
+
188
+ def test_critical_notification_fired(self, home: Path) -> None:
189
+ """A critical desktop notification fires when promotion is blocked."""
190
+ entry = _make_entry()
191
+ path = home / "memory" / "short-term" / f"{entry.memory_id}.json"
192
+ path.write_text(entry.model_dump_json(), encoding="utf-8")
193
+
194
+ skill_mock = MagicMock()
195
+ skill_mock.truth_check = MagicMock(return_value=_contradicting_result())
196
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
197
+ with patch("skcapstone.memory_verifier._fire_conflict_notification") as mock_notif:
198
+ verify_before_promotion(home, entry)
199
+ mock_notif.assert_called_once()
200
+
201
+ def test_notification_uses_critical_urgency(self, home: Path) -> None:
202
+ """The actual notify() call receives urgency=critical."""
203
+ entry = _make_entry(memory_id="notif01")
204
+ path = home / "memory" / "short-term" / "notif01.json"
205
+ path.write_text(entry.model_dump_json(), encoding="utf-8")
206
+
207
+ skill_mock = MagicMock()
208
+ skill_mock.truth_check = MagicMock(return_value=_contradicting_result())
209
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
210
+ with patch("skcapstone.notifications.notify") as mock_notify:
211
+ import skcapstone.memory_verifier as mv
212
+ # Ensure the module uses our patched notify
213
+ with patch.object(mv, "_fire_conflict_notification",
214
+ wraps=mv._fire_conflict_notification):
215
+ verify_before_promotion(home, entry)
216
+ # At least one call should have urgency=critical
217
+ calls = mock_notify.call_args_list
218
+ assert any(
219
+ call.kwargs.get("urgency") == "critical" or
220
+ (len(call.args) >= 3 and call.args[2] == "critical")
221
+ for call in calls
222
+ ), f"No critical notification call found in {calls}"
223
+
224
+ def test_verifier_source_bypasses_gate(self, home: Path) -> None:
225
+ """Conflict-report entries (source=memory_verifier) must not be re-checked."""
226
+ entry = _make_entry(memory_id="meta01")
227
+ entry = _make_entry(memory_id="meta01")
228
+ # Simulate a conflict-report entry
229
+ from dataclasses import replace
230
+ entry2 = MemoryEntry(
231
+ memory_id="meta01",
232
+ content="[CONFLICT REPORT] some conflict",
233
+ tags=["conflicting"],
234
+ source="memory_verifier",
235
+ layer=MemoryLayer.SHORT_TERM,
236
+ importance=0.8,
237
+ created_at=entry.created_at,
238
+ )
239
+ # truth_check should never be called
240
+ skill_mock = MagicMock()
241
+ skill_mock.truth_check = MagicMock(side_effect=AssertionError("should not be called"))
242
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
243
+ result = verify_before_promotion(home, entry2)
244
+ assert result.should_promote is True
245
+ skill_mock.truth_check.assert_not_called()
246
+
247
+ def test_low_coherence_no_fragments_also_blocked(self, home: Path) -> None:
248
+ """is_aligned=False with empty fragments still triggers a conflict."""
249
+ entry = _make_entry()
250
+ skill_mock = MagicMock()
251
+ # No fragments, but is_aligned=False
252
+ skill_mock.truth_check = MagicMock(return_value={
253
+ "is_aligned": False,
254
+ "collider_result": {
255
+ "coherence_score": 0.4,
256
+ "truth_grade": "weak",
257
+ "collision_fragments": [],
258
+ },
259
+ })
260
+ with patch.dict(sys.modules, {"skseed": MagicMock(), "skseed.skill": skill_mock}):
261
+ with patch("skcapstone.memory_verifier._fire_conflict_notification"):
262
+ result = verify_before_promotion(home, entry)
263
+ assert result.should_promote is False
264
+ assert result.is_conflicting is True
265
+
266
+
267
+ # ---------------------------------------------------------------------------
268
+ # Integration: memory_engine._promote() gate
269
+ # ---------------------------------------------------------------------------
270
+
271
+
272
+ class TestMemoryEnginePromoteGate:
273
+
274
+ def test_engine_promote_short_term_blocked(self, home: Path) -> None:
275
+ """_promote() in memory_engine stays in short-term when gate says no."""
276
+ from skcapstone import memory_engine
277
+
278
+ entry = _make_entry(memory_id="eng01")
279
+ path = home / "memory" / "short-term" / "eng01.json"
280
+ path.write_text(entry.model_dump_json(), encoding="utf-8")
281
+
282
+ def _fake_verify(h, e):
283
+ return VerificationResult(
284
+ should_promote=False,
285
+ is_conflicting=True,
286
+ coherence_score=0.3,
287
+ truth_grade="weak",
288
+ collision_fragments=["contradiction"],
289
+ )
290
+
291
+ # Patch at the verifier module level (local import picks it up there)
292
+ with patch("skcapstone.memory_verifier.verify_before_promotion", _fake_verify):
293
+ promoted = memory_engine._promote(home, entry, path)
294
+
295
+ assert promoted is False
296
+ # Entry should still be in short-term
297
+ assert path.exists()
298
+
299
+ def test_engine_promote_short_term_allowed(self, home: Path) -> None:
300
+ """_promote() in memory_engine moves to mid-term when gate passes."""
301
+ from skcapstone import memory_engine
302
+
303
+ entry = _make_entry(memory_id="eng02")
304
+ path = home / "memory" / "short-term" / "eng02.json"
305
+ path.write_text(entry.model_dump_json(), encoding="utf-8")
306
+
307
+ def _fake_verify(h, e):
308
+ return VerificationResult(should_promote=True, coherence_score=0.9)
309
+
310
+ with patch("skcapstone.memory_verifier.verify_before_promotion", _fake_verify):
311
+ promoted = memory_engine._promote(home, entry, path)
312
+
313
+ assert promoted is True
314
+ mid_path = home / "memory" / "mid-term" / "eng02.json"
315
+ assert mid_path.exists()
316
+
317
+ def test_engine_store_fast_path_blocked(self, home: Path) -> None:
318
+ """store() with importance>=0.7 stays in short-term when gate says no."""
319
+ from skcapstone import memory_engine
320
+
321
+ def _fake_verify(h, e):
322
+ return VerificationResult(should_promote=False, is_conflicting=True)
323
+
324
+ with patch("skcapstone.memory_verifier.verify_before_promotion", _fake_verify):
325
+ result = memory_engine.store(
326
+ home=home,
327
+ content="Important claim",
328
+ importance=0.8,
329
+ )
330
+
331
+ assert result.layer == MemoryLayer.SHORT_TERM
332
+
333
+ def test_engine_store_fast_path_allowed(self, home: Path) -> None:
334
+ """store() with importance>=0.7 promotes to mid-term when gate passes."""
335
+ from skcapstone import memory_engine
336
+
337
+ def _fake_verify(h, e):
338
+ return VerificationResult(should_promote=True, coherence_score=0.9)
339
+
340
+ with patch("skcapstone.memory_verifier.verify_before_promotion", _fake_verify):
341
+ result = memory_engine.store(
342
+ home=home,
343
+ content="Important aligned claim",
344
+ importance=0.8,
345
+ )
346
+
347
+ assert result.layer == MemoryLayer.MID_TERM
348
+
349
+
350
+ # ---------------------------------------------------------------------------
351
+ # Integration: PromotionEngine._promote() gate
352
+ # ---------------------------------------------------------------------------
353
+
354
+
355
+ class TestPromotionEngineGate:
356
+
357
+ def test_promoter_sweep_blocked_counts_as_skipped(self, home: Path) -> None:
358
+ """A truth-check-blocked candidate is not in result.promoted."""
359
+ from datetime import timedelta
360
+
361
+ from skcapstone.memory_promoter import PromotionEngine, PromotionThresholds
362
+
363
+ # Write a short-term entry that scores well above threshold
364
+ from skcapstone.memory_engine import _save_entry
365
+
366
+ created = datetime.now(timezone.utc) - timedelta(hours=30)
367
+ entry = MemoryEntry(
368
+ memory_id="sweep01",
369
+ content="decision: use sovereign architecture",
370
+ tags=["architect", "decision", "breakthrough"],
371
+ source="test",
372
+ layer=MemoryLayer.SHORT_TERM,
373
+ importance=0.9,
374
+ access_count=5,
375
+ created_at=created,
376
+ )
377
+ _save_entry(home, entry)
378
+
379
+ def _fake_verify(h, e):
380
+ return VerificationResult(should_promote=False, is_conflicting=True)
381
+
382
+ engine = PromotionEngine(home, PromotionThresholds(short_to_mid=0.1))
383
+ with patch("skcapstone.memory_verifier.verify_before_promotion", _fake_verify):
384
+ result = engine.sweep(dry_run=False)
385
+
386
+ # Candidate was found but not promoted
387
+ assert not any(c.memory_id == "sweep01" and c.promoted for c in result.candidates)
388
+
389
+ def test_promoter_sweep_allowed_promotes(self, home: Path) -> None:
390
+ """A truth-check-allowed candidate ends up in mid-term."""
391
+ from datetime import timedelta
392
+
393
+ from skcapstone.memory_promoter import PromotionEngine, PromotionThresholds
394
+ from skcapstone.memory_engine import _save_entry
395
+
396
+ created = datetime.now(timezone.utc) - timedelta(hours=30)
397
+ entry = MemoryEntry(
398
+ memory_id="sweep02",
399
+ content="decision: sovereign architecture approved",
400
+ tags=["architect", "decision"],
401
+ source="test",
402
+ layer=MemoryLayer.SHORT_TERM,
403
+ importance=0.9,
404
+ access_count=5,
405
+ created_at=created,
406
+ )
407
+ _save_entry(home, entry)
408
+
409
+ def _fake_verify(h, e):
410
+ return VerificationResult(should_promote=True, coherence_score=0.95)
411
+
412
+ engine = PromotionEngine(home, PromotionThresholds(short_to_mid=0.1))
413
+ # Sweep short-term only so the entry isn't double-promoted in one pass
414
+ with patch("skcapstone.memory_verifier.verify_before_promotion", _fake_verify):
415
+ result = engine.sweep(layer=MemoryLayer.SHORT_TERM, dry_run=False)
416
+
417
+ promoted_ids = [c.memory_id for c in result.promoted if c.promoted]
418
+ assert "sweep02" in promoted_ids
419
+ mid_path = home / "memory" / "mid-term" / "sweep02.json"
420
+ assert mid_path.exists()
@@ -0,0 +1,187 @@
1
+ """Tests for skcapstone.message_crypto — AES-256-GCM message encryption.
2
+
3
+ Covers:
4
+ - encrypt_message / decrypt_message happy-path roundtrip
5
+ - Wrong key raises InvalidTag (authentication failure)
6
+ - Tampered ciphertext raises InvalidTag
7
+ - Short key raises ValueError
8
+ - pack_encrypted / unpack_encrypted envelope helpers
9
+ - is_encrypted_content detection
10
+ - decrypt_content pass-through for plaintext
11
+ - KMS-backed encrypt_content / decrypt_content roundtrip
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ from pathlib import Path
19
+
20
+ import pytest
21
+
22
+ from skcapstone.message_crypto import (
23
+ decrypt_content,
24
+ decrypt_message,
25
+ encrypt_content,
26
+ encrypt_message,
27
+ is_encrypted_content,
28
+ pack_encrypted,
29
+ unpack_encrypted,
30
+ )
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Fixtures
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ @pytest.fixture
39
+ def aes_key() -> bytes:
40
+ """Return a random 32-byte AES key for testing."""
41
+ return os.urandom(32)
42
+
43
+
44
+ @pytest.fixture
45
+ def kms_home(tmp_path: Path) -> Path:
46
+ """Create a minimal agent home with identity for KMS key derivation."""
47
+ identity_dir = tmp_path / "identity"
48
+ identity_dir.mkdir(parents=True)
49
+ identity = {
50
+ "name": "test-agent",
51
+ "email": "test@skcapstone.local",
52
+ "fingerprint": "DEADBEEF1234567890ABCDEF1234567890ABCDEF",
53
+ "capauth_managed": False,
54
+ }
55
+ (identity_dir / "identity.json").write_text(json.dumps(identity), encoding="utf-8")
56
+ (tmp_path / "security").mkdir(parents=True)
57
+ return tmp_path
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Low-level encrypt/decrypt
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ def test_encrypt_decrypt_roundtrip(aes_key):
66
+ """Happy path: encrypt then decrypt returns the original plaintext."""
67
+ plaintext = "Hello, sovereign world! 🔒"
68
+ token = encrypt_message(plaintext, aes_key)
69
+ assert isinstance(token, str)
70
+ assert len(token) > 0
71
+ result = decrypt_message(token, aes_key)
72
+ assert result == plaintext
73
+
74
+
75
+ def test_encrypt_produces_different_ciphertext_each_call(aes_key):
76
+ """Each encryption call uses a fresh nonce → different output."""
77
+ msg = "repeat me"
78
+ t1 = encrypt_message(msg, aes_key)
79
+ t2 = encrypt_message(msg, aes_key)
80
+ assert t1 != t2, "Nonces must differ — ciphertexts should not be identical"
81
+
82
+
83
+ def test_wrong_key_raises_on_decrypt(aes_key):
84
+ """Decrypting with a different key raises InvalidTag (AES-GCM auth failure)."""
85
+ from cryptography.exceptions import InvalidTag
86
+
87
+ token = encrypt_message("secret", aes_key)
88
+ wrong_key = os.urandom(32)
89
+ with pytest.raises(InvalidTag):
90
+ decrypt_message(token, wrong_key)
91
+
92
+
93
+ def test_tampered_ciphertext_raises(aes_key):
94
+ """Bit-flipping the ciphertext causes authentication to fail."""
95
+ import base64
96
+ from cryptography.exceptions import InvalidTag
97
+
98
+ token = encrypt_message("tamper me", aes_key)
99
+ raw = bytearray(base64.b64decode(token))
100
+ raw[-1] ^= 0xFF # flip the last byte (inside the GCM tag)
101
+ bad_token = base64.b64encode(bytes(raw)).decode("ascii")
102
+ with pytest.raises(InvalidTag):
103
+ decrypt_message(bad_token, aes_key)
104
+
105
+
106
+ def test_short_key_raises_value_error():
107
+ """A key shorter than 32 bytes should raise ValueError immediately."""
108
+ with pytest.raises(ValueError, match="32-byte"):
109
+ encrypt_message("oops", b"too-short")
110
+
111
+
112
+ def test_short_key_decrypt_raises_value_error():
113
+ """decrypt_message with a short key raises ValueError."""
114
+ with pytest.raises(ValueError, match="32-byte"):
115
+ decrypt_message("sometoken", b"too-short")
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Envelope helpers
120
+ # ---------------------------------------------------------------------------
121
+
122
+
123
+ def test_pack_unpack_roundtrip(aes_key):
124
+ """pack_encrypted wraps token; unpack_encrypted recovers it."""
125
+ token = encrypt_message("envelope test", aes_key)
126
+ envelope = pack_encrypted(token)
127
+ data = json.loads(envelope)
128
+ assert data["skchat_encrypted"] is True
129
+ assert data["v"] == 1
130
+ assert data["ciphertext"] == token
131
+ recovered = unpack_encrypted(envelope)
132
+ assert recovered == token
133
+
134
+
135
+ def test_unpack_returns_none_for_plaintext():
136
+ """unpack_encrypted on plain text returns None."""
137
+ assert unpack_encrypted("just a normal message") is None
138
+
139
+
140
+ def test_unpack_returns_none_for_invalid_json():
141
+ """unpack_encrypted on malformed JSON returns None."""
142
+ assert unpack_encrypted("{not valid json") is None
143
+
144
+
145
+ def test_is_encrypted_content_true(aes_key):
146
+ """is_encrypted_content returns True for a proper envelope."""
147
+ token = encrypt_message("check me", aes_key)
148
+ envelope = pack_encrypted(token)
149
+ assert is_encrypted_content(envelope) is True
150
+
151
+
152
+ def test_is_encrypted_content_false_for_plaintext():
153
+ """is_encrypted_content returns False for plain strings."""
154
+ assert is_encrypted_content("hello world") is False
155
+ assert is_encrypted_content("") is False
156
+ assert is_encrypted_content('{"some": "json"}') is False
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # KMS-backed API
161
+ # ---------------------------------------------------------------------------
162
+
163
+
164
+ def test_kms_encrypt_decrypt_roundtrip(kms_home):
165
+ """encrypt_content → decrypt_content using KMS-derived key roundtrips cleanly."""
166
+ plaintext = "sovereign message via KMS"
167
+ envelope = encrypt_content(plaintext, kms_home)
168
+ assert is_encrypted_content(envelope), "Output must be an encrypted envelope"
169
+ result = decrypt_content(envelope, kms_home)
170
+ assert result == plaintext
171
+
172
+
173
+ def test_decrypt_content_passthrough_for_plaintext(kms_home):
174
+ """decrypt_content leaves non-encrypted content unchanged."""
175
+ plain = "nothing to see here"
176
+ result = decrypt_content(plain, kms_home)
177
+ assert result == plain
178
+
179
+
180
+ def test_kms_same_key_produced_across_calls(kms_home):
181
+ """Two derive_chat_key calls return identical material (deterministic HKDF)."""
182
+ from skcapstone.message_crypto import derive_chat_key
183
+
184
+ key1 = derive_chat_key(kms_home)
185
+ key2 = derive_chat_key(kms_home)
186
+ assert key1 == key2, "HKDF derivation must be deterministic for same identity"
187
+ assert len(key1) == 32