@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,571 @@
1
+ """Tests for Memory Fortress — integrity sealing, encryption, tamper alerts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from skcapstone.memory_fortress import (
12
+ MemoryFortress,
13
+ FortressConfig,
14
+ SealResult,
15
+ _SEAL_FIELD,
16
+ _SEALED_AT_FIELD,
17
+ _ENCRYPTED_FIELD,
18
+ )
19
+ from skcapstone.models import MemoryEntry, MemoryLayer
20
+
21
+
22
+ @pytest.fixture
23
+ def home(tmp_path: Path) -> Path:
24
+ """Create a minimal agent home for fortress tests."""
25
+ identity_dir = tmp_path / "identity"
26
+ identity_dir.mkdir()
27
+ manifest = {
28
+ "name": "test-agent",
29
+ "email": "test@skcapstone.local",
30
+ "fingerprint": "ABCD1234567890ABCDEF1234567890ABCDEF1234",
31
+ "capauth_managed": False,
32
+ }
33
+ (identity_dir / "identity.json").write_text(json.dumps(manifest), encoding="utf-8")
34
+
35
+ security_dir = tmp_path / "security"
36
+ security_dir.mkdir()
37
+
38
+ mem_dir = tmp_path / "memory"
39
+ mem_dir.mkdir()
40
+ for layer in ("short-term", "mid-term", "long-term"):
41
+ (mem_dir / layer).mkdir()
42
+
43
+ return tmp_path
44
+
45
+
46
+ @pytest.fixture
47
+ def seal_key() -> bytes:
48
+ """A deterministic seal key for tests."""
49
+ return b"test-fortress-seal-key-32-bytes!!"
50
+
51
+
52
+ @pytest.fixture
53
+ def fortress(home: Path, seal_key: bytes) -> MemoryFortress:
54
+ """Create an initialized MemoryFortress with explicit seal key."""
55
+ f = MemoryFortress(home, seal_key=seal_key)
56
+ f.initialize()
57
+ return f
58
+
59
+
60
+ @pytest.fixture
61
+ def sample_entry() -> MemoryEntry:
62
+ """A sample memory entry for testing."""
63
+ return MemoryEntry(
64
+ memory_id="abc123def456",
65
+ content="The sovereign agent remembers everything.",
66
+ tags=["test", "fortress"],
67
+ source="test",
68
+ layer=MemoryLayer.SHORT_TERM,
69
+ importance=0.8,
70
+ )
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Initialization
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ class TestInitialization:
79
+ """Tests for fortress setup."""
80
+
81
+ def test_initialize_creates_config(self, home: Path, seal_key: bytes) -> None:
82
+ """Initialize creates fortress.json config."""
83
+ f = MemoryFortress(home, seal_key=seal_key)
84
+ config = f.initialize()
85
+ assert config.enabled is True
86
+ assert (home / "memory" / "fortress.json").exists()
87
+
88
+ def test_initialize_idempotent(self, fortress: MemoryFortress) -> None:
89
+ """Multiple initializations don't break anything."""
90
+ c1 = fortress.initialize()
91
+ c2 = fortress.initialize()
92
+ assert c1.seal_algorithm == c2.seal_algorithm
93
+
94
+ def test_initialize_with_encryption(self, home: Path, seal_key: bytes) -> None:
95
+ """Initialize with encryption enabled creates config."""
96
+ f = MemoryFortress(home, seal_key=seal_key, encryption_enabled=True)
97
+ config = f.initialize()
98
+ assert config.encryption_enabled is True
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Sealing
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ class TestSealing:
107
+ """Tests for integrity seal operations."""
108
+
109
+ def test_seal_entry_adds_seal_field(
110
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry,
111
+ ) -> None:
112
+ """Sealing adds the __fortress_seal field."""
113
+ data = fortress.seal_entry(sample_entry)
114
+ assert _SEAL_FIELD in data
115
+ assert _SEALED_AT_FIELD in data
116
+
117
+ def test_seal_is_deterministic(
118
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry,
119
+ ) -> None:
120
+ """Same entry produces same seal (excluding timestamp)."""
121
+ data1 = fortress.seal_entry(sample_entry)
122
+ data2 = fortress.seal_entry(sample_entry)
123
+ # Seals match when the content is identical (timestamp excluded from seal)
124
+ # The seal covers the data dict which includes created_at etc.
125
+ assert data1[_SEAL_FIELD] == data2[_SEAL_FIELD]
126
+
127
+ def test_seal_changes_with_content(
128
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry,
129
+ ) -> None:
130
+ """Different content produces different seal."""
131
+ data1 = fortress.seal_entry(sample_entry)
132
+ sample_entry.content = "Something completely different"
133
+ data2 = fortress.seal_entry(sample_entry)
134
+ assert data1[_SEAL_FIELD] != data2[_SEAL_FIELD]
135
+
136
+ def test_seal_is_hex_string(
137
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry,
138
+ ) -> None:
139
+ """Seal is a 64-char hex string (SHA-256)."""
140
+ data = fortress.seal_entry(sample_entry)
141
+ seal = data[_SEAL_FIELD]
142
+ assert len(seal) == 64
143
+ assert all(c in "0123456789abcdef" for c in seal)
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Verify and Load
148
+ # ---------------------------------------------------------------------------
149
+
150
+
151
+ class TestVerifyAndLoad:
152
+ """Tests for integrity verification on load."""
153
+
154
+ def test_verify_sealed_entry(
155
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
156
+ ) -> None:
157
+ """Sealed entry passes verification."""
158
+ path = fortress.save_sealed(home, sample_entry)
159
+ entry, result = fortress.verify_and_load(path)
160
+ assert entry is not None
161
+ assert result.verified is True
162
+ assert result.tampered is False
163
+ assert entry.content == sample_entry.content
164
+
165
+ def test_detect_tampering(
166
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
167
+ ) -> None:
168
+ """Modified content is detected as tampering."""
169
+ path = fortress.save_sealed(home, sample_entry)
170
+
171
+ # Tamper with the file
172
+ data = json.loads(path.read_text(encoding="utf-8"))
173
+ data["content"] = "I have been tampered with!"
174
+ path.write_text(json.dumps(data, indent=2), encoding="utf-8")
175
+
176
+ entry, result = fortress.verify_and_load(path)
177
+ assert entry is None
178
+ assert result.tampered is True
179
+ assert result.verified is False
180
+ assert "tampering" in result.error.lower()
181
+
182
+ def test_detect_tag_tampering(
183
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
184
+ ) -> None:
185
+ """Modified tags are detected as tampering."""
186
+ path = fortress.save_sealed(home, sample_entry)
187
+
188
+ data = json.loads(path.read_text(encoding="utf-8"))
189
+ data["tags"] = ["hacked"]
190
+ path.write_text(json.dumps(data, indent=2), encoding="utf-8")
191
+
192
+ entry, result = fortress.verify_and_load(path)
193
+ assert result.tampered is True
194
+
195
+ def test_detect_importance_tampering(
196
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
197
+ ) -> None:
198
+ """Modified importance score is detected."""
199
+ path = fortress.save_sealed(home, sample_entry)
200
+
201
+ data = json.loads(path.read_text(encoding="utf-8"))
202
+ data["importance"] = 1.0
203
+ path.write_text(json.dumps(data, indent=2), encoding="utf-8")
204
+
205
+ entry, result = fortress.verify_and_load(path)
206
+ assert result.tampered is True
207
+
208
+ def test_legacy_unsealed_memory(self, fortress: MemoryFortress, home: Path) -> None:
209
+ """Legacy memories without seals load with verified=None."""
210
+ entry = MemoryEntry(
211
+ memory_id="legacy123",
212
+ content="Old memory without seal",
213
+ tags=["legacy"],
214
+ source="test",
215
+ layer=MemoryLayer.SHORT_TERM,
216
+ )
217
+ path = home / "memory" / "short-term" / "legacy123.json"
218
+ path.write_text(entry.model_dump_json(indent=2), encoding="utf-8")
219
+
220
+ loaded, result = fortress.verify_and_load(path)
221
+ assert loaded is not None
222
+ assert result.sealed is False
223
+ assert result.verified is None
224
+ assert result.tampered is False
225
+
226
+ def test_corrupt_json_file(self, fortress: MemoryFortress, home: Path) -> None:
227
+ """Corrupt JSON file returns error result."""
228
+ path = home / "memory" / "short-term" / "corrupt.json"
229
+ path.write_text("not valid json {{{", encoding="utf-8")
230
+
231
+ entry, result = fortress.verify_and_load(path)
232
+ assert entry is None
233
+ assert result.error is not None
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # Save Sealed
238
+ # ---------------------------------------------------------------------------
239
+
240
+
241
+ class TestSaveSealed:
242
+ """Tests for atomic sealed writes."""
243
+
244
+ def test_save_creates_file(
245
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
246
+ ) -> None:
247
+ """Save creates the sealed JSON file."""
248
+ path = fortress.save_sealed(home, sample_entry)
249
+ assert path.exists()
250
+
251
+ def test_saved_file_contains_seal(
252
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
253
+ ) -> None:
254
+ """Saved file contains the fortress seal."""
255
+ path = fortress.save_sealed(home, sample_entry)
256
+ data = json.loads(path.read_text(encoding="utf-8"))
257
+ assert _SEAL_FIELD in data
258
+ assert _SEALED_AT_FIELD in data
259
+
260
+ def test_roundtrip_save_load(
261
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
262
+ ) -> None:
263
+ """Save then load returns identical entry."""
264
+ path = fortress.save_sealed(home, sample_entry)
265
+ loaded, result = fortress.verify_and_load(path)
266
+ assert loaded is not None
267
+ assert loaded.memory_id == sample_entry.memory_id
268
+ assert loaded.content == sample_entry.content
269
+ assert loaded.tags == sample_entry.tags
270
+
271
+ def test_no_tmp_file_left(
272
+ self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
273
+ ) -> None:
274
+ """Atomic write leaves no .tmp file."""
275
+ path = fortress.save_sealed(home, sample_entry)
276
+ tmp = path.with_suffix(".json.tmp")
277
+ assert not tmp.exists()
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # Verify All
282
+ # ---------------------------------------------------------------------------
283
+
284
+
285
+ class TestVerifyAll:
286
+ """Tests for full memory scan."""
287
+
288
+ def test_verify_all_empty(self, fortress: MemoryFortress, home: Path) -> None:
289
+ """Verify all on empty memory returns empty."""
290
+ results = fortress.verify_all(home)
291
+ assert results == []
292
+
293
+ def test_verify_all_sealed(
294
+ self, fortress: MemoryFortress, home: Path,
295
+ ) -> None:
296
+ """Verify all with sealed memories passes."""
297
+ for i in range(3):
298
+ entry = MemoryEntry(
299
+ memory_id=f"mem{i:03d}",
300
+ content=f"Memory number {i}",
301
+ tags=["batch"],
302
+ source="test",
303
+ layer=MemoryLayer.SHORT_TERM,
304
+ )
305
+ fortress.save_sealed(home, entry)
306
+
307
+ results = fortress.verify_all(home)
308
+ assert len(results) == 3
309
+ assert all(r.verified is True for r in results)
310
+
311
+ def test_verify_all_detects_tampering(
312
+ self, fortress: MemoryFortress, home: Path,
313
+ ) -> None:
314
+ """Verify all detects tampered memories in batch."""
315
+ for i in range(3):
316
+ entry = MemoryEntry(
317
+ memory_id=f"scan{i:03d}",
318
+ content=f"Memory {i}",
319
+ tags=["scan"],
320
+ source="test",
321
+ layer=MemoryLayer.SHORT_TERM,
322
+ )
323
+ fortress.save_sealed(home, entry)
324
+
325
+ # Tamper with one
326
+ tampered_path = home / "memory" / "short-term" / "scan001.json"
327
+ data = json.loads(tampered_path.read_text(encoding="utf-8"))
328
+ data["content"] = "HACKED"
329
+ tampered_path.write_text(json.dumps(data), encoding="utf-8")
330
+
331
+ results = fortress.verify_all(home)
332
+ tampered = [r for r in results if r.tampered]
333
+ verified = [r for r in results if r.verified]
334
+ assert len(tampered) == 1
335
+ assert len(verified) == 2
336
+
337
+
338
+ # ---------------------------------------------------------------------------
339
+ # Seal Existing (Migration)
340
+ # ---------------------------------------------------------------------------
341
+
342
+
343
+ class TestSealExisting:
344
+ """Tests for migrating legacy memories."""
345
+
346
+ def test_seal_existing_memories(
347
+ self, fortress: MemoryFortress, home: Path,
348
+ ) -> None:
349
+ """Seal existing unsealed memories."""
350
+ for i in range(3):
351
+ entry = MemoryEntry(
352
+ memory_id=f"old{i:03d}",
353
+ content=f"Unsealed memory {i}",
354
+ tags=["legacy"],
355
+ source="test",
356
+ layer=MemoryLayer.MID_TERM,
357
+ )
358
+ path = home / "memory" / "mid-term" / f"old{i:03d}.json"
359
+ path.write_text(entry.model_dump_json(indent=2), encoding="utf-8")
360
+
361
+ sealed_count = fortress.seal_existing(home)
362
+ assert sealed_count == 3
363
+
364
+ # Verify they're now sealed
365
+ results = fortress.verify_all(home)
366
+ assert all(r.verified is True for r in results)
367
+
368
+ def test_seal_existing_skips_already_sealed(
369
+ self, fortress: MemoryFortress, home: Path,
370
+ ) -> None:
371
+ """Already-sealed memories are not re-sealed."""
372
+ entry = MemoryEntry(
373
+ memory_id="alreadysealed",
374
+ content="Already protected",
375
+ tags=["sealed"],
376
+ source="test",
377
+ layer=MemoryLayer.LONG_TERM,
378
+ )
379
+ fortress.save_sealed(home, entry)
380
+
381
+ sealed_count = fortress.seal_existing(home)
382
+ assert sealed_count == 0
383
+
384
+ def test_seal_existing_empty(
385
+ self, fortress: MemoryFortress, home: Path,
386
+ ) -> None:
387
+ """No memories to seal returns 0."""
388
+ assert fortress.seal_existing(home) == 0
389
+
390
+
391
+ # ---------------------------------------------------------------------------
392
+ # Encryption
393
+ # ---------------------------------------------------------------------------
394
+
395
+
396
+ class TestEncryption:
397
+ """Tests for at-rest encryption."""
398
+
399
+ def test_encrypted_roundtrip(self, home: Path) -> None:
400
+ """Encrypt then decrypt returns original content."""
401
+ fortress = MemoryFortress(home, encryption_enabled=True)
402
+ fortress.initialize()
403
+
404
+ entry = MemoryEntry(
405
+ memory_id="encrypted01",
406
+ content="Top secret sovereign data",
407
+ tags=["encrypted"],
408
+ source="test",
409
+ layer=MemoryLayer.SHORT_TERM,
410
+ importance=0.9,
411
+ )
412
+
413
+ path = fortress.save_sealed(home, entry)
414
+ loaded, result = fortress.verify_and_load(path)
415
+ assert loaded is not None
416
+ assert loaded.content == "Top secret sovereign data"
417
+ assert result.verified is True
418
+
419
+ def test_encrypted_content_not_plaintext(self, home: Path) -> None:
420
+ """Encrypted file does not contain plaintext content."""
421
+ fortress = MemoryFortress(home, encryption_enabled=True)
422
+ fortress.initialize()
423
+
424
+ entry = MemoryEntry(
425
+ memory_id="hidden01",
426
+ content="This text should be encrypted on disk",
427
+ tags=["secret"],
428
+ source="test",
429
+ layer=MemoryLayer.SHORT_TERM,
430
+ )
431
+
432
+ path = fortress.save_sealed(home, entry)
433
+ raw = path.read_text(encoding="utf-8")
434
+ assert "This text should be encrypted on disk" not in raw
435
+ assert _ENCRYPTED_FIELD in raw
436
+
437
+
438
+ # ---------------------------------------------------------------------------
439
+ # Different Seal Keys
440
+ # ---------------------------------------------------------------------------
441
+
442
+
443
+ class TestSealKeys:
444
+ """Tests for seal key behavior."""
445
+
446
+ def test_different_keys_different_seals(
447
+ self, home: Path, sample_entry: MemoryEntry,
448
+ ) -> None:
449
+ """Different seal keys produce different seals."""
450
+ f1 = MemoryFortress(home, seal_key=b"key-one-32-bytes-padded-here!!!!")
451
+ f1.initialize()
452
+ f2 = MemoryFortress(home, seal_key=b"key-two-32-bytes-padded-here!!!!")
453
+ f2.initialize()
454
+
455
+ data1 = f1.seal_entry(sample_entry)
456
+ data2 = f2.seal_entry(sample_entry)
457
+ assert data1[_SEAL_FIELD] != data2[_SEAL_FIELD]
458
+
459
+ def test_wrong_key_detects_tampering(
460
+ self, home: Path, sample_entry: MemoryEntry,
461
+ ) -> None:
462
+ """Loading with wrong seal key triggers tamper alert."""
463
+ f1 = MemoryFortress(home, seal_key=b"original-key-32-bytes-padded!!!")
464
+ f1.initialize()
465
+ path = f1.save_sealed(home, sample_entry)
466
+
467
+ f2 = MemoryFortress(home, seal_key=b"different-key-32-bytes-padded!!")
468
+ f2.initialize()
469
+ _, result = f2.verify_and_load(path)
470
+ assert result.tampered is True
471
+
472
+
473
+ # ---------------------------------------------------------------------------
474
+ # Status
475
+ # ---------------------------------------------------------------------------
476
+
477
+
478
+ class TestStatus:
479
+ """Tests for fortress status reporting."""
480
+
481
+ def test_status_returns_config(self, fortress: MemoryFortress) -> None:
482
+ """Status returns current config info."""
483
+ status = fortress.status()
484
+ assert status["enabled"] is True
485
+ assert status["seal_algorithm"] == "hmac-sha256"
486
+ assert status["has_seal_key"] is True
487
+
488
+ def test_status_reflects_encryption(self, home: Path) -> None:
489
+ """Status reflects encryption setting."""
490
+ f = MemoryFortress(home, seal_key=b"k" * 32, encryption_enabled=True)
491
+ f.initialize()
492
+ status = f.status()
493
+ assert status["encryption_enabled"] is True
494
+
495
+
496
+ # ---------------------------------------------------------------------------
497
+ # Audit Trail
498
+ # ---------------------------------------------------------------------------
499
+
500
+
501
+ class TestAuditTrail:
502
+ """Tests for security audit integration."""
503
+
504
+ def test_init_writes_audit(
505
+ self, home: Path, seal_key: bytes,
506
+ ) -> None:
507
+ """Initialization writes an audit event."""
508
+ f = MemoryFortress(home, seal_key=seal_key)
509
+ f.initialize()
510
+
511
+ audit_log = home / "security" / "audit.log"
512
+ assert audit_log.exists()
513
+ content = audit_log.read_text(encoding="utf-8")
514
+ assert "FORTRESS_INIT" in content
515
+
516
+ def test_tamper_writes_audit(
517
+ self, fortress: MemoryFortress, home: Path,
518
+ ) -> None:
519
+ """Tamper detection writes audit event."""
520
+ entry = MemoryEntry(
521
+ memory_id="audittamp",
522
+ content="Watch me",
523
+ tags=["audit"],
524
+ source="test",
525
+ layer=MemoryLayer.SHORT_TERM,
526
+ )
527
+ path = fortress.save_sealed(home, entry)
528
+
529
+ # Tamper
530
+ data = json.loads(path.read_text(encoding="utf-8"))
531
+ data["content"] = "EVIL"
532
+ path.write_text(json.dumps(data), encoding="utf-8")
533
+
534
+ fortress.verify_and_load(path)
535
+
536
+ audit_log = home / "security" / "audit.log"
537
+ content = audit_log.read_text(encoding="utf-8")
538
+ assert "MEMORY_TAMPER_ALERT" in content
539
+
540
+ def test_scan_writes_audit(
541
+ self, fortress: MemoryFortress, home: Path,
542
+ ) -> None:
543
+ """Full scan writes audit event."""
544
+ fortress.verify_all(home)
545
+
546
+ audit_log = home / "security" / "audit.log"
547
+ content = audit_log.read_text(encoding="utf-8")
548
+ assert "FORTRESS_SCAN" in content
549
+
550
+
551
+ # ---------------------------------------------------------------------------
552
+ # Model tests
553
+ # ---------------------------------------------------------------------------
554
+
555
+
556
+ class TestModels:
557
+ """Tests for fortress models."""
558
+
559
+ def test_fortress_config_defaults(self) -> None:
560
+ """FortressConfig has sensible defaults."""
561
+ config = FortressConfig()
562
+ assert config.enabled is True
563
+ assert config.encryption_enabled is False
564
+ assert config.seal_algorithm == "hmac-sha256"
565
+
566
+ def test_seal_result_defaults(self) -> None:
567
+ """SealResult has sensible defaults."""
568
+ result = SealResult(memory_id="test", sealed=True)
569
+ assert result.tampered is False
570
+ assert result.error is None
571
+ assert result.verified is None
@@ -0,0 +1,119 @@
1
+ """Unit tests for the memory pillar module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ import pytest
10
+
11
+ from skcapstone.models import MemoryLayer, PillarStatus
12
+ from skcapstone.pillars.memory import get_memory_stats, initialize_memory
13
+
14
+
15
+ class TestInitializeMemory:
16
+ """Tests for initialize_memory()."""
17
+
18
+ def test_creates_memory_directory(self, tmp_agent_home: Path):
19
+ initialize_memory(tmp_agent_home)
20
+ assert (tmp_agent_home / "memory").is_dir()
21
+
22
+ def test_creates_layer_subdirectories(self, tmp_agent_home: Path):
23
+ initialize_memory(tmp_agent_home)
24
+ memory_dir = tmp_agent_home / "memory"
25
+ for layer in MemoryLayer:
26
+ assert (memory_dir / layer.value).is_dir(), f"missing layer: {layer.value}"
27
+
28
+ def test_creates_short_term_dir(self, tmp_agent_home: Path):
29
+ initialize_memory(tmp_agent_home)
30
+ assert (tmp_agent_home / "memory" / "short-term").is_dir()
31
+
32
+ def test_creates_mid_term_dir(self, tmp_agent_home: Path):
33
+ initialize_memory(tmp_agent_home)
34
+ assert (tmp_agent_home / "memory" / "mid-term").is_dir()
35
+
36
+ def test_creates_long_term_dir(self, tmp_agent_home: Path):
37
+ initialize_memory(tmp_agent_home)
38
+ assert (tmp_agent_home / "memory" / "long-term").is_dir()
39
+
40
+ def test_returns_active_status(self, tmp_agent_home: Path):
41
+ state = initialize_memory(tmp_agent_home)
42
+ assert state.status == PillarStatus.ACTIVE
43
+
44
+ def test_store_path_set_to_memory_dir(self, tmp_agent_home: Path):
45
+ state = initialize_memory(tmp_agent_home)
46
+ assert state.store_path == tmp_agent_home / "memory"
47
+
48
+ def test_idempotent_second_call(self, tmp_agent_home: Path):
49
+ initialize_memory(tmp_agent_home)
50
+ state2 = initialize_memory(tmp_agent_home)
51
+ assert state2.status == PillarStatus.ACTIVE
52
+
53
+ def test_memory_home_param_ignored(self, tmp_agent_home: Path):
54
+ """memory_home is kept for backward compatibility but unused."""
55
+ state = initialize_memory(tmp_agent_home, memory_home=Path("/dev/null"))
56
+ assert state.store_path == tmp_agent_home / "memory"
57
+
58
+
59
+ class TestGetMemoryStats:
60
+ """Tests for get_memory_stats()."""
61
+
62
+ def test_returns_zero_counts_when_empty(self, tmp_agent_home: Path):
63
+ initialize_memory(tmp_agent_home)
64
+ stats = get_memory_stats(tmp_agent_home)
65
+ assert stats["short_term"] == 0
66
+ assert stats["mid_term"] == 0
67
+ assert stats["long_term"] == 0
68
+ assert stats["total"] == 0
69
+
70
+ def test_counts_json_files_in_short_term(self, tmp_agent_home: Path):
71
+ initialize_memory(tmp_agent_home)
72
+ short_dir = tmp_agent_home / "memory" / "short-term"
73
+ (short_dir / "mem001.json").write_text("{}", encoding="utf-8")
74
+ (short_dir / "mem002.json").write_text("{}", encoding="utf-8")
75
+
76
+ stats = get_memory_stats(tmp_agent_home)
77
+ assert stats["short_term"] == 2
78
+ assert stats["total"] == 2
79
+
80
+ def test_counts_json_files_in_mid_term(self, tmp_agent_home: Path):
81
+ initialize_memory(tmp_agent_home)
82
+ mid_dir = tmp_agent_home / "memory" / "mid-term"
83
+ (mid_dir / "mem001.json").write_text("{}", encoding="utf-8")
84
+
85
+ stats = get_memory_stats(tmp_agent_home)
86
+ assert stats["mid_term"] == 1
87
+
88
+ def test_counts_json_files_in_long_term(self, tmp_agent_home: Path):
89
+ initialize_memory(tmp_agent_home)
90
+ long_dir = tmp_agent_home / "memory" / "long-term"
91
+ (long_dir / "deep001.json").write_text("{}", encoding="utf-8")
92
+ (long_dir / "deep002.json").write_text("{}", encoding="utf-8")
93
+ (long_dir / "deep003.json").write_text("{}", encoding="utf-8")
94
+
95
+ stats = get_memory_stats(tmp_agent_home)
96
+ assert stats["long_term"] == 3
97
+
98
+ def test_total_is_sum_of_all_layers(self, tmp_agent_home: Path):
99
+ initialize_memory(tmp_agent_home)
100
+ (tmp_agent_home / "memory" / "short-term" / "a.json").write_text("{}")
101
+ (tmp_agent_home / "memory" / "mid-term" / "b.json").write_text("{}")
102
+ (tmp_agent_home / "memory" / "long-term" / "c.json").write_text("{}")
103
+
104
+ stats = get_memory_stats(tmp_agent_home)
105
+ assert stats["total"] == 3
106
+
107
+ def test_non_json_files_not_counted(self, tmp_agent_home: Path):
108
+ initialize_memory(tmp_agent_home)
109
+ short_dir = tmp_agent_home / "memory" / "short-term"
110
+ (short_dir / "note.md").write_text("# note")
111
+ (short_dir / "real.json").write_text("{}")
112
+
113
+ stats = get_memory_stats(tmp_agent_home)
114
+ assert stats["short_term"] == 1
115
+
116
+ def test_returns_dict_with_expected_keys(self, tmp_agent_home: Path):
117
+ initialize_memory(tmp_agent_home)
118
+ stats = get_memory_stats(tmp_agent_home)
119
+ assert set(stats.keys()) == {"short_term", "mid_term", "long_term", "total"}