@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,451 @@
1
+ """Tests for skcapstone.scheduled_tasks — cron-like scheduler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ from datetime import datetime, timedelta, timezone
8
+ from pathlib import Path
9
+ from unittest.mock import MagicMock, patch, call
10
+
11
+ import pytest
12
+
13
+ from skcapstone.scheduled_tasks import (
14
+ ScheduledTask,
15
+ TaskScheduler,
16
+ build_scheduler,
17
+ make_backend_reprobe_task,
18
+ make_heartbeat_task,
19
+ make_memory_promotion_task,
20
+ make_profile_freshness_task,
21
+ )
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # ScheduledTask unit tests
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ class TestScheduledTaskIsDue:
30
+ def test_never_run_is_always_due(self):
31
+ task = ScheduledTask(name="x", interval_seconds=60, callback=lambda: None)
32
+ assert task.is_due() is True
33
+
34
+ def test_recently_run_is_not_due(self):
35
+ task = ScheduledTask(name="x", interval_seconds=60, callback=lambda: None)
36
+ task.last_run = datetime.now(timezone.utc)
37
+ assert task.is_due() is False
38
+
39
+ def test_overdue_is_due(self):
40
+ task = ScheduledTask(name="x", interval_seconds=60, callback=lambda: None)
41
+ task.last_run = datetime.now(timezone.utc) - timedelta(seconds=61)
42
+ assert task.is_due() is True
43
+
44
+ def test_exactly_at_interval_is_due(self):
45
+ task = ScheduledTask(name="x", interval_seconds=60, callback=lambda: None)
46
+ task.last_run = datetime.now(timezone.utc) - timedelta(seconds=60)
47
+ assert task.is_due() is True
48
+
49
+ def test_custom_now_reference(self):
50
+ task = ScheduledTask(name="x", interval_seconds=60, callback=lambda: None)
51
+ task.last_run = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
52
+ future = datetime(2026, 1, 1, 12, 2, 0, tzinfo=timezone.utc) # 120s later
53
+ assert task.is_due(now=future) is True
54
+
55
+
56
+ class TestScheduledTaskRun:
57
+ def test_successful_run_increments_count(self):
58
+ calls = []
59
+ task = ScheduledTask(name="t", interval_seconds=1, callback=lambda: calls.append(1))
60
+ task.run()
61
+ assert task.run_count == 1
62
+ assert task.error_count == 0
63
+ assert task.last_error is None
64
+ assert len(calls) == 1
65
+
66
+ def test_successful_run_sets_last_run(self):
67
+ before = datetime.now(timezone.utc)
68
+ task = ScheduledTask(name="t", interval_seconds=1, callback=lambda: None)
69
+ task.run()
70
+ after = datetime.now(timezone.utc)
71
+ assert task.last_run is not None
72
+ assert before <= task.last_run <= after
73
+
74
+ def test_failed_run_records_error(self):
75
+ def _boom():
76
+ raise ValueError("something broke")
77
+
78
+ task = ScheduledTask(name="t", interval_seconds=1, callback=_boom)
79
+ task.run()
80
+ assert task.error_count == 1
81
+ assert task.run_count == 0
82
+ assert "something broke" in task.last_error
83
+
84
+ def test_failed_run_still_updates_last_run(self):
85
+ """last_run must be set even when the callback raises, so the interval resets."""
86
+ task = ScheduledTask(name="t", interval_seconds=1, callback=lambda: 1 / 0)
87
+ task.run()
88
+ assert task.last_run is not None
89
+
90
+ def test_successive_runs_accumulate_count(self):
91
+ counter = {"n": 0}
92
+
93
+ def _inc():
94
+ counter["n"] += 1
95
+
96
+ task = ScheduledTask(name="t", interval_seconds=0, callback=_inc)
97
+ for _ in range(5):
98
+ task.run()
99
+ assert task.run_count == 5
100
+ assert counter["n"] == 5
101
+
102
+ def test_cleared_error_after_recovery(self):
103
+ state = {"fail": True}
104
+
105
+ def _flaky():
106
+ if state["fail"]:
107
+ raise RuntimeError("transient")
108
+
109
+ task = ScheduledTask(name="t", interval_seconds=0, callback=_flaky)
110
+ task.run()
111
+ assert task.last_error is not None
112
+
113
+ state["fail"] = False
114
+ task.run()
115
+ assert task.last_error is None
116
+ assert task.run_count == 1
117
+ assert task.error_count == 1
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # TaskScheduler tests
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ class TestTaskSchedulerRegister:
126
+ def test_register_returns_task(self, tmp_path):
127
+ stop = threading.Event()
128
+ scheduler = TaskScheduler(tmp_path, stop)
129
+ task = scheduler.register("ping", 10, lambda: None)
130
+ assert isinstance(task, ScheduledTask)
131
+ assert task.name == "ping"
132
+ assert task.interval_seconds == 10
133
+
134
+ def test_register_multiple_tasks(self, tmp_path):
135
+ stop = threading.Event()
136
+ scheduler = TaskScheduler(tmp_path, stop)
137
+ scheduler.register("a", 5, lambda: None)
138
+ scheduler.register("b", 10, lambda: None)
139
+ scheduler.register("c", 15, lambda: None)
140
+ assert len(scheduler.status()) == 3
141
+
142
+ def test_status_reflects_unrun_tasks(self, tmp_path):
143
+ stop = threading.Event()
144
+ scheduler = TaskScheduler(tmp_path, stop)
145
+ scheduler.register("mine", 30, lambda: None)
146
+ status = scheduler.status()
147
+ assert status[0]["name"] == "mine"
148
+ assert status[0]["last_run"] is None
149
+ assert status[0]["run_count"] == 0
150
+ assert status[0]["error_count"] == 0
151
+
152
+
153
+ class TestTaskSchedulerExecution:
154
+ def test_scheduler_runs_due_task(self, tmp_path):
155
+ """Scheduler fires a task with 0-second interval within a short window."""
156
+ stop = threading.Event()
157
+ fired = threading.Event()
158
+
159
+ scheduler = TaskScheduler(tmp_path, stop, tick_interval=0.05)
160
+ scheduler.register("instant", 0, fired.set)
161
+ scheduler.start()
162
+
163
+ assert fired.wait(timeout=2.0), "Task should have fired within 2 seconds"
164
+ stop.set()
165
+
166
+ def test_scheduler_stops_cleanly(self, tmp_path):
167
+ stop = threading.Event()
168
+ scheduler = TaskScheduler(tmp_path, stop, tick_interval=0.05)
169
+ scheduler.register("noop", 999, lambda: None)
170
+ t = scheduler.start()
171
+ stop.set()
172
+ t.join(timeout=2.0)
173
+ assert not t.is_alive()
174
+
175
+ def test_scheduler_thread_is_daemon(self, tmp_path):
176
+ stop = threading.Event()
177
+ scheduler = TaskScheduler(tmp_path, stop, tick_interval=1)
178
+ t = scheduler.start()
179
+ assert t.daemon is True
180
+ stop.set()
181
+
182
+ def test_scheduler_skips_not_yet_due_task(self, tmp_path):
183
+ """A task last run just now should NOT fire again within the tick window."""
184
+ stop = threading.Event()
185
+ counter = {"n": 0}
186
+
187
+ def _count():
188
+ counter["n"] += 1
189
+
190
+ scheduler = TaskScheduler(tmp_path, stop, tick_interval=0.05)
191
+ task = scheduler.register("slow", 3600, _count)
192
+ # Pre-mark as just run so it is NOT due
193
+ task.last_run = datetime.now(timezone.utc)
194
+
195
+ scheduler.start()
196
+ time.sleep(0.2)
197
+ stop.set()
198
+
199
+ assert counter["n"] == 0, "Task should not have fired — it was just run"
200
+
201
+ def test_error_in_task_does_not_crash_scheduler(self, tmp_path):
202
+ """A raising callback must not kill the scheduler thread."""
203
+ stop = threading.Event()
204
+ second_fired = threading.Event()
205
+
206
+ def _bad():
207
+ raise RuntimeError("intentional")
208
+
209
+ scheduler = TaskScheduler(tmp_path, stop, tick_interval=0.05)
210
+ scheduler.register("bad", 0, _bad)
211
+ scheduler.register("good", 0, second_fired.set)
212
+ scheduler.start()
213
+
214
+ assert second_fired.wait(timeout=2.0), "Scheduler should survive a bad task"
215
+ stop.set()
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # build_scheduler — registration completeness and intervals
220
+ # ---------------------------------------------------------------------------
221
+
222
+
223
+ class TestBuildScheduler:
224
+ def test_registers_four_standard_tasks(self, tmp_path):
225
+ stop = threading.Event()
226
+ scheduler = build_scheduler(tmp_path, stop)
227
+ names = {s["name"] for s in scheduler.status()}
228
+ assert names == {
229
+ "heartbeat_pulse",
230
+ "backend_reprobe",
231
+ "memory_promotion_sweep",
232
+ "profile_freshness_check",
233
+ }
234
+
235
+ def test_heartbeat_interval_is_30s(self, tmp_path):
236
+ stop = threading.Event()
237
+ scheduler = build_scheduler(tmp_path, stop)
238
+ task = next(s for s in scheduler.status() if s["name"] == "heartbeat_pulse")
239
+ assert task["interval_seconds"] == 30
240
+
241
+ def test_backend_reprobe_interval_is_5min(self, tmp_path):
242
+ stop = threading.Event()
243
+ scheduler = build_scheduler(tmp_path, stop)
244
+ task = next(s for s in scheduler.status() if s["name"] == "backend_reprobe")
245
+ assert task["interval_seconds"] == 300
246
+
247
+ def test_memory_promotion_interval_is_hourly(self, tmp_path):
248
+ stop = threading.Event()
249
+ scheduler = build_scheduler(tmp_path, stop)
250
+ task = next(s for s in scheduler.status() if s["name"] == "memory_promotion_sweep")
251
+ assert task["interval_seconds"] == 3600
252
+
253
+ def test_profile_freshness_interval_is_daily(self, tmp_path):
254
+ stop = threading.Event()
255
+ scheduler = build_scheduler(tmp_path, stop)
256
+ task = next(s for s in scheduler.status() if s["name"] == "profile_freshness_check")
257
+ assert task["interval_seconds"] == 86400
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Individual task callback tests
262
+ # ---------------------------------------------------------------------------
263
+
264
+
265
+ class TestMemoryPromotionTask:
266
+ def test_calls_sweep_and_logs_promotions(self, tmp_path):
267
+ mock_result = MagicMock()
268
+ mock_result.scanned = 10
269
+ mock_result.promoted = [MagicMock(), MagicMock()] # 2 promoted
270
+
271
+ mock_engine = MagicMock()
272
+ mock_engine.sweep.return_value = mock_result
273
+
274
+ callback = make_memory_promotion_task(tmp_path)
275
+
276
+ # PromotionEngine is imported lazily inside the closure via
277
+ # `from .memory_promoter import PromotionEngine` — patch the source.
278
+ with patch("skcapstone.memory_promoter.PromotionEngine", return_value=mock_engine) as MockEngine:
279
+ callback()
280
+ MockEngine.assert_called_once_with(tmp_path)
281
+ mock_engine.sweep.assert_called_once()
282
+
283
+ def test_no_promotions_does_not_raise(self, tmp_path):
284
+ mock_result = MagicMock()
285
+ mock_result.scanned = 5
286
+ mock_result.promoted = []
287
+
288
+ mock_engine = MagicMock()
289
+ mock_engine.sweep.return_value = mock_result
290
+
291
+ callback = make_memory_promotion_task(tmp_path)
292
+ with patch("skcapstone.memory_promoter.PromotionEngine", return_value=mock_engine):
293
+ callback() # should not raise
294
+
295
+ def test_import_error_propagates_as_exception(self, tmp_path):
296
+ """If PromotionEngine raises on import the task should propagate (caught by runner)."""
297
+ callback = make_memory_promotion_task(tmp_path)
298
+ with patch("skcapstone.memory_promoter.PromotionEngine", side_effect=RuntimeError("unavailable")):
299
+ with pytest.raises(RuntimeError, match="unavailable"):
300
+ callback()
301
+
302
+
303
+ class TestBackendReprobeTask:
304
+ def test_calls_probe_on_bridge(self):
305
+ mock_bridge = MagicMock()
306
+ mock_bridge._available = {"ollama": True, "passthrough": True}
307
+
308
+ mock_loop = MagicMock()
309
+ mock_loop._bridge = mock_bridge
310
+
311
+ callback = make_backend_reprobe_task(mock_loop)
312
+ callback()
313
+ mock_bridge._probe_available_backends.assert_called_once()
314
+
315
+ def test_noop_when_loop_is_none(self):
316
+ callback = make_backend_reprobe_task(None)
317
+ callback() # should not raise
318
+
319
+ def test_noop_when_bridge_missing(self):
320
+ mock_loop = MagicMock(spec=[]) # no _bridge attribute
321
+ callback = make_backend_reprobe_task(mock_loop)
322
+ callback() # should not raise
323
+
324
+ def test_noop_when_probe_fn_missing(self):
325
+ mock_bridge = MagicMock(spec=[]) # no _probe_available_backends
326
+ mock_loop = MagicMock()
327
+ mock_loop._bridge = mock_bridge
328
+ callback = make_backend_reprobe_task(mock_loop)
329
+ callback() # should not raise
330
+
331
+
332
+ class TestHeartbeatTask:
333
+ def test_calls_pulse_with_active_state(self):
334
+ mock_beacon = MagicMock()
335
+ callback = make_heartbeat_task(mock_beacon, lambda: True)
336
+ callback()
337
+ mock_beacon.pulse.assert_called_once_with(consciousness_active=True)
338
+
339
+ def test_calls_pulse_with_inactive_state(self):
340
+ mock_beacon = MagicMock()
341
+ callback = make_heartbeat_task(mock_beacon, lambda: False)
342
+ callback()
343
+ mock_beacon.pulse.assert_called_once_with(consciousness_active=False)
344
+
345
+ def test_noop_when_beacon_is_none(self):
346
+ callback = make_heartbeat_task(None, lambda: True)
347
+ callback() # should not raise
348
+
349
+ def test_uses_fn_result_dynamically(self):
350
+ """consciousness_active_fn is called each time, not captured at build time."""
351
+ mock_beacon = MagicMock()
352
+ state = {"active": False}
353
+ callback = make_heartbeat_task(mock_beacon, lambda: state["active"])
354
+
355
+ callback()
356
+ mock_beacon.pulse.assert_called_with(consciousness_active=False)
357
+
358
+ state["active"] = True
359
+ callback()
360
+ mock_beacon.pulse.assert_called_with(consciousness_active=True)
361
+
362
+
363
+ class TestProfileFreshnessTask:
364
+ def test_fresh_files_produce_no_warning(self, tmp_path, caplog):
365
+ import logging
366
+
367
+ identity_dir = tmp_path / "identity"
368
+ identity_dir.mkdir()
369
+ (identity_dir / "identity.json").write_text("{}")
370
+
371
+ callback = make_profile_freshness_task(tmp_path, max_age_days=7)
372
+ with caplog.at_level(logging.WARNING, logger="skcapstone.scheduled_tasks"):
373
+ callback()
374
+ assert not any("Profile freshness" in r.message for r in caplog.records)
375
+
376
+ def test_stale_identity_triggers_warning(self, tmp_path, caplog):
377
+ import logging
378
+ import os
379
+
380
+ identity_dir = tmp_path / "identity"
381
+ identity_dir.mkdir()
382
+ identity_file = identity_dir / "identity.json"
383
+ identity_file.write_text("{}")
384
+
385
+ # Set mtime to 10 days ago
386
+ old_mtime = time.time() - (10 * 86400)
387
+ os.utime(identity_file, (old_mtime, old_mtime))
388
+
389
+ callback = make_profile_freshness_task(tmp_path, max_age_days=7)
390
+ with caplog.at_level(logging.WARNING, logger="skcapstone.scheduled_tasks"):
391
+ callback()
392
+
393
+ warnings = [r.message for r in caplog.records if r.levelno == logging.WARNING]
394
+ assert any("identity.json" in w for w in warnings)
395
+
396
+ def test_stale_model_profile_triggers_warning(self, tmp_path, caplog):
397
+ import logging
398
+ import os
399
+
400
+ profiles_dir = tmp_path / "data" / "model_profiles"
401
+ profiles_dir.mkdir(parents=True)
402
+ profile_file = profiles_dir / "llama3.json"
403
+ profile_file.write_text("{}")
404
+
405
+ old_mtime = time.time() - (15 * 86400)
406
+ os.utime(profile_file, (old_mtime, old_mtime))
407
+
408
+ callback = make_profile_freshness_task(tmp_path, max_age_days=7)
409
+ with caplog.at_level(logging.WARNING, logger="skcapstone.scheduled_tasks"):
410
+ callback()
411
+
412
+ warnings = [r.message for r in caplog.records if r.levelno == logging.WARNING]
413
+ assert any("llama3" in w for w in warnings)
414
+
415
+ def test_missing_identity_dir_does_not_raise(self, tmp_path):
416
+ callback = make_profile_freshness_task(tmp_path)
417
+ callback() # identity dir absent — should not raise
418
+
419
+ def test_missing_profiles_dir_does_not_raise(self, tmp_path):
420
+ callback = make_profile_freshness_task(tmp_path)
421
+ callback() # data/model_profiles absent — should not raise
422
+
423
+ def test_custom_max_age_respected(self, tmp_path, caplog):
424
+ import logging
425
+ import os
426
+
427
+ identity_dir = tmp_path / "identity"
428
+ identity_dir.mkdir()
429
+ identity_file = identity_dir / "identity.json"
430
+ identity_file.write_text("{}")
431
+
432
+ # 3 days old
433
+ old_mtime = time.time() - (3 * 86400)
434
+ os.utime(identity_file, (old_mtime, old_mtime))
435
+
436
+ # max_age_days=2 → should warn
437
+ callback = make_profile_freshness_task(tmp_path, max_age_days=2)
438
+ with caplog.at_level(logging.WARNING, logger="skcapstone.scheduled_tasks"):
439
+ callback()
440
+
441
+ warnings = [r.message for r in caplog.records if r.levelno == logging.WARNING]
442
+ assert any("identity.json" in w for w in warnings)
443
+
444
+ # max_age_days=5 → should NOT warn
445
+ caplog.clear()
446
+ callback2 = make_profile_freshness_task(tmp_path, max_age_days=5)
447
+ with caplog.at_level(logging.WARNING, logger="skcapstone.scheduled_tasks"):
448
+ callback2()
449
+
450
+ warnings2 = [r.message for r in caplog.records if r.levelno == logging.WARNING]
451
+ assert not any("identity.json" in w for w in warnings2)
@@ -0,0 +1,250 @@
1
+ """Security tests for skcapstone.
2
+
3
+ Covers:
4
+ - Peer name sanitization / path traversal prevention
5
+ - Large message (oversized inbox file) rejection
6
+ - Invalid JSON in inbox files
7
+
8
+ These map to the findings from the sprint-14 security audit.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import tempfile
16
+ from pathlib import Path
17
+ from unittest.mock import MagicMock, patch
18
+
19
+ import pytest
20
+
21
+ from skcapstone.consciousness_loop import (
22
+ ConsciousnessConfig,
23
+ ConsciousnessLoop,
24
+ SystemPromptBuilder,
25
+ _sanitize_peer_name,
26
+ )
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # _sanitize_peer_name unit tests
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ class TestSanitizePeerName:
35
+ """Unit tests for the _sanitize_peer_name helper."""
36
+
37
+ def test_normal_name_passes_through(self):
38
+ assert _sanitize_peer_name("alice") == "alice"
39
+
40
+ def test_alphanumeric_with_dash(self):
41
+ assert _sanitize_peer_name("agent-007") == "agent-007"
42
+
43
+ def test_at_sign_allowed(self):
44
+ assert _sanitize_peer_name("opus@skworld.io") == "opus@skworld.io"
45
+
46
+ def test_dotted_name_allowed(self):
47
+ assert _sanitize_peer_name("v1.2.3") == "v1.2.3"
48
+
49
+ # --- path traversal ---
50
+
51
+ def test_slash_stripped(self):
52
+ result = _sanitize_peer_name("../../../etc/passwd")
53
+ assert "/" not in result
54
+ assert ".." not in result or result.count(".") <= 1
55
+
56
+ def test_backslash_stripped(self):
57
+ result = _sanitize_peer_name("..\\Windows\\system32")
58
+ assert "\\" not in result
59
+
60
+ def test_pure_dotdot_rejected(self):
61
+ """'..'' alone is stripped and falls back to 'unknown'."""
62
+ result = _sanitize_peer_name("..")
63
+ # After stripping leading/trailing dots, should be empty → "unknown"
64
+ assert result == "unknown"
65
+
66
+ def test_dotdot_with_slash(self):
67
+ result = _sanitize_peer_name("../../secret")
68
+ assert "/" not in result
69
+ assert result != "../../secret"
70
+
71
+ def test_null_byte_stripped(self):
72
+ result = _sanitize_peer_name("alice\x00evil")
73
+ assert "\x00" not in result
74
+
75
+ def test_empty_string_returns_unknown(self):
76
+ assert _sanitize_peer_name("") == "unknown"
77
+
78
+ def test_none_returns_unknown(self):
79
+ assert _sanitize_peer_name(None) == "unknown" # type: ignore[arg-type]
80
+
81
+ def test_spaces_stripped(self):
82
+ result = _sanitize_peer_name("alice bob")
83
+ assert " " not in result
84
+
85
+ def test_length_capped_at_64(self):
86
+ long_name = "a" * 200
87
+ result = _sanitize_peer_name(long_name)
88
+ assert len(result) <= 64
89
+
90
+ def test_special_chars_stripped(self):
91
+ result = _sanitize_peer_name("peer<script>alert(1)</script>")
92
+ assert "<" not in result
93
+ assert ">" not in result
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Path traversal via SystemPromptBuilder._persist_peer_history
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ class TestPeerHistoryPathTraversal:
102
+ """Verify that malicious peer names cannot write outside the conversations dir."""
103
+
104
+ def _make_builder(self, tmp_path: Path) -> SystemPromptBuilder:
105
+ """Return a SystemPromptBuilder backed by tmp_path."""
106
+ return SystemPromptBuilder(home=tmp_path, max_tokens=4096)
107
+
108
+ def test_traversal_peer_stays_inside_conversations(self, tmp_path):
109
+ """A sender like '../../../etc/passwd' must not escape conversations/."""
110
+ builder = self._make_builder(tmp_path)
111
+ conversations_dir = tmp_path / "conversations"
112
+
113
+ # Simulate receiving a message from a malicious peer
114
+ malicious_peer = "../../../etc/passwd"
115
+ builder.add_to_history(malicious_peer, "user", "hello")
116
+
117
+ # Only files inside conversations/ should exist
118
+ for written_file in conversations_dir.rglob("*"):
119
+ try:
120
+ written_file.relative_to(conversations_dir)
121
+ except ValueError:
122
+ pytest.fail(
123
+ f"Path traversal detected: {written_file} is outside {conversations_dir}"
124
+ )
125
+
126
+ def test_dotdot_peer_sanitized_to_unknown(self, tmp_path):
127
+ builder = self._make_builder(tmp_path)
128
+ builder.add_to_history("..", "user", "hi")
129
+ conversations_dir = tmp_path / "conversations"
130
+ assert (conversations_dir / "unknown.json").exists()
131
+
132
+ def test_slash_in_peer_name_sanitized(self, tmp_path):
133
+ builder = self._make_builder(tmp_path)
134
+ builder.add_to_history("a/b/c", "user", "hi")
135
+ conversations_dir = tmp_path / "conversations"
136
+ # Should write abc.json (slashes stripped) or similar safe name
137
+ for f in conversations_dir.iterdir():
138
+ assert "/" not in f.name
139
+
140
+ def test_null_byte_in_peer_name_sanitized(self, tmp_path):
141
+ builder = self._make_builder(tmp_path)
142
+ builder.add_to_history("peer\x00evil", "user", "hi")
143
+ conversations_dir = tmp_path / "conversations"
144
+ for f in conversations_dir.iterdir():
145
+ assert "\x00" not in f.name
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Large message rejection (file-size cap in ConsciousnessLoop._on_inbox_file)
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ class TestLargeMessageRejected:
154
+ """The inbox handler must reject files larger than 1 MB."""
155
+
156
+ def _make_loop(self, tmp_path: Path) -> ConsciousnessLoop:
157
+ config = ConsciousnessConfig(use_inotify=False)
158
+ loop = ConsciousnessLoop(
159
+ config=config,
160
+ home=tmp_path / "agent",
161
+ shared_root=tmp_path / "shared",
162
+ )
163
+ return loop
164
+
165
+ def test_oversized_file_is_dropped(self, tmp_path):
166
+ """A 1.1 MB inbox file must not be processed."""
167
+ loop = self._make_loop(tmp_path)
168
+
169
+ inbox_file = tmp_path / "big.skc.json"
170
+ # Write 1.1 MB of data — exceeds the 1_000_000 byte cap
171
+ inbox_file.write_bytes(b"x" * 1_100_000)
172
+
173
+ submitted = []
174
+ loop._executor.submit = lambda fn, *a, **kw: submitted.append((fn, a)) # type: ignore[method-assign]
175
+
176
+ loop._on_inbox_file(inbox_file)
177
+
178
+ assert submitted == [], "Oversized file should have been dropped without submitting"
179
+
180
+ def test_1mb_minus_one_byte_is_processed(self, tmp_path):
181
+ """A file just below the cap should be attempted (may fail on parse — that's fine)."""
182
+ loop = self._make_loop(tmp_path)
183
+
184
+ inbox_file = tmp_path / "ok.skc.json"
185
+ # Valid JSON just under the limit
186
+ payload = json.dumps({"sender": "alice", "payload": {"content": "hi"}})
187
+ inbox_file.write_text(payload, encoding="utf-8")
188
+
189
+ submitted = []
190
+ original_submit = loop._executor.submit
191
+
192
+ def capture_submit(fn, *a, **kw):
193
+ submitted.append((fn, a))
194
+ return MagicMock()
195
+
196
+ loop._executor.submit = capture_submit # type: ignore[method-assign]
197
+ loop._on_inbox_file(inbox_file)
198
+
199
+ # Under the cap → should be submitted (even if the envelope later fails)
200
+ assert len(submitted) == 1, "File under size cap should be submitted for processing"
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # Invalid JSON rejection
205
+ # ---------------------------------------------------------------------------
206
+
207
+
208
+ class TestInvalidJsonRejected:
209
+ """Malformed JSON in the inbox must not crash the consciousness loop."""
210
+
211
+ def _make_loop(self, tmp_path: Path) -> ConsciousnessLoop:
212
+ config = ConsciousnessConfig(use_inotify=False)
213
+ return ConsciousnessLoop(
214
+ config=config,
215
+ home=tmp_path / "agent",
216
+ shared_root=tmp_path / "shared",
217
+ )
218
+
219
+ def test_invalid_json_does_not_raise(self, tmp_path):
220
+ """Malformed JSON must be silently dropped, not crash."""
221
+ loop = self._make_loop(tmp_path)
222
+ inbox_file = tmp_path / "bad.skc.json"
223
+ inbox_file.write_text("{invalid json{{", encoding="utf-8")
224
+
225
+ # Must not raise
226
+ loop._on_inbox_file(inbox_file)
227
+
228
+ def test_non_dict_json_does_not_raise(self, tmp_path):
229
+ """A valid JSON array (not a dict) must also be silently dropped."""
230
+ loop = self._make_loop(tmp_path)
231
+ inbox_file = tmp_path / "array.skc.json"
232
+ inbox_file.write_text("[1, 2, 3]", encoding="utf-8")
233
+
234
+ loop._on_inbox_file(inbox_file)
235
+
236
+ def test_truncated_json_does_not_raise(self, tmp_path):
237
+ """Truncated / partially-written JSON must be silently dropped."""
238
+ loop = self._make_loop(tmp_path)
239
+ inbox_file = tmp_path / "truncated.skc.json"
240
+ inbox_file.write_text('{"sender": "alice", "payload":', encoding="utf-8")
241
+
242
+ loop._on_inbox_file(inbox_file)
243
+
244
+ def test_empty_file_does_not_raise(self, tmp_path):
245
+ """An empty inbox file must not crash."""
246
+ loop = self._make_loop(tmp_path)
247
+ inbox_file = tmp_path / "empty.skc.json"
248
+ inbox_file.write_bytes(b"")
249
+
250
+ loop._on_inbox_file(inbox_file)