@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
@@ -6,10 +6,18 @@ Then let the transport layer do its job.
6
6
 
7
7
  The vault is a tarball of selected ~/.skcapstone/ directories,
8
8
  encrypted with PGP (via CapAuth) and signed to prove authenticity.
9
+
10
+ Hardening guarantees:
11
+ - SHA-256 hashes for every file in the archive
12
+ - SHA-256 hash of the archive itself
13
+ - GPG detached signature on the manifest
14
+ - Integrity verification before extraction
15
+ - Key rotation re-encrypts all existing vaults
9
16
  """
10
17
 
11
18
  from __future__ import annotations
12
19
 
20
+ import hashlib
13
21
  import json
14
22
  import logging
15
23
  import os
@@ -28,6 +36,42 @@ PILLARS_TO_SYNC = ["identity", "memory", "trust", "config", "skills"]
28
36
  EXCLUDE_PATTERNS = {"__pycache__", ".pyc", ".git", "audit.log"}
29
37
 
30
38
 
39
+ class VaultIntegrityError(Exception):
40
+ """Raised when vault integrity verification fails."""
41
+
42
+
43
+ class VaultSignatureError(Exception):
44
+ """Raised when vault signature verification fails."""
45
+
46
+
47
+ def _sha256_file(path: Path) -> str:
48
+ """Compute SHA-256 hex digest of a file.
49
+
50
+ Args:
51
+ path: File to hash.
52
+
53
+ Returns:
54
+ Hex-encoded SHA-256 digest.
55
+ """
56
+ h = hashlib.sha256()
57
+ with open(path, "rb") as f:
58
+ for chunk in iter(lambda: f.read(8192), b""):
59
+ h.update(chunk)
60
+ return h.hexdigest()
61
+
62
+
63
+ def _sha256_bytes(data: bytes) -> str:
64
+ """Compute SHA-256 hex digest of bytes.
65
+
66
+ Args:
67
+ data: Bytes to hash.
68
+
69
+ Returns:
70
+ Hex-encoded SHA-256 digest.
71
+ """
72
+ return hashlib.sha256(data).hexdigest()
73
+
74
+
31
75
  class Vault:
32
76
  """Manages creation and extraction of encrypted agent state vaults.
33
77
 
@@ -57,6 +101,7 @@ class Vault:
57
101
  pillars: Optional[list[str]] = None,
58
102
  encrypt: bool = False,
59
103
  passphrase: Optional[str] = None,
104
+ sign: bool = False,
60
105
  ) -> Path:
61
106
  """Pack agent state into a vault archive.
62
107
 
@@ -64,6 +109,7 @@ class Vault:
64
109
  pillars: Which pillars to include. Defaults to all syncable pillars.
65
110
  encrypt: Whether to GPG-encrypt the archive.
66
111
  passphrase: Passphrase for encryption (required if encrypt=True).
112
+ sign: Whether to GPG-sign the manifest.
67
113
 
68
114
  Returns:
69
115
  Path to the created vault file (.tar.gz or .tar.gz.gpg).
@@ -75,6 +121,8 @@ class Vault:
75
121
  archive_path = self.vault_dir / archive_name
76
122
 
77
123
  included = []
124
+ file_hashes: dict[str, str] = {}
125
+
78
126
  with tarfile.open(archive_path, "w:gz") as tar:
79
127
  for pillar in target_pillars:
80
128
  pillar_dir = self.agent_home / pillar
@@ -94,6 +142,7 @@ class Vault:
94
142
  full_path.relative_to(self.agent_home)
95
143
  )
96
144
  tar.add(str(full_path), arcname=arcname)
145
+ file_hashes[arcname] = _sha256_file(full_path)
97
146
 
98
147
  included.append(pillar)
99
148
 
@@ -103,6 +152,9 @@ class Vault:
103
152
  str(manifest_path),
104
153
  arcname="manifest.json",
105
154
  )
155
+ file_hashes["manifest.json"] = _sha256_file(manifest_path)
156
+
157
+ archive_hash = _sha256_file(archive_path)
106
158
 
107
159
  manifest = VaultManifest(
108
160
  agent_name=self._get_agent_name(),
@@ -110,12 +162,21 @@ class Vault:
110
162
  created_at=datetime.now(timezone.utc),
111
163
  pillars_included=included,
112
164
  encrypted=encrypt,
165
+ file_hashes=file_hashes,
166
+ archive_hash=archive_hash,
167
+ fingerprint=self._get_agent_fingerprint(),
113
168
  )
114
169
 
170
+ if sign:
171
+ sig = self._sign_manifest(manifest, passphrase)
172
+ if sig:
173
+ manifest.signature = sig
174
+ manifest.signed_by = self._get_agent_fingerprint()
175
+
115
176
  manifest_file = archive_path.with_suffix(".manifest.json")
116
177
  manifest_file.write_text(
117
178
  manifest.model_dump_json(indent=2)
118
- )
179
+ , encoding="utf-8")
119
180
 
120
181
  if encrypt:
121
182
  encrypted_path = self._encrypt_vault(
@@ -126,9 +187,10 @@ class Vault:
126
187
  return encrypted_path
127
188
 
128
189
  logger.info(
129
- "Vault packed: %s (%d pillars)",
190
+ "Vault packed: %s (%d pillars, %d files hashed)",
130
191
  archive_path,
131
192
  len(included),
193
+ len(file_hashes),
132
194
  )
133
195
  return archive_path
134
196
 
@@ -138,40 +200,130 @@ class Vault:
138
200
  decrypt: bool = False,
139
201
  passphrase: Optional[str] = None,
140
202
  target: Optional[Path] = None,
203
+ verify_signature: bool = True,
204
+ verify_hashes: bool = True,
141
205
  ) -> Path:
142
206
  """Unpack a vault archive to restore agent state.
143
207
 
208
+ Verifies integrity before extraction when manifest is present.
209
+
144
210
  Args:
145
211
  vault_path: Path to the vault file.
146
212
  decrypt: Whether the vault is GPG-encrypted.
147
213
  passphrase: Passphrase for decryption.
148
214
  target: Where to extract. Defaults to agent_home.
215
+ verify_signature: Whether to verify the manifest signature.
216
+ verify_hashes: Whether to verify SHA-256 file hashes.
149
217
 
150
218
  Returns:
151
219
  Path to the extraction directory.
220
+
221
+ Raises:
222
+ VaultSignatureError: If signature verification fails.
223
+ VaultIntegrityError: If hash verification fails.
152
224
  """
153
225
  extract_to = target or self.agent_home
154
226
 
155
227
  if decrypt:
156
228
  vault_path = self._decrypt_vault(vault_path, passphrase)
157
229
 
158
- manifest_file = vault_path.with_suffix(".manifest.json")
159
- if manifest_file.exists():
160
- manifest_data = json.loads(manifest_file.read_text())
161
- manifest = VaultManifest(**manifest_data)
162
- logger.info(
163
- "Restoring vault from %s (agent=%s, pillars=%s)",
164
- manifest.source_host,
165
- manifest.agent_name,
166
- manifest.pillars_included,
167
- )
230
+ manifest = self._load_and_verify_manifest(
231
+ vault_path, verify_signature
232
+ )
233
+
234
+ if manifest and manifest.archive_hash and verify_hashes:
235
+ actual_hash = _sha256_file(vault_path)
236
+ if actual_hash != manifest.archive_hash:
237
+ raise VaultIntegrityError(
238
+ f"Archive hash mismatch: expected {manifest.archive_hash}, "
239
+ f"got {actual_hash}"
240
+ )
241
+ logger.info("Archive integrity verified (SHA-256)")
168
242
 
169
243
  with tarfile.open(vault_path, "r:gz") as tar:
170
244
  tar.extractall(path=extract_to, filter="data")
171
245
 
246
+ if manifest and manifest.file_hashes and verify_hashes:
247
+ self._verify_file_hashes(extract_to, manifest.file_hashes)
248
+ logger.info(
249
+ "All %d file hashes verified", len(manifest.file_hashes)
250
+ )
251
+
172
252
  logger.info("Vault unpacked to %s", extract_to)
173
253
  return extract_to
174
254
 
255
+ def rotate_keys(
256
+ self,
257
+ old_passphrase: Optional[str] = None,
258
+ new_passphrase: Optional[str] = None,
259
+ ) -> list[Path]:
260
+ """Re-encrypt all vault archives with a new passphrase.
261
+
262
+ Decrypts each .gpg vault with the old passphrase, then
263
+ re-encrypts with the new one. Non-encrypted vaults are
264
+ encrypted with the new passphrase.
265
+
266
+ Args:
267
+ old_passphrase: Current passphrase for existing encrypted vaults.
268
+ new_passphrase: New passphrase for re-encryption.
269
+
270
+ Returns:
271
+ List of paths to re-encrypted vaults.
272
+ """
273
+ rotated: list[Path] = []
274
+
275
+ for vault_file in sorted(self.vault_dir.glob("vault-*.tar.gz.gpg")):
276
+ try:
277
+ decrypted = self._decrypt_vault(vault_file, old_passphrase)
278
+ new_encrypted = self._encrypt_vault(decrypted, new_passphrase)
279
+ decrypted.unlink()
280
+ vault_file.unlink()
281
+
282
+ old_manifest = vault_file.with_name(
283
+ vault_file.name.replace(".tar.gz.gpg", ".tar.gz.manifest.json")
284
+ )
285
+ if old_manifest.exists():
286
+ data = json.loads(old_manifest.read_text(encoding="utf-8"))
287
+ data["encrypted"] = True
288
+ data["archive_hash"] = _sha256_file(new_encrypted)
289
+ old_manifest.write_text(json.dumps(data, indent=2), encoding="utf-8")
290
+
291
+ rotated.append(new_encrypted)
292
+ logger.info("Rotated encryption: %s", new_encrypted.name)
293
+ except RuntimeError as exc:
294
+ logger.error(
295
+ "Failed to rotate %s: %s", vault_file.name, exc
296
+ )
297
+
298
+ for vault_file in sorted(self.vault_dir.glob("vault-*.tar.gz")):
299
+ if vault_file.name.endswith(".gpg"):
300
+ continue
301
+ manifest_file = vault_file.with_suffix(".manifest.json")
302
+ if manifest_file.exists():
303
+ data = json.loads(manifest_file.read_text(encoding="utf-8"))
304
+ if data.get("encrypted"):
305
+ continue
306
+
307
+ try:
308
+ new_encrypted = self._encrypt_vault(vault_file, new_passphrase)
309
+ vault_file.unlink()
310
+
311
+ if manifest_file.exists():
312
+ data = json.loads(manifest_file.read_text(encoding="utf-8"))
313
+ data["encrypted"] = True
314
+ data["archive_hash"] = _sha256_file(new_encrypted)
315
+ manifest_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
316
+
317
+ rotated.append(new_encrypted)
318
+ logger.info("Encrypted plaintext vault: %s", new_encrypted.name)
319
+ except (RuntimeError, OSError) as exc:
320
+ logger.error(
321
+ "Failed to encrypt %s: %s", vault_file.name, exc
322
+ )
323
+
324
+ logger.info("Key rotation complete: %d vaults rotated", len(rotated))
325
+ return rotated
326
+
175
327
  def list_vaults(self) -> list[dict]:
176
328
  """List all vault archives in the vault directory.
177
329
 
@@ -183,16 +335,191 @@ class Vault:
183
335
  if f.suffix == ".json":
184
336
  continue
185
337
  manifest_file = f.with_suffix(".manifest.json")
186
- meta = {"path": f, "size": f.stat().st_size}
338
+ meta: dict = {"path": f, "size": f.stat().st_size}
187
339
  if manifest_file.exists():
188
340
  try:
189
- data = json.loads(manifest_file.read_text())
341
+ data = json.loads(manifest_file.read_text(encoding="utf-8"))
190
342
  meta.update(data)
191
343
  except json.JSONDecodeError:
192
344
  pass
193
345
  vaults.append(meta)
194
346
  return vaults
195
347
 
348
+ def _load_and_verify_manifest(
349
+ self, vault_path: Path, verify_signature: bool
350
+ ) -> Optional[VaultManifest]:
351
+ """Load manifest for a vault and optionally verify its signature.
352
+
353
+ Args:
354
+ vault_path: Path to the vault archive.
355
+ verify_signature: Whether to verify the GPG signature.
356
+
357
+ Returns:
358
+ VaultManifest if found, None otherwise.
359
+
360
+ Raises:
361
+ VaultSignatureError: If signature is present but invalid.
362
+ """
363
+ manifest_file = vault_path.with_suffix(".manifest.json")
364
+ if not manifest_file.exists():
365
+ base = vault_path.name
366
+ if base.endswith(".gpg"):
367
+ base = base[:-4]
368
+ manifest_file = vault_path.parent / (base + ".manifest.json")
369
+
370
+ if not manifest_file.exists():
371
+ logger.debug("No manifest found for %s", vault_path.name)
372
+ return None
373
+
374
+ manifest_data = json.loads(manifest_file.read_text(encoding="utf-8"))
375
+ manifest = VaultManifest(**manifest_data)
376
+
377
+ if verify_signature and manifest.signature:
378
+ if not self._verify_signature(manifest):
379
+ raise VaultSignatureError(
380
+ f"Invalid signature on manifest for {vault_path.name}"
381
+ )
382
+ logger.info("Manifest signature verified")
383
+
384
+ logger.info(
385
+ "Restoring vault from %s (agent=%s, pillars=%s)",
386
+ manifest.source_host,
387
+ manifest.agent_name,
388
+ manifest.pillars_included,
389
+ )
390
+ return manifest
391
+
392
+ def _verify_file_hashes(
393
+ self, extract_dir: Path, expected_hashes: dict[str, str]
394
+ ) -> None:
395
+ """Verify SHA-256 hashes of extracted files.
396
+
397
+ Args:
398
+ extract_dir: Directory where files were extracted.
399
+ expected_hashes: Map of relative path -> expected SHA-256 hex.
400
+
401
+ Raises:
402
+ VaultIntegrityError: If any file hash doesn't match.
403
+ """
404
+ for rel_path, expected_hash in expected_hashes.items():
405
+ file_path = extract_dir / rel_path
406
+ if not file_path.exists():
407
+ logger.warning("Expected file missing: %s", rel_path)
408
+ continue
409
+ actual_hash = _sha256_file(file_path)
410
+ if actual_hash != expected_hash:
411
+ raise VaultIntegrityError(
412
+ f"Hash mismatch for {rel_path}: "
413
+ f"expected {expected_hash}, got {actual_hash}"
414
+ )
415
+
416
+ def _sign_manifest(
417
+ self, manifest: VaultManifest, passphrase: Optional[str]
418
+ ) -> Optional[str]:
419
+ """Sign the manifest data with the agent's private key.
420
+
421
+ Args:
422
+ manifest: The manifest to sign (signature field excluded).
423
+ passphrase: Key passphrase.
424
+
425
+ Returns:
426
+ Base64 signature string, or None if signing unavailable.
427
+ """
428
+ try:
429
+ from capauth.crypto import get_backend
430
+
431
+ backend = get_backend()
432
+ private_key_path = self.agent_home / "identity" / "agent.key"
433
+ if not private_key_path.exists():
434
+ logger.debug("No private key found for signing")
435
+ return None
436
+
437
+ sign_data = manifest.model_dump_json(
438
+ exclude={"signature", "signed_by"}
439
+ ).encode()
440
+ sig = backend.sign(
441
+ sign_data,
442
+ private_key_path.read_text(encoding="utf-8"),
443
+ passphrase or "",
444
+ )
445
+ return sig
446
+ except (ImportError, Exception) as exc:
447
+ logger.debug("CapAuth signing unavailable: %s", exc)
448
+
449
+ try:
450
+ import subprocess
451
+
452
+ private_key_path = self.agent_home / "identity" / "agent.key"
453
+ if not private_key_path.exists():
454
+ return None
455
+
456
+ sign_data = manifest.model_dump_json(
457
+ exclude={"signature", "signed_by"}
458
+ ).encode()
459
+
460
+ result = subprocess.run(
461
+ ["gpg", "--batch", "--yes", "--detach-sign", "--armor",
462
+ "--default-key", manifest.fingerprint or ""],
463
+ input=sign_data,
464
+ capture_output=True,
465
+ check=False,
466
+ )
467
+ if result.returncode == 0:
468
+ return result.stdout.decode()
469
+ except Exception as exc:
470
+ logger.debug("GPG signing failed: %s", exc)
471
+
472
+ return None
473
+
474
+ def _verify_signature(self, manifest: VaultManifest) -> bool:
475
+ """Verify a manifest's GPG signature.
476
+
477
+ Args:
478
+ manifest: Manifest with signature to verify.
479
+
480
+ Returns:
481
+ True if signature is valid, False otherwise.
482
+ """
483
+ if not manifest.signature:
484
+ return False
485
+
486
+ try:
487
+ from capauth.crypto import get_backend
488
+
489
+ backend = get_backend()
490
+ sign_data = manifest.model_dump_json(
491
+ exclude={"signature", "signed_by"}
492
+ ).encode()
493
+ return backend.verify(sign_data, manifest.signature)
494
+ except (ImportError, Exception) as exc:
495
+ logger.debug("CapAuth verify unavailable: %s", exc)
496
+
497
+ try:
498
+ import subprocess
499
+
500
+ sign_data = manifest.model_dump_json(
501
+ exclude={"signature", "signed_by"}
502
+ ).encode()
503
+
504
+ with tempfile.NamedTemporaryFile(suffix=".sig", delete=False) as sig_file:
505
+ sig_file.write(manifest.signature.encode())
506
+ sig_path = sig_file.name
507
+
508
+ try:
509
+ result = subprocess.run(
510
+ ["gpg", "--batch", "--verify", sig_path, "-"],
511
+ input=sign_data,
512
+ capture_output=True,
513
+ check=False,
514
+ )
515
+ return result.returncode == 0
516
+ finally:
517
+ Path(sig_path).unlink(missing_ok=True)
518
+ except Exception as exc:
519
+ logger.debug("GPG verify failed: %s", exc)
520
+
521
+ return False
522
+
196
523
  def _encrypt_vault(
197
524
  self, archive_path: Path, passphrase: Optional[str]
198
525
  ) -> Path:
@@ -211,17 +538,17 @@ class Vault:
211
538
  data = archive_path.read_bytes()
212
539
  identity_file = self.agent_home / "identity" / "identity.json"
213
540
  if identity_file.exists():
214
- identity = json.loads(identity_file.read_text())
541
+ identity = json.loads(identity_file.read_text(encoding="utf-8"))
215
542
  private_key_path = (
216
543
  self.agent_home / "identity" / "agent.key"
217
544
  )
218
545
  if private_key_path.exists():
219
546
  signed = backend.sign(
220
547
  data,
221
- private_key_path.read_text(),
548
+ private_key_path.read_text(encoding="utf-8"),
222
549
  passphrase or "",
223
550
  )
224
- output_path.write_text(signed)
551
+ output_path.write_text(signed, encoding="utf-8")
225
552
  logger.info("Vault encrypted via CapAuth")
226
553
  return output_path
227
554
  except (ImportError, Exception) as exc:
@@ -277,8 +604,19 @@ class Vault:
277
604
  manifest = self.agent_home / "manifest.json"
278
605
  if manifest.exists():
279
606
  try:
280
- data = json.loads(manifest.read_text())
607
+ data = json.loads(manifest.read_text(encoding="utf-8"))
281
608
  return data.get("name", "unknown")
282
609
  except json.JSONDecodeError:
283
610
  pass
284
611
  return "unknown"
612
+
613
+ def _get_agent_fingerprint(self) -> Optional[str]:
614
+ """Read agent PGP fingerprint from identity."""
615
+ identity_file = self.agent_home / "identity" / "identity.json"
616
+ if identity_file.exists():
617
+ try:
618
+ data = json.loads(identity_file.read_text(encoding="utf-8"))
619
+ return data.get("fingerprint")
620
+ except json.JSONDecodeError:
621
+ pass
622
+ return None