@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,581 @@
1
+ """
2
+ Unit tests for skcapstone Vault (sync/vault.py).
3
+
4
+ Covers packing, unpacking, integrity verification, key helpers,
5
+ exclusion logic, manifest models, and encryption paths with mocked GPG.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import tarfile
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from unittest.mock import MagicMock, patch
16
+
17
+ import pytest
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Fixtures
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ @pytest.fixture
26
+ def agent_home(tmp_path: Path) -> Path:
27
+ home = tmp_path / ".skcapstone"
28
+ home.mkdir()
29
+
30
+ for pillar in ("identity", "memory", "trust", "config", "skills"):
31
+ (home / pillar).mkdir()
32
+
33
+ (home / "identity" / "identity.json").write_text(
34
+ json.dumps({
35
+ "name": "VaultTestAgent",
36
+ "fingerprint": "BBBB2222CCCC3333DDDD4444EEEE5555FFFF6666",
37
+ })
38
+ )
39
+ (home / "trust" / "trust.json").write_text(
40
+ json.dumps({"depth": 4.0, "trust_level": 0.85})
41
+ )
42
+ (home / "config" / "config.yaml").write_text("agent_name: VaultTestAgent\n")
43
+ (home / "manifest.json").write_text(
44
+ json.dumps({"name": "VaultTestAgent", "version": "0.2.0", "connectors": []})
45
+ )
46
+ for layer in ("short-term", "mid-term", "long-term"):
47
+ (home / "memory" / layer).mkdir(parents=True)
48
+ (home / "memory" / "long-term" / "important.json").write_text(
49
+ json.dumps({"content": "vault test memory"})
50
+ )
51
+ return home
52
+
53
+
54
+ @pytest.fixture
55
+ def vault(agent_home: Path):
56
+ from skcapstone.sync.vault import Vault
57
+
58
+ return Vault(agent_home)
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Helper functions
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ class TestHashHelpers:
67
+ def test_sha256_file(self, tmp_path: Path):
68
+ from skcapstone.sync.vault import _sha256_file
69
+
70
+ f = tmp_path / "data.bin"
71
+ f.write_bytes(b"hello world")
72
+ digest = _sha256_file(f)
73
+ expected = hashlib.sha256(b"hello world").hexdigest()
74
+ assert digest == expected
75
+ assert len(digest) == 64
76
+
77
+ def test_sha256_bytes(self):
78
+ from skcapstone.sync.vault import _sha256_bytes
79
+
80
+ digest = _sha256_bytes(b"sovereign")
81
+ expected = hashlib.sha256(b"sovereign").hexdigest()
82
+ assert digest == expected
83
+ assert len(digest) == 64
84
+
85
+ def test_sha256_empty_bytes(self):
86
+ from skcapstone.sync.vault import _sha256_bytes
87
+
88
+ digest = _sha256_bytes(b"")
89
+ assert digest == hashlib.sha256(b"").hexdigest()
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Vault construction
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ class TestVaultConstruction:
98
+ def test_creates_vault_dir(self, agent_home: Path):
99
+ from skcapstone.sync.vault import Vault
100
+
101
+ Vault(agent_home)
102
+ assert (agent_home / "vault").is_dir()
103
+
104
+ def test_expands_home_tilde(self, tmp_path: Path):
105
+ """Vault should call expanduser() on agent_home."""
106
+ real_home = tmp_path / "agent"
107
+ real_home.mkdir()
108
+ from skcapstone.sync.vault import Vault
109
+
110
+ v = Vault(real_home)
111
+ assert v.agent_home == real_home
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Exclusion logic
116
+ # ---------------------------------------------------------------------------
117
+
118
+
119
+ class TestShouldExclude:
120
+ def test_excludes_pycache(self, vault):
121
+ assert vault._should_exclude("__pycache__") is True
122
+
123
+ def test_excludes_pyc(self, vault):
124
+ assert vault._should_exclude("compiled.pyc") is True
125
+
126
+ def test_excludes_git(self, vault):
127
+ assert vault._should_exclude(".git") is True
128
+
129
+ def test_excludes_audit_log(self, vault):
130
+ assert vault._should_exclude("audit.log") is True
131
+
132
+ def test_allows_normal_files(self, vault):
133
+ assert vault._should_exclude("identity.json") is False
134
+ assert vault._should_exclude("trust.json") is False
135
+ assert vault._should_exclude("memories.json") is False
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # pack
140
+ # ---------------------------------------------------------------------------
141
+
142
+
143
+ class TestVaultPack:
144
+ def test_pack_archive_naming(self, vault):
145
+ """Archive name should match vault-<host>-<timestamp>.tar.gz pattern."""
146
+ path = vault.pack(encrypt=False)
147
+ assert path.name.startswith("vault-")
148
+ assert path.name.endswith(".tar.gz")
149
+
150
+ def test_pack_archive_in_vault_dir(self, vault, agent_home: Path):
151
+ path = vault.pack(encrypt=False)
152
+ assert path.parent == agent_home / "vault"
153
+
154
+ def test_pack_creates_manifest_file(self, vault):
155
+ path = vault.pack(encrypt=False)
156
+ manifest = path.with_suffix(".manifest.json")
157
+ assert manifest.exists()
158
+
159
+ def test_manifest_agent_name_from_manifest_json(self, vault):
160
+ path = vault.pack(encrypt=False)
161
+ data = json.loads(path.with_suffix(".manifest.json").read_text())
162
+ assert data["agent_name"] == "VaultTestAgent"
163
+
164
+ def test_manifest_includes_all_pillars(self, vault):
165
+ path = vault.pack(encrypt=False)
166
+ data = json.loads(path.with_suffix(".manifest.json").read_text())
167
+ included = data["pillars_included"]
168
+ assert "identity" in included
169
+ assert "trust" in included
170
+ assert "config" in included
171
+ assert "memory" in included
172
+
173
+ def test_pack_custom_pillar_selection(self, vault):
174
+ """Only requested pillars should be included."""
175
+ path = vault.pack(pillars=["identity", "trust"], encrypt=False)
176
+ with tarfile.open(path, "r:gz") as tar:
177
+ names = tar.getnames()
178
+ assert any("identity/" in n for n in names)
179
+ assert any("trust/" in n for n in names)
180
+ assert not any("config/" in n for n in names)
181
+
182
+ def test_pack_skips_missing_pillar(self, vault):
183
+ """A pillar that doesn't exist on disk is silently skipped."""
184
+ path = vault.pack(pillars=["identity", "nonexistent"], encrypt=False)
185
+ data = json.loads(path.with_suffix(".manifest.json").read_text())
186
+ assert "identity" in data["pillars_included"]
187
+ assert "nonexistent" not in data["pillars_included"]
188
+
189
+ def test_pack_excludes_pycache_files(self, vault, agent_home: Path):
190
+ """__pycache__ and .pyc must not appear in the archive."""
191
+ pycache = agent_home / "identity" / "__pycache__"
192
+ pycache.mkdir()
193
+ (pycache / "cached.pyc").write_bytes(b"junk bytecode")
194
+
195
+ path = vault.pack(encrypt=False)
196
+ with tarfile.open(path, "r:gz") as tar:
197
+ names = tar.getnames()
198
+ assert not any("__pycache__" in n for n in names)
199
+ assert not any(".pyc" in n for n in names)
200
+
201
+ def test_pack_includes_manifest_json(self, vault, agent_home: Path):
202
+ """Top-level manifest.json should be bundled in the archive."""
203
+ path = vault.pack(encrypt=False)
204
+ with tarfile.open(path, "r:gz") as tar:
205
+ assert "manifest.json" in tar.getnames()
206
+
207
+ def test_pack_skips_manifest_if_not_present(self, vault, agent_home: Path):
208
+ """Pack should not crash when manifest.json doesn't exist."""
209
+ (agent_home / "manifest.json").unlink()
210
+ path = vault.pack(encrypt=False)
211
+ assert path.exists()
212
+
213
+ def test_pack_file_hashes_in_manifest(self, vault):
214
+ path = vault.pack(encrypt=False)
215
+ data = json.loads(path.with_suffix(".manifest.json").read_text())
216
+ assert isinstance(data["file_hashes"], dict)
217
+ assert len(data["file_hashes"]) > 0
218
+ for rel_path, h in data["file_hashes"].items():
219
+ assert len(h) == 64, f"Bad hash length for {rel_path}"
220
+
221
+ def test_pack_archive_hash_in_manifest(self, vault):
222
+ from skcapstone.sync.vault import _sha256_file
223
+
224
+ path = vault.pack(encrypt=False)
225
+ data = json.loads(path.with_suffix(".manifest.json").read_text())
226
+ assert data["archive_hash"] == _sha256_file(path)
227
+
228
+ def test_pack_schema_version(self, vault):
229
+ path = vault.pack(encrypt=False)
230
+ data = json.loads(path.with_suffix(".manifest.json").read_text())
231
+ assert data["schema_version"] == "1.1"
232
+
233
+ def test_pack_encrypted_flag_false(self, vault):
234
+ path = vault.pack(encrypt=False)
235
+ data = json.loads(path.with_suffix(".manifest.json").read_text())
236
+ assert data["encrypted"] is False
237
+
238
+ def test_pack_fingerprint_from_identity(self, vault):
239
+ path = vault.pack(encrypt=False)
240
+ data = json.loads(path.with_suffix(".manifest.json").read_text())
241
+ assert data["fingerprint"] == "BBBB2222CCCC3333DDDD4444EEEE5555FFFF6666"
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # pack + encryption (mocked GPG)
246
+ # ---------------------------------------------------------------------------
247
+
248
+
249
+ class TestVaultPackEncrypt:
250
+ def test_pack_encrypt_returns_gpg_path(self, vault):
251
+ """pack(encrypt=True) should return a .tar.gz.gpg path."""
252
+ def fake_encrypt(archive_path, passphrase):
253
+ # Create the .gpg output; pack() will unlink the original itself
254
+ gpg_path = archive_path.with_suffix(archive_path.suffix + ".gpg")
255
+ gpg_path.write_bytes(b"encrypted")
256
+ return gpg_path
257
+
258
+ with patch.object(vault, "_encrypt_vault", side_effect=fake_encrypt):
259
+ result = vault.pack(encrypt=True, passphrase="secret")
260
+
261
+ assert result.name.endswith(".gpg")
262
+
263
+ def test_pack_encrypt_removes_plaintext(self, vault):
264
+ """The plaintext .tar.gz should be deleted after encryption."""
265
+ def fake_encrypt(archive_path, passphrase):
266
+ gpg_path = archive_path.with_suffix(archive_path.suffix + ".gpg")
267
+ gpg_path.write_bytes(b"encrypted")
268
+ return gpg_path
269
+
270
+ with patch.object(vault, "_encrypt_vault", side_effect=fake_encrypt):
271
+ result = vault.pack(encrypt=True, passphrase="secret")
272
+
273
+ plain = result.with_name(result.name[:-4]) # strip .gpg
274
+ assert not plain.exists()
275
+ assert result.exists()
276
+
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # unpack
280
+ # ---------------------------------------------------------------------------
281
+
282
+
283
+ class TestVaultUnpack:
284
+ def test_unpack_extracts_files(self, vault, tmp_path: Path):
285
+ archive = vault.pack(encrypt=False)
286
+ restore = tmp_path / "restored"
287
+ restore.mkdir()
288
+ result = vault.unpack(archive, target=restore, verify_signature=False)
289
+ assert result == restore
290
+ assert (restore / "identity" / "identity.json").exists()
291
+ assert (restore / "trust" / "trust.json").exists()
292
+
293
+ def test_unpack_default_target_is_agent_home(self, vault, agent_home: Path, tmp_path: Path):
294
+ """Without explicit target, unpack extracts to agent_home."""
295
+ alt_vault = Vault_helper(agent_home)
296
+ archive = alt_vault.pack(encrypt=False)
297
+ result = alt_vault.unpack(archive, verify_signature=False)
298
+ assert result == agent_home
299
+
300
+ def test_unpack_without_manifest(self, vault, tmp_path: Path, agent_home: Path):
301
+ """Unpack works gracefully when no manifest file exists."""
302
+ archive = vault.pack(encrypt=False)
303
+ manifest = archive.with_suffix(".manifest.json")
304
+ manifest.unlink()
305
+
306
+ restore = tmp_path / "no-manifest"
307
+ restore.mkdir()
308
+ result = vault.unpack(archive, target=restore, verify_signature=False)
309
+ assert result == restore
310
+
311
+ def test_unpack_detects_tampered_archive(self, vault, tmp_path: Path):
312
+ """Appending bytes to archive triggers VaultIntegrityError."""
313
+ from skcapstone.sync.vault import VaultIntegrityError
314
+
315
+ archive = vault.pack(encrypt=False)
316
+ with open(archive, "ab") as f:
317
+ f.write(b"\x00TAMPERED\x00")
318
+
319
+ restore = tmp_path / "tampered"
320
+ restore.mkdir()
321
+ with pytest.raises(VaultIntegrityError, match="[Hh]ash mismatch"):
322
+ vault.unpack(archive, target=restore, verify_signature=False)
323
+
324
+ def test_unpack_detects_tampered_file_via_manifest(self, vault, tmp_path: Path, agent_home: Path):
325
+ """Faking a file hash in manifest triggers VaultIntegrityError on extraction."""
326
+ from skcapstone.sync.vault import VaultIntegrityError
327
+
328
+ archive = vault.pack(encrypt=False)
329
+ manifest_path = archive.with_suffix(".manifest.json")
330
+ data = json.loads(manifest_path.read_text())
331
+
332
+ # Nullify archive_hash to pass that check, corrupt a file hash
333
+ data["archive_hash"] = None
334
+ first_key = next(iter(data["file_hashes"]))
335
+ data["file_hashes"][first_key] = "deadbeef" * 8
336
+ manifest_path.write_text(json.dumps(data))
337
+
338
+ restore = tmp_path / "file-tampered"
339
+ restore.mkdir()
340
+ with pytest.raises(VaultIntegrityError, match="[Hh]ash mismatch"):
341
+ vault.unpack(archive, target=restore, verify_signature=False)
342
+
343
+ def test_unpack_skip_hash_verification(self, vault, tmp_path: Path):
344
+ """verify_hashes=False skips all integrity checks."""
345
+ archive = vault.pack(encrypt=False)
346
+ with open(archive, "ab") as f:
347
+ f.write(b"\x00CORRUPT\x00")
348
+
349
+ restore = tmp_path / "skip"
350
+ restore.mkdir()
351
+ # Should not raise
352
+ vault.unpack(archive, target=restore, verify_signature=False, verify_hashes=False)
353
+
354
+ def test_unpack_with_tampered_archive_hash_in_manifest(self, vault, tmp_path: Path):
355
+ """If manifest archive_hash is wrong, VaultIntegrityError is raised."""
356
+ from skcapstone.sync.vault import VaultIntegrityError
357
+
358
+ archive = vault.pack(encrypt=False)
359
+ manifest_path = archive.with_suffix(".manifest.json")
360
+ data = json.loads(manifest_path.read_text())
361
+ data["archive_hash"] = "0" * 64
362
+ manifest_path.write_text(json.dumps(data))
363
+
364
+ restore = tmp_path / "bad-hash"
365
+ restore.mkdir()
366
+ with pytest.raises(VaultIntegrityError, match="Archive hash mismatch"):
367
+ vault.unpack(archive, target=restore, verify_signature=False)
368
+
369
+ def test_unpack_valid_hashes_succeeds(self, vault, tmp_path: Path):
370
+ archive = vault.pack(encrypt=False)
371
+ restore = tmp_path / "valid"
372
+ restore.mkdir()
373
+ result = vault.unpack(archive, target=restore, verify_signature=False)
374
+ assert result == restore
375
+ assert (restore / "manifest.json").exists()
376
+
377
+
378
+ # ---------------------------------------------------------------------------
379
+ # _get_agent_name and _get_agent_fingerprint
380
+ # ---------------------------------------------------------------------------
381
+
382
+
383
+ class TestVaultAgentHelpers:
384
+ def test_get_agent_name_from_manifest(self, vault):
385
+ assert vault._get_agent_name() == "VaultTestAgent"
386
+
387
+ def test_get_agent_name_no_manifest(self, vault, agent_home: Path):
388
+ (agent_home / "manifest.json").unlink()
389
+ assert vault._get_agent_name() == "unknown"
390
+
391
+ def test_get_agent_name_corrupt_manifest(self, vault, agent_home: Path):
392
+ (agent_home / "manifest.json").write_text("NOT JSON {{{")
393
+ assert vault._get_agent_name() == "unknown"
394
+
395
+ def test_get_agent_fingerprint_from_identity(self, vault):
396
+ fp = vault._get_agent_fingerprint()
397
+ assert fp == "BBBB2222CCCC3333DDDD4444EEEE5555FFFF6666"
398
+
399
+ def test_get_agent_fingerprint_no_identity_file(self, vault, agent_home: Path):
400
+ (agent_home / "identity" / "identity.json").unlink()
401
+ assert vault._get_agent_fingerprint() is None
402
+
403
+ def test_get_agent_fingerprint_corrupt_identity(self, vault, agent_home: Path):
404
+ (agent_home / "identity" / "identity.json").write_text("{BROKEN}")
405
+ assert vault._get_agent_fingerprint() is None
406
+
407
+ def test_get_agent_fingerprint_no_identity_dir(self, vault, agent_home: Path):
408
+ import shutil
409
+
410
+ shutil.rmtree(agent_home / "identity")
411
+ assert vault._get_agent_fingerprint() is None
412
+
413
+
414
+ # ---------------------------------------------------------------------------
415
+ # list_vaults
416
+ # ---------------------------------------------------------------------------
417
+
418
+
419
+ class TestListVaults:
420
+ def test_empty_vault_dir(self, vault):
421
+ assert vault.list_vaults() == []
422
+
423
+ def test_single_vault(self, vault):
424
+ vault.pack(encrypt=False)
425
+ result = vault.list_vaults()
426
+ assert len(result) == 1
427
+
428
+ def test_two_vaults(self, vault):
429
+ import time
430
+
431
+ vault.pack(encrypt=False)
432
+ time.sleep(1.1)
433
+ vault.pack(encrypt=False)
434
+ result = vault.list_vaults()
435
+ assert len(result) == 2
436
+
437
+ def test_list_includes_metadata(self, vault):
438
+ vault.pack(encrypt=False)
439
+ entries = vault.list_vaults()
440
+ entry = entries[0]
441
+ assert "path" in entry
442
+ assert "size" in entry
443
+ # Manifest fields should be merged
444
+ assert "agent_name" in entry
445
+
446
+ def test_manifest_json_files_excluded(self, vault, agent_home: Path):
447
+ """list_vaults should not count .manifest.json files as vaults."""
448
+ vault.pack(encrypt=False)
449
+ entries = vault.list_vaults()
450
+ for e in entries:
451
+ assert not str(e["path"]).endswith(".json")
452
+
453
+
454
+ # ---------------------------------------------------------------------------
455
+ # VaultManifest model
456
+ # ---------------------------------------------------------------------------
457
+
458
+
459
+ class TestVaultManifestModel:
460
+ def test_default_schema_version(self):
461
+ from skcapstone.sync.models import VaultManifest
462
+
463
+ m = VaultManifest(
464
+ agent_name="A", source_host="h", created_at=datetime.now(timezone.utc)
465
+ )
466
+ assert m.schema_version == "1.1"
467
+
468
+ def test_default_encrypted_true(self):
469
+ from skcapstone.sync.models import VaultManifest
470
+
471
+ m = VaultManifest(
472
+ agent_name="A", source_host="h", created_at=datetime.now(timezone.utc)
473
+ )
474
+ assert m.encrypted is True
475
+
476
+ def test_default_pillars_empty(self):
477
+ from skcapstone.sync.models import VaultManifest
478
+
479
+ m = VaultManifest(
480
+ agent_name="A", source_host="h", created_at=datetime.now(timezone.utc)
481
+ )
482
+ assert m.pillars_included == []
483
+
484
+ def test_default_file_hashes_empty(self):
485
+ from skcapstone.sync.models import VaultManifest
486
+
487
+ m = VaultManifest(
488
+ agent_name="A", source_host="h", created_at=datetime.now(timezone.utc)
489
+ )
490
+ assert m.file_hashes == {}
491
+
492
+ def test_roundtrip_json(self):
493
+ from skcapstone.sync.models import VaultManifest
494
+
495
+ m = VaultManifest(
496
+ agent_name="Roundtrip",
497
+ source_host="node01",
498
+ created_at=datetime(2026, 2, 28, tzinfo=timezone.utc),
499
+ pillars_included=["identity", "memory"],
500
+ encrypted=False,
501
+ file_hashes={"identity/identity.json": "a" * 64},
502
+ archive_hash="b" * 64,
503
+ fingerprint="FFFFFFFFFFFFFFFFFFFFFFFF",
504
+ )
505
+ restored = VaultManifest.model_validate_json(m.model_dump_json())
506
+ assert restored.agent_name == "Roundtrip"
507
+ assert restored.encrypted is False
508
+ assert restored.file_hashes == {"identity/identity.json": "a" * 64}
509
+ assert restored.archive_hash == "b" * 64
510
+ assert restored.fingerprint == "FFFFFFFFFFFFFFFFFFFFFFFF"
511
+
512
+ def test_signature_fields(self):
513
+ from skcapstone.sync.models import VaultManifest
514
+
515
+ m = VaultManifest(
516
+ agent_name="A",
517
+ source_host="h",
518
+ created_at=datetime.now(timezone.utc),
519
+ signature="SIG_BASE64",
520
+ signed_by="FP1234",
521
+ )
522
+ assert m.signature == "SIG_BASE64"
523
+ assert m.signed_by == "FP1234"
524
+
525
+
526
+ # ---------------------------------------------------------------------------
527
+ # VaultIntegrityError / VaultSignatureError exceptions
528
+ # ---------------------------------------------------------------------------
529
+
530
+
531
+ class TestVaultExceptions:
532
+ def test_integrity_error_is_exception(self):
533
+ from skcapstone.sync.vault import VaultIntegrityError
534
+
535
+ err = VaultIntegrityError("test")
536
+ assert isinstance(err, Exception)
537
+ assert str(err) == "test"
538
+
539
+ def test_signature_error_is_exception(self):
540
+ from skcapstone.sync.vault import VaultSignatureError
541
+
542
+ err = VaultSignatureError("bad sig")
543
+ assert isinstance(err, Exception)
544
+
545
+ def test_signature_error_raised_on_invalid_signature(self, vault, tmp_path: Path):
546
+ """unpack should raise VaultSignatureError for manifests with a bad signature."""
547
+ from skcapstone.sync.vault import VaultSignatureError
548
+
549
+ archive = vault.pack(encrypt=False)
550
+ manifest_path = archive.with_suffix(".manifest.json")
551
+ data = json.loads(manifest_path.read_text())
552
+ data["signature"] = "BADSIG"
553
+ data["archive_hash"] = None # skip archive hash check
554
+ manifest_path.write_text(json.dumps(data))
555
+
556
+ restore = tmp_path / "badsig"
557
+ restore.mkdir()
558
+ with patch.object(vault, "_verify_signature", return_value=False):
559
+ with pytest.raises(VaultSignatureError):
560
+ vault.unpack(
561
+ archive, target=restore, verify_signature=True, verify_hashes=False
562
+ )
563
+
564
+ def test_no_signature_does_not_raise(self, vault, tmp_path: Path):
565
+ """A manifest with no signature should not trigger VaultSignatureError."""
566
+ archive = vault.pack(encrypt=False)
567
+ restore = tmp_path / "nosig"
568
+ restore.mkdir()
569
+ # Should not raise even with verify_signature=True
570
+ vault.unpack(archive, target=restore, verify_signature=True, verify_hashes=False)
571
+
572
+
573
+ # ---------------------------------------------------------------------------
574
+ # Helpers for test reuse
575
+ # ---------------------------------------------------------------------------
576
+
577
+
578
+ def Vault_helper(agent_home: Path):
579
+ from skcapstone.sync.vault import Vault
580
+
581
+ return Vault(agent_home)