@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,1156 @@
1
+ """
2
+ FUSE Mount — Sovereign Virtual Filesystem.
3
+
4
+ Exposes the sovereign agent's data (memories, identity, inbox, outbox,
5
+ coordination tasks) as a mountable POSIX filesystem via FUSE.
6
+
7
+ Virtual directory layout::
8
+
9
+ /
10
+ ├── memories/
11
+ │ ├── short/ — short-term memory files (.md)
12
+ │ ├── mid/ — mid-term memory files (.md)
13
+ │ └── long/ — long-term memory files (.md)
14
+ ├── documents/ — SKSeal signed documents
15
+ ├── identity/
16
+ │ ├── card.json — CapAuth identity card
17
+ │ └── fingerprint.txt — PGP fingerprint
18
+ ├── inbox/ — SKComm incoming messages (read-only)
19
+ ├── outbox/ — Write here to send via SKComm
20
+ └── coordination/ — Task board files (.json)
21
+
22
+ Writing to ``/outbox/<agent_name>.msg`` enqueues a message via SKComm.
23
+
24
+ Dependencies (optional):
25
+ pip install skcapstone[fuse] # pulls in fusepy
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import errno
31
+ import json
32
+ import logging
33
+ import os
34
+ import stat
35
+ import subprocess
36
+ import sys
37
+ import time
38
+ from datetime import datetime, timezone
39
+ from pathlib import Path
40
+ from typing import Any, Dict, List, Optional, Tuple
41
+
42
+ logger = logging.getLogger("skcapstone.fuse")
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Layer name mapping: virtual dir slug → MemoryLayer value
46
+ # ---------------------------------------------------------------------------
47
+
48
+ _LAYER_SLUG_TO_VALUE: Dict[str, str] = {
49
+ "short": "short-term",
50
+ "mid": "mid-term",
51
+ "long": "long-term",
52
+ }
53
+
54
+ _LAYER_VALUE_TO_SLUG: Dict[str, str] = {v: k for k, v in _LAYER_SLUG_TO_VALUE.items()}
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Virtual filesystem path constants
58
+ # ---------------------------------------------------------------------------
59
+
60
+ _MEMORIES_DIR = "memories"
61
+ _DOCUMENTS_DIR = "documents"
62
+ _IDENTITY_DIR = "identity"
63
+ _INBOX_DIR = "inbox"
64
+ _OUTBOX_DIR = "outbox"
65
+ _COORDINATION_DIR = "coordination"
66
+
67
+ _TOP_LEVEL_DIRS = [
68
+ _MEMORIES_DIR,
69
+ _DOCUMENTS_DIR,
70
+ _IDENTITY_DIR,
71
+ _INBOX_DIR,
72
+ _OUTBOX_DIR,
73
+ _COORDINATION_DIR,
74
+ ]
75
+
76
+ _MEMORY_SUBDIRS = list(_LAYER_SLUG_TO_VALUE.keys()) # short, mid, long
77
+
78
+ _IDENTITY_FILES = ["card.json", "fingerprint.txt"]
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Helpers
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ def _now_ts() -> float:
87
+ """Return the current Unix timestamp as a float.
88
+
89
+ Returns:
90
+ Current UTC time as a float POSIX timestamp.
91
+ """
92
+ return datetime.now(timezone.utc).timestamp()
93
+
94
+
95
+ def _dir_stat(nlink: int = 2) -> Dict[str, Any]:
96
+ """Build a stat dict for a virtual directory.
97
+
98
+ Args:
99
+ nlink: Number of hard links (default: 2).
100
+
101
+ Returns:
102
+ Stat dictionary suitable for FUSE Operations.getattr().
103
+ """
104
+ ts = _now_ts()
105
+ return {
106
+ "st_mode": stat.S_IFDIR | 0o555,
107
+ "st_nlink": nlink,
108
+ "st_uid": os.getuid(),
109
+ "st_gid": os.getgid(),
110
+ "st_size": 0,
111
+ "st_atime": ts,
112
+ "st_mtime": ts,
113
+ "st_ctime": ts,
114
+ }
115
+
116
+
117
+ def _file_stat(size: int, writable: bool = False) -> Dict[str, Any]:
118
+ """Build a stat dict for a virtual file.
119
+
120
+ Args:
121
+ size: File size in bytes.
122
+ writable: Whether the file is writable (e.g., outbox files).
123
+
124
+ Returns:
125
+ Stat dictionary suitable for FUSE Operations.getattr().
126
+ """
127
+ ts = _now_ts()
128
+ mode = stat.S_IFREG | (0o644 if writable else 0o444)
129
+ return {
130
+ "st_mode": mode,
131
+ "st_nlink": 1,
132
+ "st_uid": os.getuid(),
133
+ "st_gid": os.getgid(),
134
+ "st_size": size,
135
+ "st_atime": ts,
136
+ "st_mtime": ts,
137
+ "st_ctime": ts,
138
+ }
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Content generators
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ def _memory_to_markdown(memory: Dict[str, Any]) -> bytes:
147
+ """Render a memory dict as a Markdown document.
148
+
149
+ Args:
150
+ memory: Parsed JSON dict of a MemoryEntry.
151
+
152
+ Returns:
153
+ UTF-8 encoded Markdown bytes.
154
+ """
155
+ lines: List[str] = []
156
+ lines.append(f"# Memory: {memory.get('memory_id', 'unknown')}")
157
+ lines.append("")
158
+
159
+ created = memory.get("created_at", "")
160
+ if created:
161
+ lines.append(f"**Created:** {created}")
162
+
163
+ layer = memory.get("layer", "")
164
+ if layer:
165
+ lines.append(f"**Layer:** {layer}")
166
+
167
+ importance = memory.get("importance")
168
+ if importance is not None:
169
+ lines.append(f"**Importance:** {importance:.2f}")
170
+
171
+ tags = memory.get("tags", [])
172
+ if tags:
173
+ lines.append(f"**Tags:** {', '.join(tags)}")
174
+
175
+ soul = memory.get("soul_context")
176
+ if soul:
177
+ lines.append(f"**Soul:** {soul}")
178
+
179
+ source = memory.get("source", "")
180
+ if source:
181
+ lines.append(f"**Source:** {source}")
182
+
183
+ lines.append("")
184
+ lines.append("## Content")
185
+ lines.append("")
186
+ lines.append(memory.get("content", ""))
187
+
188
+ metadata = memory.get("metadata") or {}
189
+ if metadata:
190
+ lines.append("")
191
+ lines.append("## Metadata")
192
+ lines.append("")
193
+ for k, v in metadata.items():
194
+ lines.append(f"- **{k}:** {v}")
195
+
196
+ return "\n".join(lines).encode("utf-8")
197
+
198
+
199
+ def _load_memory_file(memory_dir: Path, layer_value: str, memory_id: str) -> Optional[bytes]:
200
+ """Load a memory JSON file and render it as Markdown.
201
+
202
+ Args:
203
+ memory_dir: Root memory directory (``~/.skcapstone/memory``).
204
+ layer_value: MemoryLayer value string (e.g. ``short-term``).
205
+ memory_id: Memory ID without extension.
206
+
207
+ Returns:
208
+ UTF-8 encoded Markdown bytes, or None if not found/invalid.
209
+ """
210
+ path = memory_dir / layer_value / f"{memory_id}.json"
211
+ if not path.exists():
212
+ return None
213
+ try:
214
+ data = json.loads(path.read_text(encoding="utf-8"))
215
+ return _memory_to_markdown(data)
216
+ except (json.JSONDecodeError, OSError) as exc:
217
+ logger.warning("Failed to load memory %s: %s", path, exc)
218
+ return None
219
+
220
+
221
+ def _list_memory_ids(memory_dir: Path, layer_value: str) -> List[str]:
222
+ """List all memory IDs for a given layer.
223
+
224
+ Args:
225
+ memory_dir: Root memory directory.
226
+ layer_value: MemoryLayer value string.
227
+
228
+ Returns:
229
+ List of memory IDs (without .json extension), sorted.
230
+ """
231
+ layer_dir = memory_dir / layer_value
232
+ if not layer_dir.exists():
233
+ return []
234
+ return sorted(p.stem for p in layer_dir.glob("*.json"))
235
+
236
+
237
+ def _build_identity_card(agent_home: Path) -> bytes:
238
+ """Build a JSON identity card from the CapAuth profile.
239
+
240
+ Falls back to manifest data if CapAuth is unavailable.
241
+
242
+ Args:
243
+ agent_home: Agent home directory.
244
+
245
+ Returns:
246
+ UTF-8 encoded JSON bytes.
247
+ """
248
+ # Try CapAuth profile
249
+ capauth_profile = Path("~/.capauth/profile.json").expanduser()
250
+ if capauth_profile.exists():
251
+ try:
252
+ data = json.loads(capauth_profile.read_text(encoding="utf-8"))
253
+ card: Dict[str, Any] = {
254
+ "name": data.get("name", "unknown"),
255
+ "email": data.get("email", ""),
256
+ "fingerprint": data.get("fingerprint", ""),
257
+ "created_at": data.get("created_at", ""),
258
+ "source": "capauth",
259
+ }
260
+ return json.dumps(card, indent=2).encode("utf-8")
261
+ except (json.JSONDecodeError, OSError) as exc:
262
+ logger.warning("Failed to read CapAuth profile for identity card: %s", exc)
263
+
264
+ # Fall back to manifest
265
+ manifest_path = agent_home / "manifest.json"
266
+ if manifest_path.exists():
267
+ try:
268
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
269
+ card = {
270
+ "name": data.get("name", "unknown"),
271
+ "fingerprint": data.get("identity", {}).get("fingerprint", ""),
272
+ "created_at": data.get("created_at", ""),
273
+ "source": "manifest",
274
+ }
275
+ return json.dumps(card, indent=2).encode("utf-8")
276
+ except (json.JSONDecodeError, OSError) as exc:
277
+ logger.warning("Failed to read manifest for identity card: %s", exc)
278
+
279
+ return json.dumps({"name": "unknown", "fingerprint": "", "source": "fallback"}).encode("utf-8")
280
+
281
+
282
+ def _build_fingerprint_txt(agent_home: Path) -> bytes:
283
+ """Extract the PGP fingerprint as plain text.
284
+
285
+ Args:
286
+ agent_home: Agent home directory.
287
+
288
+ Returns:
289
+ UTF-8 encoded fingerprint bytes (newline-terminated).
290
+ """
291
+ # Try CapAuth profile
292
+ capauth_profile = Path("~/.capauth/profile.json").expanduser()
293
+ if capauth_profile.exists():
294
+ try:
295
+ data = json.loads(capauth_profile.read_text(encoding="utf-8"))
296
+ fp = data.get("fingerprint", "")
297
+ if fp:
298
+ return (fp + "\n").encode("utf-8")
299
+ except (json.JSONDecodeError, OSError) as exc:
300
+ logger.warning("Failed to read CapAuth profile for fingerprint: %s", exc)
301
+
302
+ # Try manifest
303
+ manifest_path = agent_home / "manifest.json"
304
+ if manifest_path.exists():
305
+ try:
306
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
307
+ fp = data.get("identity", {}).get("fingerprint", "")
308
+ if fp:
309
+ return (fp + "\n").encode("utf-8")
310
+ except (json.JSONDecodeError, OSError) as exc:
311
+ logger.warning("Failed to read manifest for fingerprint: %s", exc)
312
+
313
+ return b"(no fingerprint)\n"
314
+
315
+
316
+ def _list_inbox(agent_home: Path) -> List[str]:
317
+ """List files in the SKComm inbox.
318
+
319
+ Args:
320
+ agent_home: Agent home directory.
321
+
322
+ Returns:
323
+ Sorted list of inbox filenames.
324
+ """
325
+ inbox_dir = agent_home / "comms" / "inbox"
326
+ if not inbox_dir.exists():
327
+ return []
328
+ return sorted(p.name for p in inbox_dir.iterdir() if p.is_file())
329
+
330
+
331
+ def _read_inbox_file(agent_home: Path, filename: str) -> Optional[bytes]:
332
+ """Read a message from the SKComm inbox.
333
+
334
+ Args:
335
+ agent_home: Agent home directory.
336
+ filename: Name of the inbox file.
337
+
338
+ Returns:
339
+ File contents as bytes, or None if not found.
340
+ """
341
+ path = agent_home / "comms" / "inbox" / filename
342
+ if not path.exists() or not path.is_file():
343
+ return None
344
+ try:
345
+ return path.read_bytes()
346
+ except OSError:
347
+ return None
348
+
349
+
350
+ def _list_documents(agent_home: Path) -> List[str]:
351
+ """List signed documents in the sovereign documents directory.
352
+
353
+ Args:
354
+ agent_home: Agent home directory.
355
+
356
+ Returns:
357
+ Sorted list of document filenames.
358
+ """
359
+ docs_dir = agent_home / "documents"
360
+ if not docs_dir.exists():
361
+ return []
362
+ return sorted(p.name for p in docs_dir.iterdir() if p.is_file())
363
+
364
+
365
+ def _read_document(agent_home: Path, filename: str) -> Optional[bytes]:
366
+ """Read a signed document.
367
+
368
+ Args:
369
+ agent_home: Agent home directory.
370
+ filename: Name of the document file.
371
+
372
+ Returns:
373
+ File contents as bytes, or None if not found.
374
+ """
375
+ path = agent_home / "documents" / filename
376
+ if not path.exists() or not path.is_file():
377
+ return None
378
+ try:
379
+ return path.read_bytes()
380
+ except OSError:
381
+ return None
382
+
383
+
384
+ def _list_coordination_tasks(agent_home: Path) -> List[str]:
385
+ """List coordination task files.
386
+
387
+ Args:
388
+ agent_home: Agent home directory.
389
+
390
+ Returns:
391
+ Sorted list of task JSON filenames.
392
+ """
393
+ tasks_dir = agent_home / "coordination" / "tasks"
394
+ if not tasks_dir.exists():
395
+ return []
396
+ return sorted(p.name for p in tasks_dir.glob("*.json"))
397
+
398
+
399
+ def _read_coordination_task(agent_home: Path, filename: str) -> Optional[bytes]:
400
+ """Read a coordination task JSON file.
401
+
402
+ Args:
403
+ agent_home: Agent home directory.
404
+ filename: Name of the task JSON file.
405
+
406
+ Returns:
407
+ File contents as bytes, or None if not found.
408
+ """
409
+ path = agent_home / "coordination" / "tasks" / filename
410
+ if not path.exists():
411
+ return None
412
+ try:
413
+ return path.read_bytes()
414
+ except OSError:
415
+ return None
416
+
417
+
418
+ # ---------------------------------------------------------------------------
419
+ # SKComm send helper
420
+ # ---------------------------------------------------------------------------
421
+
422
+
423
+ def _send_via_skcomm(agent_home: Path, recipient: str, message: str) -> bool:
424
+ """Send a message via SKComm by writing to the outbox directory.
425
+
426
+ Attempts to use the skcapstone CLI for delivery. Falls back to writing
427
+ an envelope JSON file in the outbox directory.
428
+
429
+ Args:
430
+ agent_home: Agent home directory.
431
+ recipient: Recipient agent name.
432
+ message: Message content to deliver.
433
+
434
+ Returns:
435
+ True if the message was queued successfully.
436
+ """
437
+ # Try skcapstone comm send CLI
438
+ try:
439
+ result = subprocess.run(
440
+ ["skcapstone", "comm", "send", recipient, "--message", message],
441
+ capture_output=True,
442
+ text=True,
443
+ timeout=10,
444
+ )
445
+ if result.returncode == 0:
446
+ logger.info("Sent message to %s via skcapstone CLI", recipient)
447
+ return True
448
+ logger.debug("skcapstone CLI send failed: %s", result.stderr)
449
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as exc:
450
+ logger.debug("skcapstone CLI unavailable: %s", exc)
451
+
452
+ # Fallback: write envelope JSON to outbox
453
+ outbox_dir = agent_home / "comms" / "outbox"
454
+ outbox_dir.mkdir(parents=True, exist_ok=True)
455
+ ts = datetime.now(timezone.utc).isoformat()
456
+ envelope = {
457
+ "recipient": recipient,
458
+ "message": message,
459
+ "queued_at": ts,
460
+ "delivered": False,
461
+ }
462
+ envelope_name = f"{recipient}_{int(time.time())}.json"
463
+ envelope_path = outbox_dir / envelope_name
464
+ try:
465
+ envelope_path.write_text(json.dumps(envelope, indent=2), encoding="utf-8")
466
+ logger.info("Queued message to %s at %s", recipient, envelope_path)
467
+ return True
468
+ except OSError as exc:
469
+ logger.error("Failed to queue message to %s: %s", recipient, exc)
470
+ return False
471
+
472
+
473
+ # ---------------------------------------------------------------------------
474
+ # Path parser
475
+ # ---------------------------------------------------------------------------
476
+
477
+
478
+ def _parse_path(path: str) -> Tuple[str, ...]:
479
+ """Parse a virtual FS path into clean components.
480
+
481
+ Args:
482
+ path: POSIX path string (e.g. ``/memories/short/abc123.md``).
483
+
484
+ Returns:
485
+ Tuple of path components with empty strings removed.
486
+ """
487
+ return tuple(p for p in path.strip("/").split("/") if p)
488
+
489
+
490
+ # ---------------------------------------------------------------------------
491
+ # SovereignFS
492
+ # ---------------------------------------------------------------------------
493
+
494
+
495
+ class SovereignFS:
496
+ """FUSE Operations implementation for the sovereign virtual filesystem.
497
+
498
+ Exposes agent memories, identity, inbox, outbox, and coordination tasks
499
+ as a read-mostly virtual filesystem. Writing to ``/outbox/<agent>.msg``
500
+ delivers a message via SKComm.
501
+
502
+ This class is designed to be used with ``fusepy``:
503
+
504
+ .. code-block:: python
505
+
506
+ import fuse
507
+ fs = SovereignFS(agent_home=Path("~/.skcapstone").expanduser())
508
+ fuse.FUSE(fs, mount_point, nothreads=True, foreground=True)
509
+
510
+ Args:
511
+ agent_home: Sovereign agent home directory.
512
+ """
513
+
514
+ def __init__(self, agent_home: Path) -> None:
515
+ self._home = agent_home
516
+ agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
517
+ self._memory_dir = agent_home / "agents" / agent_name / "memory"
518
+ # Buffer for outbox writes: maps virtual path → bytes written so far
519
+ self._outbox_buffers: Dict[str, bytes] = {}
520
+
521
+ # ------------------------------------------------------------------
522
+ # Internal helpers
523
+ # ------------------------------------------------------------------
524
+
525
+ def _memory_content(self, layer_slug: str, filename: str) -> Optional[bytes]:
526
+ """Resolve and render a memory file.
527
+
528
+ Args:
529
+ layer_slug: Virtual layer slug (``short``, ``mid``, or ``long``).
530
+ filename: Filename (``<id>.md``).
531
+
532
+ Returns:
533
+ Markdown bytes, or None if not found.
534
+ """
535
+ if filename.endswith(".md"):
536
+ memory_id = filename[:-3]
537
+ else:
538
+ memory_id = filename
539
+ layer_value = _LAYER_SLUG_TO_VALUE.get(layer_slug)
540
+ if not layer_value:
541
+ return None
542
+ return _load_memory_file(self._memory_dir, layer_value, memory_id)
543
+
544
+ def _resolve_file_content(self, parts: Tuple[str, ...]) -> Optional[bytes]:
545
+ """Resolve path components to file content.
546
+
547
+ Args:
548
+ parts: Parsed path components.
549
+
550
+ Returns:
551
+ File content as bytes, or None if the path is not a file.
552
+ """
553
+ if not parts:
554
+ return None
555
+
556
+ top = parts[0]
557
+
558
+ # /memories/short|mid|long/<id>.md
559
+ if top == _MEMORIES_DIR and len(parts) == 3:
560
+ return self._memory_content(parts[1], parts[2])
561
+
562
+ # /identity/card.json or /identity/fingerprint.txt
563
+ if top == _IDENTITY_DIR and len(parts) == 2:
564
+ if parts[1] == "card.json":
565
+ return _build_identity_card(self._home)
566
+ if parts[1] == "fingerprint.txt":
567
+ return _build_fingerprint_txt(self._home)
568
+
569
+ # /inbox/<filename>
570
+ if top == _INBOX_DIR and len(parts) == 2:
571
+ return _read_inbox_file(self._home, parts[1])
572
+
573
+ # /documents/<filename>
574
+ if top == _DOCUMENTS_DIR and len(parts) == 2:
575
+ return _read_document(self._home, parts[1])
576
+
577
+ # /coordination/<task>.json
578
+ if top == _COORDINATION_DIR and len(parts) == 2:
579
+ return _read_coordination_task(self._home, parts[1])
580
+
581
+ # /outbox/<agent>.msg — reads back from in-memory buffer
582
+ if top == _OUTBOX_DIR and len(parts) == 2:
583
+ path_key = "/" + "/".join(parts)
584
+ return self._outbox_buffers.get(path_key, b"")
585
+
586
+ return None
587
+
588
+ def _is_dir(self, parts: Tuple[str, ...]) -> bool:
589
+ """Check if a set of path components resolves to a virtual directory.
590
+
591
+ Args:
592
+ parts: Parsed path components.
593
+
594
+ Returns:
595
+ True if this path is a known virtual directory.
596
+ """
597
+ if not parts:
598
+ return True # root
599
+
600
+ top = parts[0]
601
+
602
+ if len(parts) == 1:
603
+ return top in _TOP_LEVEL_DIRS
604
+
605
+ if top == _MEMORIES_DIR and len(parts) == 2:
606
+ return parts[1] in _MEMORY_SUBDIRS
607
+
608
+ return False
609
+
610
+ def _is_file(self, parts: Tuple[str, ...]) -> bool:
611
+ """Check if path components resolve to a readable virtual file.
612
+
613
+ Args:
614
+ parts: Parsed path components.
615
+
616
+ Returns:
617
+ True if the path is a valid virtual file.
618
+ """
619
+ return self._resolve_file_content(parts) is not None
620
+
621
+ def _file_size(self, parts: Tuple[str, ...]) -> int:
622
+ """Return the byte size of a virtual file.
623
+
624
+ Args:
625
+ parts: Parsed path components.
626
+
627
+ Returns:
628
+ Size in bytes (0 if content is unavailable).
629
+ """
630
+ content = self._resolve_file_content(parts)
631
+ return len(content) if content is not None else 0
632
+
633
+ # ------------------------------------------------------------------
634
+ # FUSE Operations
635
+ # ------------------------------------------------------------------
636
+
637
+ def getattr(self, path: str, fh: Optional[int] = None) -> Dict[str, Any]:
638
+ """Return stat-like attributes for a path.
639
+
640
+ Args:
641
+ path: Virtual filesystem path.
642
+ fh: Open file handle (unused).
643
+
644
+ Returns:
645
+ Stat attribute dictionary.
646
+
647
+ Raises:
648
+ OSError: With ``errno.ENOENT`` if the path does not exist.
649
+ """
650
+ parts = _parse_path(path)
651
+
652
+ if self._is_dir(parts):
653
+ nlink = (
654
+ 2 + len(_MEMORY_SUBDIRS)
655
+ if parts and parts[0] == _MEMORIES_DIR and len(parts) == 1
656
+ else 2
657
+ )
658
+ return _dir_stat(nlink=nlink)
659
+
660
+ if self._is_file(parts):
661
+ size = self._file_size(parts)
662
+ writable = bool(parts) and parts[0] == _OUTBOX_DIR
663
+ return _file_stat(size=size, writable=writable)
664
+
665
+ raise OSError(errno.ENOENT, "No such file or directory", path)
666
+
667
+ def readdir(self, path: str, fh: Optional[int]) -> List[str]:
668
+ """Return directory listing for a virtual path.
669
+
670
+ Args:
671
+ path: Virtual filesystem path.
672
+ fh: Open file handle (unused).
673
+
674
+ Returns:
675
+ List of entry names including ``.`` and ``..``.
676
+
677
+ Raises:
678
+ OSError: With ``errno.ENOENT`` if the path is not a directory.
679
+ """
680
+ parts = _parse_path(path)
681
+ entries = [".", ".."]
682
+
683
+ if not parts:
684
+ # Root
685
+ entries.extend(_TOP_LEVEL_DIRS)
686
+ return entries
687
+
688
+ top = parts[0]
689
+
690
+ if top == _MEMORIES_DIR and len(parts) == 1:
691
+ entries.extend(_MEMORY_SUBDIRS)
692
+ return entries
693
+
694
+ if top == _MEMORIES_DIR and len(parts) == 2:
695
+ slug = parts[1]
696
+ layer_value = _LAYER_SLUG_TO_VALUE.get(slug)
697
+ if layer_value:
698
+ ids = _list_memory_ids(self._memory_dir, layer_value)
699
+ entries.extend(f"{mid}.md" for mid in ids)
700
+ return entries
701
+
702
+ if top == _IDENTITY_DIR and len(parts) == 1:
703
+ entries.extend(_IDENTITY_FILES)
704
+ return entries
705
+
706
+ if top == _INBOX_DIR and len(parts) == 1:
707
+ entries.extend(_list_inbox(self._home))
708
+ return entries
709
+
710
+ if top == _OUTBOX_DIR and len(parts) == 1:
711
+ # List any buffered outbox files
712
+ prefix = f"/{_OUTBOX_DIR}/"
713
+ entries.extend(k[len(prefix) :] for k in self._outbox_buffers if k.startswith(prefix))
714
+ return entries
715
+
716
+ if top == _DOCUMENTS_DIR and len(parts) == 1:
717
+ entries.extend(_list_documents(self._home))
718
+ return entries
719
+
720
+ if top == _COORDINATION_DIR and len(parts) == 1:
721
+ entries.extend(_list_coordination_tasks(self._home))
722
+ return entries
723
+
724
+ raise OSError(errno.ENOENT, "No such file or directory", path)
725
+
726
+ def open(self, path: str, flags: int) -> int:
727
+ """Open a virtual file.
728
+
729
+ Only read and write flags are honoured; outbox files accept writes.
730
+
731
+ Args:
732
+ path: Virtual filesystem path.
733
+ flags: Open flags bitmask (os.O_RDONLY, os.O_WRONLY, os.O_RDWR, etc.).
734
+
735
+ Returns:
736
+ Always 0 (no per-fd state needed).
737
+
738
+ Raises:
739
+ OSError: With appropriate errno if the path is not accessible.
740
+ """
741
+ parts = _parse_path(path)
742
+
743
+ is_write = bool(flags & (os.O_WRONLY | os.O_RDWR))
744
+ is_outbox = bool(parts) and parts[0] == _OUTBOX_DIR
745
+
746
+ if is_write:
747
+ if not is_outbox:
748
+ raise OSError(errno.EACCES, "Read-only filesystem", path)
749
+ # Initialize outbox buffer
750
+ self._outbox_buffers[path] = b""
751
+ return 0
752
+
753
+ if not self._is_file(parts):
754
+ raise OSError(errno.ENOENT, "No such file or directory", path)
755
+
756
+ return 0
757
+
758
+ def read(self, path: str, size: int, offset: int, fh: int) -> bytes:
759
+ """Read bytes from a virtual file.
760
+
761
+ Args:
762
+ path: Virtual filesystem path.
763
+ size: Maximum number of bytes to return.
764
+ offset: Byte offset to start reading from.
765
+ fh: Open file handle (unused).
766
+
767
+ Returns:
768
+ Bytes slice from the file content.
769
+
770
+ Raises:
771
+ OSError: With ``errno.ENOENT`` if the path is not a file.
772
+ """
773
+ parts = _parse_path(path)
774
+ content = self._resolve_file_content(parts)
775
+ if content is None:
776
+ raise OSError(errno.ENOENT, "No such file or directory", path)
777
+ return content[offset : offset + size]
778
+
779
+ def write(self, path: str, data: bytes, offset: int, fh: int) -> int:
780
+ """Write bytes to an outbox file, buffering until flush.
781
+
782
+ Only ``/outbox/<agent_name>.msg`` paths are writable. On the first
783
+ write the buffer is initialised; subsequent writes append.
784
+
785
+ Args:
786
+ path: Virtual filesystem path (must be under ``/outbox/``).
787
+ data: Bytes to write.
788
+ offset: Byte offset (used to detect new vs. appended writes).
789
+ fh: Open file handle (unused).
790
+
791
+ Returns:
792
+ Number of bytes written.
793
+
794
+ Raises:
795
+ OSError: With ``errno.EACCES`` if the path is not in ``/outbox/``.
796
+ """
797
+ parts = _parse_path(path)
798
+ if not parts or parts[0] != _OUTBOX_DIR:
799
+ raise OSError(errno.EACCES, "Read-only filesystem", path)
800
+
801
+ if path not in self._outbox_buffers or offset == 0:
802
+ self._outbox_buffers[path] = b""
803
+
804
+ buf = self._outbox_buffers.get(path, b"")
805
+ self._outbox_buffers[path] = buf[:offset] + data
806
+ return len(data)
807
+
808
+ def create(self, path: str, mode: int, fi: Optional[Any] = None) -> int:
809
+ """Create a new outbox file.
810
+
811
+ Only ``/outbox/<agent_name>.msg`` paths may be created.
812
+
813
+ Args:
814
+ path: Virtual filesystem path.
815
+ mode: File permission mode (stored but not enforced in virtual FS).
816
+ fi: FUSE file info structure (unused).
817
+
818
+ Returns:
819
+ Always 0.
820
+
821
+ Raises:
822
+ OSError: With ``errno.EACCES`` if the path is not under ``/outbox/``.
823
+ """
824
+ parts = _parse_path(path)
825
+ if not parts or parts[0] != _OUTBOX_DIR:
826
+ raise OSError(errno.EACCES, "Read-only filesystem", path)
827
+
828
+ self._outbox_buffers[path] = b""
829
+ return 0
830
+
831
+ def flush(self, path: str, fh: int) -> int:
832
+ """Flush an outbox file buffer, delivering the message via SKComm.
833
+
834
+ Called when an outbox file handle is closed. The accumulated buffer
835
+ is interpreted as the message body; the filename (without ``.msg``)
836
+ is used as the recipient agent name.
837
+
838
+ Args:
839
+ path: Virtual filesystem path.
840
+ fh: Open file handle (unused).
841
+
842
+ Returns:
843
+ Always 0.
844
+ """
845
+ parts = _parse_path(path)
846
+ if not parts or parts[0] != _OUTBOX_DIR:
847
+ return 0
848
+
849
+ filename = parts[-1]
850
+ # Strip .msg suffix to get the recipient name
851
+ recipient = filename[:-4] if filename.endswith(".msg") else filename
852
+
853
+ message_bytes = self._outbox_buffers.get(path, b"")
854
+ if not message_bytes:
855
+ return 0
856
+
857
+ try:
858
+ message = message_bytes.decode("utf-8").strip()
859
+ except UnicodeDecodeError:
860
+ logger.warning("Outbox message for %s is not valid UTF-8", recipient)
861
+ return 0
862
+
863
+ if message:
864
+ _send_via_skcomm(self._home, recipient, message)
865
+
866
+ # Clear buffer after sending
867
+ self._outbox_buffers.pop(path, None)
868
+ return 0
869
+
870
+ def release(self, path: str, fh: int) -> int:
871
+ """Release a file handle, flushing outbox if needed.
872
+
873
+ Args:
874
+ path: Virtual filesystem path.
875
+ fh: Open file handle.
876
+
877
+ Returns:
878
+ Always 0.
879
+ """
880
+ # Flush any remaining outbox data
881
+ self.flush(path, fh)
882
+ return 0
883
+
884
+ def truncate(self, path: str, length: int, fh: Optional[int] = None) -> None:
885
+ """Truncate a file in the outbox buffer.
886
+
887
+ Args:
888
+ path: Virtual filesystem path.
889
+ length: Target length in bytes.
890
+ fh: Open file handle (unused).
891
+
892
+ Raises:
893
+ OSError: With ``errno.EACCES`` if the path is not under ``/outbox/``.
894
+ """
895
+ parts = _parse_path(path)
896
+ if not parts or parts[0] != _OUTBOX_DIR:
897
+ raise OSError(errno.EACCES, "Read-only filesystem", path)
898
+
899
+ buf = self._outbox_buffers.get(path, b"")
900
+ self._outbox_buffers[path] = buf[:length]
901
+
902
+ # Pass-through stubs for operations that the kernel may call
903
+ def chmod(self, path: str, mode: int) -> int:
904
+ """Ignore chmod on the virtual filesystem."""
905
+ return 0
906
+
907
+ def chown(self, path: str, uid: int, gid: int) -> int:
908
+ """Ignore chown on the virtual filesystem."""
909
+ return 0
910
+
911
+ def utimens(self, path: str, times: Optional[Tuple[float, float]] = None) -> int:
912
+ """Ignore utimens on the virtual filesystem."""
913
+ return 0
914
+
915
+
916
+ # ---------------------------------------------------------------------------
917
+ # FUSEDaemon — lifecycle manager
918
+ # ---------------------------------------------------------------------------
919
+
920
+
921
+ class FUSEDaemon:
922
+ """Lifecycle manager for the SovereignFS FUSE mount.
923
+
924
+ Handles mounting, unmounting, and status checks for the sovereign
925
+ virtual filesystem.
926
+
927
+ Args:
928
+ mount_point: Directory to mount the filesystem at.
929
+ Defaults to ``~/.sovereign/mount/``.
930
+ agent_home: Agent home directory.
931
+ Defaults to ``~/.skcapstone``.
932
+ """
933
+
934
+ _PID_FILE = "fuse.pid"
935
+ _STATE_FILE = "fuse_state.json"
936
+
937
+ def __init__(
938
+ self,
939
+ mount_point: Optional[Path] = None,
940
+ agent_home: Optional[Path] = None,
941
+ ) -> None:
942
+ self._mount_point = (mount_point or Path("~/.sovereign/mount")).expanduser()
943
+ self._agent_home = (agent_home or Path("~/.skcapstone")).expanduser()
944
+ self._state_dir = self._agent_home / "fuse"
945
+
946
+ def _state_file(self) -> Path:
947
+ """Path to the FUSE daemon state file.
948
+
949
+ Returns:
950
+ Absolute path to the JSON state file.
951
+ """
952
+ return self._state_dir / self._STATE_FILE
953
+
954
+ def _pid_file(self) -> Path:
955
+ """Path to the FUSE daemon PID file.
956
+
957
+ Returns:
958
+ Absolute path to the PID file.
959
+ """
960
+ return self._state_dir / self._PID_FILE
961
+
962
+ def _write_state(self, mounted: bool, pid: Optional[int] = None) -> None:
963
+ """Persist the FUSE daemon state to disk.
964
+
965
+ Args:
966
+ mounted: Whether the filesystem is currently mounted.
967
+ pid: Process ID of the FUSE daemon (if any).
968
+ """
969
+ self._state_dir.mkdir(parents=True, exist_ok=True)
970
+ state = {
971
+ "mounted": mounted,
972
+ "mount_point": str(self._mount_point),
973
+ "agent_home": str(self._agent_home),
974
+ "pid": pid,
975
+ "updated_at": datetime.now(timezone.utc).isoformat(),
976
+ }
977
+ self._state_file().write_text(json.dumps(state, indent=2), encoding="utf-8")
978
+
979
+ def _read_state(self) -> Optional[Dict[str, Any]]:
980
+ """Read the FUSE daemon state from disk.
981
+
982
+ Returns:
983
+ State dictionary, or None if missing or corrupt.
984
+ """
985
+ path = self._state_file()
986
+ if not path.exists():
987
+ return None
988
+ try:
989
+ return json.loads(path.read_text(encoding="utf-8"))
990
+ except (json.JSONDecodeError, OSError):
991
+ return None
992
+
993
+ def _is_mounted(self) -> bool:
994
+ """Check whether the mount point is currently active.
995
+
996
+ Uses ``/proc/mounts`` on Linux for reliable detection.
997
+
998
+ Returns:
999
+ True if the mount point appears to be mounted.
1000
+ """
1001
+ mount_str = str(self._mount_point)
1002
+
1003
+ # Linux: parse /proc/mounts
1004
+ proc_mounts = Path("/proc/mounts")
1005
+ if proc_mounts.exists():
1006
+ try:
1007
+ for line in proc_mounts.read_text(encoding="utf-8").splitlines():
1008
+ parts = line.split()
1009
+ if len(parts) >= 2 and parts[1] == mount_str:
1010
+ return True
1011
+ except OSError as exc:
1012
+ logger.warning("Failed to read /proc/mounts: %s", exc)
1013
+ return False
1014
+
1015
+ # macOS / other: use mount command
1016
+ try:
1017
+ result = subprocess.run(
1018
+ ["mount"],
1019
+ capture_output=True,
1020
+ text=True,
1021
+ timeout=5,
1022
+ )
1023
+ return mount_str in result.stdout
1024
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
1025
+ return False
1026
+
1027
+ def start(self, foreground: bool = False) -> bool:
1028
+ """Mount the sovereign virtual filesystem.
1029
+
1030
+ Attempts to import ``fuse`` (fusepy) and mount the SovereignFS
1031
+ filesystem at the configured mount point. If ``foreground=False``
1032
+ the mount runs as a daemon process.
1033
+
1034
+ Args:
1035
+ foreground: If True, mount in the foreground (blocks until unmounted).
1036
+ Useful for debugging.
1037
+
1038
+ Returns:
1039
+ True if the mount was initiated successfully.
1040
+ """
1041
+ try:
1042
+ import fuse as _fuse # type: ignore[import]
1043
+ except ImportError:
1044
+ logger.error("fusepy is not installed. Install with: pip install skcapstone[fuse]")
1045
+ return False
1046
+
1047
+ if self._is_mounted():
1048
+ logger.info("Already mounted at %s", self._mount_point)
1049
+ return True
1050
+
1051
+ self._mount_point.mkdir(parents=True, exist_ok=True)
1052
+ self._state_dir.mkdir(parents=True, exist_ok=True)
1053
+
1054
+ if foreground:
1055
+ logger.info("Mounting sovereign filesystem at %s (foreground)", self._mount_point)
1056
+ try:
1057
+ fs = SovereignFS(agent_home=self._agent_home)
1058
+ self._write_state(mounted=True, pid=os.getpid())
1059
+ _fuse.FUSE(
1060
+ fs,
1061
+ str(self._mount_point),
1062
+ nothreads=True,
1063
+ foreground=True,
1064
+ allow_other=False,
1065
+ )
1066
+ return True
1067
+ except Exception as exc:
1068
+ logger.error("Failed to mount filesystem: %s", exc)
1069
+ self._write_state(mounted=False)
1070
+ return False
1071
+ else:
1072
+ # Background mount: re-exec this function in a child process
1073
+ logger.info("Mounting sovereign filesystem at %s (background)", self._mount_point)
1074
+ try:
1075
+ proc = subprocess.Popen(
1076
+ [
1077
+ sys.executable,
1078
+ "-c",
1079
+ (
1080
+ "from pathlib import Path; "
1081
+ "from skcapstone.fuse_mount import FUSEDaemon; "
1082
+ f"FUSEDaemon("
1083
+ f" mount_point=Path({str(self._mount_point)!r}), "
1084
+ f" agent_home=Path({str(self._agent_home)!r})"
1085
+ f").start(foreground=True)"
1086
+ ),
1087
+ ],
1088
+ start_new_session=True,
1089
+ stdout=subprocess.DEVNULL,
1090
+ stderr=subprocess.DEVNULL,
1091
+ )
1092
+ self._write_state(mounted=True, pid=proc.pid)
1093
+ self._pid_file().write_text(str(proc.pid), encoding="utf-8")
1094
+ logger.info("FUSE daemon started with pid %d", proc.pid)
1095
+ return True
1096
+ except OSError as exc:
1097
+ logger.error("Failed to start FUSE daemon: %s", exc)
1098
+ self._write_state(mounted=False)
1099
+ return False
1100
+
1101
+ def stop(self) -> bool:
1102
+ """Unmount the sovereign virtual filesystem.
1103
+
1104
+ Attempts ``fusermount -u`` (Linux) or ``umount`` (macOS).
1105
+
1106
+ Returns:
1107
+ True if the filesystem was successfully unmounted.
1108
+ """
1109
+ if not self._is_mounted():
1110
+ logger.info("Not mounted at %s", self._mount_point)
1111
+ self._write_state(mounted=False)
1112
+ return True
1113
+
1114
+ mount_str = str(self._mount_point)
1115
+
1116
+ # Linux: fusermount
1117
+ for cmd in (["fusermount", "-u", mount_str], ["umount", mount_str]):
1118
+ try:
1119
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
1120
+ if result.returncode == 0:
1121
+ logger.info("Unmounted %s", mount_str)
1122
+ self._write_state(mounted=False)
1123
+ return True
1124
+ logger.debug(
1125
+ "%s failed (rc=%d): %s",
1126
+ " ".join(cmd),
1127
+ result.returncode,
1128
+ result.stderr.strip(),
1129
+ )
1130
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as exc:
1131
+ logger.debug("Unmount command %s failed: %s", cmd, exc)
1132
+
1133
+ logger.error("Could not unmount %s — try: fusermount -u %s", mount_str, mount_str)
1134
+ return False
1135
+
1136
+ def status(self) -> Dict[str, Any]:
1137
+ """Return the current FUSE daemon status.
1138
+
1139
+ Returns:
1140
+ Dictionary with keys:
1141
+ - ``mounted`` (bool): Whether the FS is currently mounted.
1142
+ - ``mount_point`` (str): Mount point path.
1143
+ - ``agent_home`` (str): Agent home path.
1144
+ - ``pid`` (int | None): Daemon process ID, if known.
1145
+ - ``updated_at`` (str | None): Last state update timestamp.
1146
+ """
1147
+ state = self._read_state() or {}
1148
+ mounted = self._is_mounted()
1149
+
1150
+ return {
1151
+ "mounted": mounted,
1152
+ "mount_point": str(self._mount_point),
1153
+ "agent_home": str(self._agent_home),
1154
+ "pid": state.get("pid"),
1155
+ "updated_at": state.get("updated_at"),
1156
+ }