ltcai 11.5.2 → 11.7.0

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 (443) hide show
  1. package/README.md +93 -148
  2. package/bin/ltcai.js +234 -24
  3. package/docs/CHANGELOG.md +119 -0
  4. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  5. package/docs/DEVELOPMENT.md +8 -6
  6. package/docs/ENTERPRISE.md +2 -1
  7. package/docs/MULTI_AGENT_RUNTIME.md +12 -5
  8. package/docs/ONBOARDING.md +1 -1
  9. package/docs/OPERATIONS.md +6 -3
  10. package/docs/REALTIME_COLLABORATION.md +1 -1
  11. package/docs/TRUST_MODEL.md +1 -1
  12. package/docs/WHY_LATTICE.md +1 -1
  13. package/docs/WORKFLOW_DESIGNER.md +3 -2
  14. package/docs/kg-schema.md +1 -1
  15. package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
  16. package/lattice_brain/__init__.py +42 -79
  17. package/lattice_brain/graph/__init__.py +9 -24
  18. package/lattice_brain/graph/_kg_common/__init__.py +13 -24
  19. package/lattice_brain/ingestion/__init__.py +19 -58
  20. package/lattice_brain/ingestion/pipeline.py +20 -398
  21. package/lattice_brain/multimodal/__init__.py +7 -18
  22. package/lattice_brain/multimodal/images.py +6 -264
  23. package/lattice_brain/multimodal/video.py +8 -247
  24. package/lattice_brain/runtime/__init__.py +8 -79
  25. package/lattice_brain/runtime/hooks.py +22 -584
  26. package/latticeai/__init__.py +1 -1
  27. package/latticeai/api/agent_worker_seam.py +7 -80
  28. package/latticeai/api/health.py +6 -20
  29. package/latticeai/api/local_files.py +16 -613
  30. package/latticeai/api/models.py +12 -87
  31. package/latticeai/api/search.py +17 -255
  32. package/latticeai/api/tools.py +69 -750
  33. package/latticeai/api/voice_capture.py +8 -70
  34. package/latticeai/api/worker_compute.py +842 -0
  35. package/latticeai/api/worker_seams.py +216 -0
  36. package/latticeai/app_factory.py +23 -232
  37. package/latticeai/cli/entrypoint.py +36 -249
  38. package/latticeai/core/agent_permission.py +16 -91
  39. package/latticeai/core/messages.py +89 -531
  40. package/latticeai/runtime/access_runtime.py +0 -14
  41. package/latticeai/runtime/bootstrap.py +10 -21
  42. package/latticeai/runtime/brain_runtime.py +35 -43
  43. package/latticeai/runtime/build_phases/__init__.py +21 -37
  44. package/latticeai/runtime/build_phases/features.py +99 -389
  45. package/latticeai/runtime/build_phases/foundation.py +82 -392
  46. package/latticeai/runtime/build_phases/web.py +91 -408
  47. package/latticeai/runtime/build_phases/worker_profile.py +244 -0
  48. package/latticeai/runtime/lifespan_runtime.py +7 -16
  49. package/latticeai/runtime/platform_services_runtime.py +1 -15
  50. package/latticeai/runtime/runtime_context.py +13 -130
  51. package/latticeai/runtime/security_runtime.py +10 -103
  52. package/latticeai/services/architecture_readiness.py +82 -43
  53. package/latticeai/services/model_runtime/__init__.py +2 -11
  54. package/latticeai/services/model_runtime/service.py +4 -28
  55. package/latticeai/services/p_reinforce.py +10 -261
  56. package/latticeai/services/product_readiness.py +31 -38
  57. package/latticeai/services/search_service.py +37 -795
  58. package/latticeai/services/tool_dispatch.py +30 -386
  59. package/latticeai/services/voice_capture.py +13 -107
  60. package/latticeai/tools/__init__.py +22 -49
  61. package/latticeai/tools/commands.py +0 -163
  62. package/latticeai/tools/computer.py +0 -39
  63. package/latticeai/tools/documents.py +1 -134
  64. package/latticeai/tools/filesystem.py +1 -247
  65. package/latticeai/tools/knowledge.py +1 -52
  66. package/latticeai/tools/local_files.py +0 -20
  67. package/latticeai/worker_app.py +75 -0
  68. package/package.json +2 -3
  69. package/requirements.txt +0 -5
  70. package/scripts/agent_eval.py +16 -28
  71. package/scripts/brain_quality_eval.py +20 -183
  72. package/scripts/bump_version.py +5 -5
  73. package/scripts/check_current_release_docs.mjs +7 -4
  74. package/scripts/check_openapi_drift.mjs +10 -0
  75. package/scripts/check_server_i18n.mjs +2 -18
  76. package/scripts/compose_openapi.py +377 -0
  77. package/scripts/export_openapi.py +32 -4
  78. package/scripts/gen_messages_catalog_fixture.py +302 -0
  79. package/scripts/gen_openapi_fragments.py +365 -0
  80. package/scripts/gen_redact_fixture.py +196 -0
  81. package/scripts/gen_worker_allowlist_fixture.py +115 -0
  82. package/scripts/generate_agent_parity_fixtures.py +33 -14
  83. package/scripts/openapi_route_families.json +2161 -0
  84. package/scripts/release_screen_claims.json +54 -0
  85. package/scripts/run_integration_tests.mjs +134 -29
  86. package/scripts/run_sidecar_e2e.mjs +104 -11
  87. package/scripts/wheel_smoke.py +32 -22
  88. package/src-tauri/Cargo.lock +375 -9
  89. package/src-tauri/Cargo.toml +1 -1
  90. package/src-tauri/src/backend.rs +151 -45
  91. package/src-tauri/src/main.rs +18 -18
  92. package/src-tauri/src/topology.rs +58 -88
  93. package/src-tauri/tauri.conf.json +1 -2
  94. package/static/app/asset-manifest.json +41 -41
  95. package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
  96. package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
  97. package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
  98. package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
  99. package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
  100. package/static/app/assets/Capture-BsTokYkk.js +1 -0
  101. package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
  102. package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
  103. package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
  104. package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
  105. package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
  106. package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
  107. package/static/app/assets/System-CMHSO9qM.js +1 -0
  108. package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
  109. package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
  110. package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
  111. package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
  112. package/static/app/assets/circle-check-DruOxB-4.js +1 -0
  113. package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
  114. package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
  115. package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
  116. package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
  117. package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
  118. package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
  119. package/static/app/assets/index-D9x-kSNy.css +2 -0
  120. package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
  121. package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
  122. package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
  123. package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
  124. package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
  125. package/static/app/assets/search-CT9aho2j.js +1 -0
  126. package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
  127. package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
  128. package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
  129. package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
  130. package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
  131. package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
  132. package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
  133. package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
  134. package/static/app/index.html +4 -4
  135. package/static/sw.js +1 -1
  136. package/lattice_brain/archive.py +0 -522
  137. package/lattice_brain/context.py +0 -325
  138. package/lattice_brain/conversations.py +0 -382
  139. package/lattice_brain/core.py +0 -82
  140. package/lattice_brain/graph/_kg_contract.py +0 -249
  141. package/lattice_brain/graph/curator.py +0 -676
  142. package/lattice_brain/graph/discovery.py +0 -595
  143. package/lattice_brain/graph/discovery_index/__init__.py +0 -35
  144. package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
  145. package/lattice_brain/graph/discovery_index/extract.py +0 -137
  146. package/lattice_brain/graph/discovery_index/scan.py +0 -411
  147. package/lattice_brain/graph/discovery_index/upsert.py +0 -495
  148. package/lattice_brain/graph/documents.py +0 -380
  149. package/lattice_brain/graph/fusion.py +0 -395
  150. package/lattice_brain/graph/identity.py +0 -175
  151. package/lattice_brain/graph/image_vectors.py +0 -230
  152. package/lattice_brain/graph/ingest.py +0 -829
  153. package/lattice_brain/graph/network.py +0 -205
  154. package/lattice_brain/graph/proactive.py +0 -724
  155. package/lattice_brain/graph/projection/__init__.py +0 -42
  156. package/lattice_brain/graph/projection/curation.py +0 -500
  157. package/lattice_brain/graph/projection/v2_schema.py +0 -518
  158. package/lattice_brain/graph/provenance.py +0 -524
  159. package/lattice_brain/graph/rerank.py +0 -163
  160. package/lattice_brain/graph/retrieval/__init__.py +0 -54
  161. package/lattice_brain/graph/retrieval/context.py +0 -197
  162. package/lattice_brain/graph/retrieval/graph_view.py +0 -319
  163. package/lattice_brain/graph/retrieval/hybrid.py +0 -488
  164. package/lattice_brain/graph/retrieval/maintenance.py +0 -121
  165. package/lattice_brain/graph/retrieval/signals.py +0 -95
  166. package/lattice_brain/graph/retrieval_docgen.py +0 -253
  167. package/lattice_brain/graph/retrieval_policy.py +0 -180
  168. package/lattice_brain/graph/retrieval_reads.py +0 -769
  169. package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
  170. package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
  171. package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
  172. package/lattice_brain/graph/retrieval_vector/search.py +0 -560
  173. package/lattice_brain/graph/retrieval_vector/status.py +0 -374
  174. package/lattice_brain/graph/schema.py +0 -792
  175. package/lattice_brain/graph/store.py +0 -268
  176. package/lattice_brain/graph/vector_index/__init__.py +0 -85
  177. package/lattice_brain/graph/vector_index/base.py +0 -167
  178. package/lattice_brain/graph/vector_index/brute_force.py +0 -110
  179. package/lattice_brain/graph/vector_index/hnsw.py +0 -290
  180. package/lattice_brain/graph/vector_index/jobs.py +0 -287
  181. package/lattice_brain/graph/vector_index/quantized.py +0 -148
  182. package/lattice_brain/graph/vector_index/selector.py +0 -161
  183. package/lattice_brain/graph/write_master.py +0 -308
  184. package/lattice_brain/ingestion/_contract.py +0 -90
  185. package/lattice_brain/ingestion/folder_scan.py +0 -57
  186. package/lattice_brain/ingestion/folders.py +0 -258
  187. package/lattice_brain/ingestion/jobs_api.py +0 -107
  188. package/lattice_brain/ingestion/routing.py +0 -295
  189. package/lattice_brain/ingestion_jobs.py +0 -380
  190. package/lattice_brain/memory.py +0 -75
  191. package/lattice_brain/portability/__init__.py +0 -90
  192. package/lattice_brain/portability/_contract.py +0 -42
  193. package/lattice_brain/portability/backups.py +0 -338
  194. package/lattice_brain/portability/bundles.py +0 -136
  195. package/lattice_brain/portability/constants.py +0 -93
  196. package/lattice_brain/portability/fsops.py +0 -138
  197. package/lattice_brain/portability/service.py +0 -41
  198. package/lattice_brain/portability/sharing.py +0 -710
  199. package/lattice_brain/quality.py +0 -543
  200. package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
  201. package/lattice_brain/runtime/agent_runtime.py +0 -859
  202. package/lattice_brain/runtime/contracts.py +0 -460
  203. package/lattice_brain/runtime/multi_agent.py +0 -942
  204. package/lattice_brain/runtime/statuses.py +0 -10
  205. package/lattice_brain/sealed_box.py +0 -240
  206. package/lattice_brain/self_model.py +0 -675
  207. package/lattice_brain/sensitivity.py +0 -94
  208. package/lattice_brain/storage/__init__.py +0 -22
  209. package/lattice_brain/storage/base.py +0 -100
  210. package/lattice_brain/storage/docker.py +0 -105
  211. package/lattice_brain/storage/factory.py +0 -31
  212. package/lattice_brain/storage/migration.py +0 -191
  213. package/lattice_brain/storage/postgres.py +0 -123
  214. package/lattice_brain/storage/sqlite.py +0 -143
  215. package/lattice_brain/synthesis.py +0 -824
  216. package/lattice_brain/workflow.py +0 -497
  217. package/latticeai/api/admin.py +0 -471
  218. package/latticeai/api/agent_registry.py +0 -105
  219. package/latticeai/api/agents.py +0 -228
  220. package/latticeai/api/auth.py +0 -383
  221. package/latticeai/api/automation_intelligence.py +0 -401
  222. package/latticeai/api/brain_intelligence.py +0 -199
  223. package/latticeai/api/browser.py +0 -493
  224. package/latticeai/api/change_proposals.py +0 -89
  225. package/latticeai/api/chat.py +0 -572
  226. package/latticeai/api/chat_agent_http.py +0 -892
  227. package/latticeai/api/chat_contracts.py +0 -73
  228. package/latticeai/api/chat_documents.py +0 -276
  229. package/latticeai/api/chat_helpers.py +0 -460
  230. package/latticeai/api/chat_history.py +0 -101
  231. package/latticeai/api/chat_hybrid.py +0 -113
  232. package/latticeai/api/chat_intents.py +0 -646
  233. package/latticeai/api/chat_stream.py +0 -216
  234. package/latticeai/api/chronicle.py +0 -63
  235. package/latticeai/api/command_center.py +0 -51
  236. package/latticeai/api/computer_use.py +0 -474
  237. package/latticeai/api/evidence_actions.py +0 -48
  238. package/latticeai/api/features.py +0 -70
  239. package/latticeai/api/funnel_metrics.py +0 -31
  240. package/latticeai/api/garden.py +0 -34
  241. package/latticeai/api/hooks.py +0 -165
  242. package/latticeai/api/index_jobs.py +0 -145
  243. package/latticeai/api/invitations.py +0 -100
  244. package/latticeai/api/knowledge_graph.py +0 -536
  245. package/latticeai/api/marketplace.py +0 -105
  246. package/latticeai/api/mcp.py +0 -482
  247. package/latticeai/api/memory.py +0 -270
  248. package/latticeai/api/network.py +0 -81
  249. package/latticeai/api/network_boundary.py +0 -225
  250. package/latticeai/api/permission_mode.py +0 -61
  251. package/latticeai/api/permissions.py +0 -436
  252. package/latticeai/api/plugins.py +0 -126
  253. package/latticeai/api/portability.py +0 -391
  254. package/latticeai/api/project_sessions.py +0 -114
  255. package/latticeai/api/realtime.py +0 -118
  256. package/latticeai/api/review_queue.py +0 -364
  257. package/latticeai/api/security_dashboard.py +0 -604
  258. package/latticeai/api/setup.py +0 -319
  259. package/latticeai/api/static_routes.py +0 -354
  260. package/latticeai/api/ui_redirects.py +0 -26
  261. package/latticeai/api/workflow_designer.py +0 -394
  262. package/latticeai/api/workspace.py +0 -856
  263. package/latticeai/api/workspace_scope.py +0 -125
  264. package/latticeai/core/agent/__init__.py +0 -93
  265. package/latticeai/core/agent/_contract.py +0 -79
  266. package/latticeai/core/agent/context.py +0 -57
  267. package/latticeai/core/agent/deps.py +0 -125
  268. package/latticeai/core/agent/execution.py +0 -622
  269. package/latticeai/core/agent/planning.py +0 -145
  270. package/latticeai/core/agent/recovery.py +0 -157
  271. package/latticeai/core/agent/runtime.py +0 -210
  272. package/latticeai/core/agent/verification.py +0 -231
  273. package/latticeai/core/agent_eval.py +0 -739
  274. package/latticeai/core/agent_helpers.py +0 -493
  275. package/latticeai/core/agent_profiles.py +0 -110
  276. package/latticeai/core/agent_prompts.py +0 -171
  277. package/latticeai/core/agent_registry.py +0 -232
  278. package/latticeai/core/agent_state.py +0 -41
  279. package/latticeai/core/agent_trace.py +0 -104
  280. package/latticeai/core/artifact_ledger.py +0 -109
  281. package/latticeai/core/audit.py +0 -260
  282. package/latticeai/core/builtin_hooks.py +0 -105
  283. package/latticeai/core/context_builder.py +0 -394
  284. package/latticeai/core/document_generator.py +0 -103
  285. package/latticeai/core/enterprise.py +0 -154
  286. package/latticeai/core/enterprise_admin.py +0 -158
  287. package/latticeai/core/file_generation/__init__.py +0 -115
  288. package/latticeai/core/file_generation/bundles.py +0 -76
  289. package/latticeai/core/file_generation/extraction.py +0 -154
  290. package/latticeai/core/file_generation/inference.py +0 -235
  291. package/latticeai/core/file_generation/orchestration.py +0 -152
  292. package/latticeai/core/file_generation/prompting.py +0 -117
  293. package/latticeai/core/file_generation/repair.py +0 -114
  294. package/latticeai/core/file_generation/sanitize.py +0 -61
  295. package/latticeai/core/file_generation/validation.py +0 -201
  296. package/latticeai/core/invitations.py +0 -132
  297. package/latticeai/core/legacy_compatibility.py +0 -243
  298. package/latticeai/core/logging_safety.py +0 -46
  299. package/latticeai/core/marketplace.py +0 -293
  300. package/latticeai/core/mcp_catalog.py +0 -452
  301. package/latticeai/core/mcp_registry.py +0 -506
  302. package/latticeai/core/network_boundary.py +0 -168
  303. package/latticeai/core/oidc.py +0 -208
  304. package/latticeai/core/plugins.py +0 -432
  305. package/latticeai/core/product_hardening.py +0 -218
  306. package/latticeai/core/project_sessions.py +0 -337
  307. package/latticeai/core/realtime.py +0 -238
  308. package/latticeai/core/run_explain.py +0 -426
  309. package/latticeai/core/run_store.py +0 -252
  310. package/latticeai/core/timezones.py +0 -80
  311. package/latticeai/core/workspace_computer_memory.py +0 -84
  312. package/latticeai/core/workspace_graph_trace.py +0 -155
  313. package/latticeai/core/workspace_indexing.py +0 -102
  314. package/latticeai/core/workspace_memory.py +0 -77
  315. package/latticeai/core/workspace_onboarding.py +0 -104
  316. package/latticeai/core/workspace_os.py +0 -978
  317. package/latticeai/core/workspace_os_constants.py +0 -126
  318. package/latticeai/core/workspace_os_state.py +0 -180
  319. package/latticeai/core/workspace_os_utils.py +0 -103
  320. package/latticeai/core/workspace_permissions.py +0 -101
  321. package/latticeai/core/workspace_plugins.py +0 -97
  322. package/latticeai/core/workspace_relationships.py +0 -99
  323. package/latticeai/core/workspace_reorganization.py +0 -335
  324. package/latticeai/core/workspace_review_items.py +0 -112
  325. package/latticeai/core/workspace_runs.py +0 -726
  326. package/latticeai/core/workspace_skills.py +0 -109
  327. package/latticeai/core/workspace_snapshots.py +0 -198
  328. package/latticeai/core/workspace_timeline.py +0 -110
  329. package/latticeai/integrations/__init__.py +0 -0
  330. package/latticeai/integrations/telegram_bot/__init__.py +0 -123
  331. package/latticeai/integrations/telegram_bot/__main__.py +0 -17
  332. package/latticeai/integrations/telegram_bot/config.py +0 -86
  333. package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
  334. package/latticeai/integrations/telegram_bot/flows.py +0 -478
  335. package/latticeai/integrations/telegram_bot/helpers.py +0 -322
  336. package/latticeai/integrations/telegram_bot/screens.py +0 -394
  337. package/latticeai/runtime/audit_runtime.py +0 -76
  338. package/latticeai/runtime/automation_runtime.py +0 -81
  339. package/latticeai/runtime/chat_wiring.py +0 -141
  340. package/latticeai/runtime/context_runtime.py +0 -66
  341. package/latticeai/runtime/feature_toggle_wiring.py +0 -157
  342. package/latticeai/runtime/history_runtime.py +0 -163
  343. package/latticeai/runtime/history_writer.py +0 -138
  344. package/latticeai/runtime/hooks_runtime.py +0 -77
  345. package/latticeai/runtime/model_wiring.py +0 -68
  346. package/latticeai/runtime/namespace_runtime.py +0 -163
  347. package/latticeai/runtime/network_boundary_wiring.py +0 -117
  348. package/latticeai/runtime/network_config_runtime.py +0 -56
  349. package/latticeai/runtime/permission_mode_wiring.py +0 -112
  350. package/latticeai/runtime/persistence_runtime.py +0 -159
  351. package/latticeai/runtime/platform_runtime_wiring.py +0 -89
  352. package/latticeai/runtime/review_wiring.py +0 -42
  353. package/latticeai/runtime/router_registration.py +0 -693
  354. package/latticeai/runtime/service_singletons.py +0 -55
  355. package/latticeai/runtime/sso_config_runtime.py +0 -128
  356. package/latticeai/runtime/user_key_runtime.py +0 -106
  357. package/latticeai/runtime/web_runtime.py +0 -92
  358. package/latticeai/server_app.py +0 -51
  359. package/latticeai/services/app_context.py +0 -130
  360. package/latticeai/services/automation_execution.py +0 -266
  361. package/latticeai/services/automation_intelligence.py +0 -614
  362. package/latticeai/services/brain_automation.py +0 -191
  363. package/latticeai/services/brain_intelligence/__init__.py +0 -58
  364. package/latticeai/services/brain_intelligence/_contract.py +0 -71
  365. package/latticeai/services/brain_intelligence/consistency.py +0 -193
  366. package/latticeai/services/brain_intelligence/constants.py +0 -47
  367. package/latticeai/services/brain_intelligence/digest.py +0 -258
  368. package/latticeai/services/brain_intelligence/health.py +0 -331
  369. package/latticeai/services/brain_intelligence/proposals.py +0 -259
  370. package/latticeai/services/brain_intelligence/sampling.py +0 -84
  371. package/latticeai/services/brain_intelligence/service.py +0 -48
  372. package/latticeai/services/change_proposals.py +0 -471
  373. package/latticeai/services/chat_service.py +0 -243
  374. package/latticeai/services/chronicle.py +0 -555
  375. package/latticeai/services/cloud_egress_audit.py +0 -85
  376. package/latticeai/services/cloud_extraction.py +0 -129
  377. package/latticeai/services/cloud_streaming.py +0 -268
  378. package/latticeai/services/cloud_token_guard.py +0 -84
  379. package/latticeai/services/command_center.py +0 -548
  380. package/latticeai/services/evidence_actions.py +0 -258
  381. package/latticeai/services/feature_toggles.py +0 -502
  382. package/latticeai/services/folder_watch.py +0 -520
  383. package/latticeai/services/funnel_metrics.py +0 -307
  384. package/latticeai/services/hybrid_chat.py +0 -316
  385. package/latticeai/services/hybrid_context.py +0 -228
  386. package/latticeai/services/hybrid_policy.py +0 -129
  387. package/latticeai/services/interop_bridges.py +0 -978
  388. package/latticeai/services/local_knowledge.py +0 -465
  389. package/latticeai/services/memory_service/__init__.py +0 -52
  390. package/latticeai/services/memory_service/_contract.py +0 -100
  391. package/latticeai/services/memory_service/brief.py +0 -431
  392. package/latticeai/services/memory_service/constants.py +0 -57
  393. package/latticeai/services/memory_service/maintenance.py +0 -138
  394. package/latticeai/services/memory_service/manager.py +0 -186
  395. package/latticeai/services/memory_service/proof.py +0 -136
  396. package/latticeai/services/memory_service/recall.py +0 -225
  397. package/latticeai/services/memory_service/service.py +0 -48
  398. package/latticeai/services/memory_service/stores.py +0 -110
  399. package/latticeai/services/mode_store.py +0 -132
  400. package/latticeai/services/model_recommendation.py +0 -224
  401. package/latticeai/services/model_runtime/cloud.py +0 -87
  402. package/latticeai/services/network_boundary_service.py +0 -117
  403. package/latticeai/services/obsidian_bridge.py +0 -609
  404. package/latticeai/services/openai_compatible_adapter.py +0 -101
  405. package/latticeai/services/permission_mode_service.py +0 -122
  406. package/latticeai/services/platform_runtime.py +0 -366
  407. package/latticeai/services/review_queue.py +0 -380
  408. package/latticeai/services/router_context.py +0 -59
  409. package/latticeai/services/run_executor.py +0 -387
  410. package/latticeai/services/self_model_service.py +0 -171
  411. package/latticeai/services/setup_detection.py +0 -147
  412. package/latticeai/services/triggers.py +0 -378
  413. package/latticeai/services/upload_service.py +0 -172
  414. package/latticeai/services/workspace_service.py +0 -165
  415. package/latticeai/setup/__init__.py +0 -25
  416. package/latticeai/setup/auto_setup.py +0 -846
  417. package/latticeai/setup/demo_corpus.py +0 -98
  418. package/latticeai/setup/wizard/__init__.py +0 -126
  419. package/latticeai/setup/wizard/catalog.py +0 -175
  420. package/latticeai/setup/wizard/detect.py +0 -302
  421. package/latticeai/setup/wizard/install.py +0 -348
  422. package/latticeai/setup/wizard/paths.py +0 -165
  423. package/latticeai/setup/wizard/plans.py +0 -74
  424. package/latticeai/setup/wizard/recommend.py +0 -320
  425. package/scripts/bench_agent_smoke.py +0 -409
  426. package/scripts/bench_models.py +0 -540
  427. package/scripts/bench_vector_index.py +0 -295
  428. package/scripts/funnel_soft_gate.py +0 -192
  429. package/scripts/generate_agent_loop_fixtures.py +0 -994
  430. package/scripts/generate_rust_parity_fixtures.py +0 -908
  431. package/scripts/migrate_brain_storage.py +0 -57
  432. package/scripts/parity_fixture_corpus_context.py +0 -162
  433. package/scripts/parity_fixture_corpus_docgen.py +0 -341
  434. package/scripts/profile_kg.py +0 -355
  435. package/server.py +0 -30
  436. package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
  437. package/static/app/assets/Capture-DiQ219jW.js +0 -1
  438. package/static/app/assets/System-CieofHQa.js +0 -1
  439. package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
  440. package/static/app/assets/circle-check-qqLug9nU.js +0 -1
  441. package/static/app/assets/index-DxmOfNRi.css +0 -2
  442. package/static/app/assets/primitives-SNp0LRJz.js +0 -1
  443. package/static/app/assets/search-BcHqkjoy.js +0 -1
@@ -1,395 +0,0 @@
1
- """Query-class aware retrieval fusion weights (backlog #5, review §7.2 E).
2
-
3
- Hybrid retrieval fuses keyword/lexical, vector, and graph channels. A single
4
- static weight set treats "이 함수 왜 죽어?" and "어제 회의에서 뭐 결정했지?"
5
- identically, which is measurably wrong: code questions live on exact
6
- identifiers (lexical), person questions lean on relationship edges (graph),
7
- recency questions need fresher lexical hits over semantic neighbors.
8
-
9
- This module is deliberately tiny and deterministic:
10
-
11
- * :func:`classify_query` — pure ko/en heuristics → ``fact | code | person |
12
- recency`` (``fact`` is the fallback class and its weights are identical to
13
- the historical defaults, so fact-class behavior is byte-compatible).
14
- * :data:`DEFAULT_FUSION_WEIGHTS` — the per-class weight table. ``keyword`` /
15
- ``vector`` / ``graph`` feed the three-channel service fusion
16
- (``SearchService.hybrid_search``); ``alpha`` is the vector share for the
17
- two-channel graph-layer fusion (``KnowledgeGraphStore.hybrid_search``).
18
- * :func:`fusion_profile` — classify + resolve weights, honoring the
19
- ``LATTICEAI_FUSION_WEIGHTS`` env override (JSON, partial per-class merges,
20
- e.g. ``{"code": {"alpha": 0.2}}``).
21
-
22
- Never uses ``or`` on scores (score 0 is falsy but valid) and never raises:
23
- a malformed override is ignored, an unknown class falls back to ``fact``.
24
- """
25
-
26
- from __future__ import annotations
27
-
28
- import json
29
- import os
30
- import re
31
- from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
32
-
33
- from ..gates import FeatureGate
34
- from ..quiet import quiet
35
-
36
- QUERY_CLASSES = ("fact", "code", "person", "recency")
37
-
38
- FUSION_WEIGHTS_ENV = "LATTICEAI_FUSION_WEIGHTS"
39
-
40
- # ── fusion strategy (v11.1.0) ────────────────────────────────────────────────
41
- # Linear alpha fusion needs the two channels' scores to be comparable. They
42
- # are not: the lexical channel scores 1/rank while the vector channel scores a
43
- # max-normalized cosine, so a query where every cosine lands near 0.9 flattens
44
- # the vector channel to noise. Reciprocal Rank Fusion ignores score magnitudes
45
- # entirely and fuses *positions*, which is exactly the property that survives
46
- # incomparable scales.
47
- #
48
- # It is opt-in, per query class, and off everywhere by default: alpha fusion
49
- # is what every existing ranking assertion in this repo describes, and a
50
- # ranking change dressed up as a bug fix is how retrieval quality regressions
51
- # get shipped. Turn it on with LATTICEAI_FUSION_STRATEGY=rrf (all classes) or
52
- # a JSON object like {"code": "rrf"} (per class).
53
- FUSION_STRATEGY_ENV = "LATTICEAI_FUSION_STRATEGY"
54
- #: The one-switch form of the per-class table above, for the settings panel:
55
- #: "combine by rank instead of score", everywhere. It is applied as the *base*
56
- #: of the table, so the per-class ``LATTICEAI_FUSION_STRATEGY`` config — the
57
- #: more specific statement — still wins over it, and an install that touched
58
- #: neither is byte-identical to what it was.
59
- FUSION_RRF_ENV = "LATTICEAI_FUSION_RRF"
60
- FUSION_STRATEGIES = ("alpha", "rrf")
61
- DEFAULT_FUSION_STRATEGY: Dict[str, str] = dict.fromkeys(QUERY_CLASSES, "alpha")
62
- FUSION_RRF_GATE = FeatureGate(
63
- FUSION_RRF_ENV,
64
- default=False,
65
- name="fusion_rrf",
66
- detail="Search channels are combined by rank rather than by score.",
67
- )
68
- #: The smoothing constant from the original RRF paper (Cormack et al., 2009).
69
- #: Larger k flattens the curve, so rank 1 wins by less.
70
- DEFAULT_RRF_K = 60
71
-
72
- # Per-class fusion weights. "fact" mirrors the pre-fusion-gate defaults
73
- # (SearchService DEFAULT_HYBRID_WEIGHTS + graph-layer alpha=0.6) so the
74
- # fallback class introduces zero behavioral drift.
75
- DEFAULT_FUSION_WEIGHTS: Dict[str, Dict[str, float]] = {
76
- "fact": {"keyword": 0.35, "vector": 0.40, "graph": 0.25, "alpha": 0.60},
77
- # Code recall lives on exact identifiers/tokens → lexical dominates.
78
- "code": {"keyword": 0.55, "vector": 0.25, "graph": 0.20, "alpha": 0.35},
79
- # Person questions are relationship questions → graph channel leads.
80
- "person": {"keyword": 0.30, "vector": 0.30, "graph": 0.40, "alpha": 0.45},
81
- # Recency questions want fresh literal hits over semantic neighbors.
82
- "recency": {"keyword": 0.45, "vector": 0.35, "graph": 0.20, "alpha": 0.50},
83
- }
84
-
85
- # ── query-class heuristics (pure, ko/en) ─────────────────────────────────────
86
-
87
- _CODE_FENCE_RE = re.compile(r"```|`[^`\n]+`")
88
- _CODE_IDENT_RE = re.compile(
89
- # ``\b`` fails between an identifier and an attached Korean particle
90
- # ("ingest_folder가"), so use explicit ASCII-boundary lookbehinds instead.
91
- r"[A-Za-z_][A-Za-z0-9_]*\(\)" # call syntax foo()
92
- r"|(?<![A-Za-z0-9_])[a-z0-9]+_[a-z0-9_]+" # snake_case
93
- r"|(?<![A-Za-z0-9_])[a-z]+[A-Z][A-Za-z0-9]*" # camelCase
94
- r"|(?<![A-Za-z0-9_])[\w-]+\.(?:py|js|jsx|ts|tsx|json|yaml|yml|css|html|sql|sh|go|rs|java|rb)(?![A-Za-z0-9])"
95
- )
96
- _CODE_WORD_RE = re.compile(
97
- r"\b(?:def|class|import|function|traceback|exception|stack\s*trace|"
98
- r"bug|error|null|undefined|api|sql|regex)\b"
99
- r"|코드|함수|버그|에러|오류|스택|컴파일|빌드\s*실패|구현",
100
- re.IGNORECASE,
101
- )
102
- _PERSON_RE = re.compile(
103
- r"\b(?:who|whom|whose)\b|누구|어떤\s*사람|담당자|팀원|동료|만난\s*사람"
104
- r"|[\w.+-]+@[\w-]+\.[\w.]+" # an email address names a person
105
- r"|[가-힣]{2,4}\s*(?:님|씨)(?:\s|$|[을를이가은는의??.!,])",
106
- re.IGNORECASE,
107
- )
108
- _RECENCY_RE = re.compile(
109
- r"최근|어제|오늘|그저께|방금|아까|지난\s*주|지난주|지난\s*달|지난달"
110
- r"|이번\s*주|이번주|이번\s*달|이번달"
111
- r"|\b(?:recent|recently|yesterday|today|latest|last\s+(?:week|month|night|meeting))\b",
112
- re.IGNORECASE,
113
- )
114
-
115
-
116
- def classify_query(query: Any) -> str:
117
- """Classify a retrieval query into fact / code / person / recency.
118
-
119
- Precedence: code (strongest structural signal) → recency (explicit time
120
- words) → person → fact. Deterministic, never raises, empty → fact.
121
- """
122
- text = str(query or "").strip()
123
- if not text:
124
- return "fact"
125
- if _CODE_FENCE_RE.search(text) or _CODE_IDENT_RE.search(text) or _CODE_WORD_RE.search(text):
126
- return "code"
127
- if _RECENCY_RE.search(text):
128
- return "recency"
129
- if _PERSON_RE.search(text):
130
- return "person"
131
- return "fact"
132
-
133
-
134
- def _env_overrides() -> Dict[str, Dict[str, float]]:
135
- """Parse LATTICEAI_FUSION_WEIGHTS (JSON) → per-class partial overrides."""
136
- raw = os.getenv(FUSION_WEIGHTS_ENV, "").strip()
137
- if not raw:
138
- return {}
139
- try:
140
- payload = json.loads(raw)
141
- except (ValueError, TypeError):
142
- return {}
143
- if not isinstance(payload, dict):
144
- return {}
145
- overrides: Dict[str, Dict[str, float]] = {}
146
- for cls, table in payload.items():
147
- if cls not in DEFAULT_FUSION_WEIGHTS or not isinstance(table, dict):
148
- continue
149
- cleaned: Dict[str, float] = {}
150
- for key, value in table.items():
151
- if key not in ("keyword", "vector", "graph", "alpha"):
152
- continue
153
- try:
154
- cleaned[key] = max(0.0, min(1.0, float(value)))
155
- except (TypeError, ValueError):
156
- quiet()
157
- continue
158
- if cleaned:
159
- overrides[str(cls)] = cleaned
160
- return overrides
161
-
162
-
163
- def fusion_weight_table(
164
- overrides: Optional[Mapping[str, Mapping[str, float]]] = None,
165
- ) -> Dict[str, Dict[str, float]]:
166
- """Full per-class table: defaults ← env override ← caller override."""
167
- table = {cls: dict(weights) for cls, weights in DEFAULT_FUSION_WEIGHTS.items()}
168
- for source in (_env_overrides(), overrides or {}):
169
- for cls, partial in source.items():
170
- if cls in table and isinstance(partial, Mapping):
171
- for key, value in partial.items():
172
- if key in table[cls]:
173
- try:
174
- table[cls][key] = max(0.0, min(1.0, float(value)))
175
- except (TypeError, ValueError):
176
- quiet()
177
- continue
178
- return table
179
-
180
-
181
- def _env_strategy_overrides() -> Dict[str, str]:
182
- """Parse ``LATTICEAI_FUSION_STRATEGY`` → per-class strategy overrides.
183
-
184
- Two accepted forms: a bare strategy name (``rrf``) applies to every class,
185
- a JSON object (``{"code": "rrf"}``) applies per class. Anything else —
186
- a typo, malformed JSON, an unknown strategy — is ignored rather than
187
- raising, so a bad env var degrades to the default fusion instead of
188
- taking every search down.
189
- """
190
- raw = os.getenv(FUSION_STRATEGY_ENV, "").strip()
191
- if not raw:
192
- return {}
193
- if raw.lower() in FUSION_STRATEGIES:
194
- return dict.fromkeys(QUERY_CLASSES, raw.lower())
195
- try:
196
- payload = json.loads(raw)
197
- except (ValueError, TypeError):
198
- return {}
199
- if not isinstance(payload, dict):
200
- return {}
201
- return {
202
- str(cls): str(value).lower()
203
- for cls, value in payload.items()
204
- if cls in DEFAULT_FUSION_STRATEGY and str(value).lower() in FUSION_STRATEGIES
205
- }
206
-
207
-
208
- def fusion_strategy_table(
209
- overrides: Optional[Mapping[str, str]] = None,
210
- ) -> Dict[str, str]:
211
- """Full per-class table: defaults ← simple switch ← env override ← caller.
212
-
213
- The simple switch (:data:`FUSION_RRF_GATE`, the settings panel's "combine by
214
- rank") sits *under* the per-class env config on purpose: a person who wrote
215
- ``{"code": "alpha"}`` said something specific, and a single global flag must
216
- not overrule it.
217
- """
218
- table = dict(DEFAULT_FUSION_STRATEGY)
219
- if FUSION_RRF_GATE.enabled():
220
- table = dict.fromkeys(QUERY_CLASSES, "rrf")
221
- for source in (_env_strategy_overrides(), overrides or {}):
222
- for cls, value in source.items():
223
- if cls in table and str(value).lower() in FUSION_STRATEGIES:
224
- table[cls] = str(value).lower()
225
- return table
226
-
227
-
228
- def rrf_score(rank: int, *, k: int = DEFAULT_RRF_K) -> float:
229
- """One channel's contribution for an item it ranked at ``rank`` (1-based)."""
230
- return 1.0 / (int(k) + max(1, int(rank)))
231
-
232
-
233
- def rrf_fuse(
234
- rankings: Mapping[str, Sequence[str]],
235
- *,
236
- k: int = DEFAULT_RRF_K,
237
- weights: Optional[Mapping[str, float]] = None,
238
- ) -> Dict[str, float]:
239
- """Reciprocal Rank Fusion over per-channel id rankings.
240
-
241
- ``rankings`` maps a channel name to its ids in rank order (best first).
242
- An id absent from a channel simply contributes nothing from it — RRF has
243
- no notion of a "missing score" to guess at, which is the other reason it
244
- survives channels that fail independently.
245
-
246
- ``weights`` optionally scales a channel's contribution (a channel missing
247
- from the mapping weighs 1.0). Deterministic and pure.
248
- """
249
- fused: Dict[str, float] = {}
250
- for channel, ordered in rankings.items():
251
- weight = 1.0 if weights is None else float(weights.get(channel, 1.0))
252
- for rank, item_id in enumerate(ordered, start=1):
253
- fused[item_id] = fused.get(item_id, 0.0) + weight * rrf_score(rank, k=k)
254
- return fused
255
-
256
-
257
- # ── graph traversal candidate expansion (v11.1.0) ────────────────────────────
258
- # Both retrieval channels can only return what they matched. A note that
259
- # never says "postgres" but is one CONTAINS edge away from the one that does
260
- # is unreachable by either — and it is often the answer. Expansion walks one
261
- # hop out from the top hits and offers those neighbours as candidates.
262
- #
263
- # It is capped, off by default, and reports its own counts, because the
264
- # failure mode is dilution: an unbounded expansion turns a precise answer into
265
- # a tour of the graph.
266
- GRAPH_EXPANSION_ENV = "LATTICEAI_GRAPH_EXPANSION"
267
- #: Resolved when asked so the settings panel can move it without a restart; the
268
- #: env parsing is the same set of words the hand-written check used.
269
- GRAPH_EXPANSION_GATE = FeatureGate(
270
- GRAPH_EXPANSION_ENV,
271
- default=False,
272
- name="graph_expansion",
273
- detail="Memories one link away from a hit are offered as extra candidates.",
274
- )
275
- DEFAULT_EXPANSION_SEEDS = 3
276
- DEFAULT_EXPANSION_CAP = 5
277
- #: Expanded candidates inherit a damped share of their seed's score: they are
278
- #: related to a hit, they are not themselves a hit.
279
- EXPANSION_DECAY = 0.5
280
-
281
-
282
- def graph_expansion_enabled() -> bool:
283
- """True when the expansion gate opts in (default: off)."""
284
- return GRAPH_EXPANSION_GATE.enabled()
285
-
286
-
287
- def expand_with_neighbors(
288
- seeds: Sequence[Tuple[str, float]],
289
- neighbors_fn: Callable[[str], Any],
290
- *,
291
- exclude: Optional[Sequence[str]] = None,
292
- cap: int = DEFAULT_EXPANSION_CAP,
293
- ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
294
- """One-hop neighbours of ``seeds`` as extra candidates + an honest report.
295
-
296
- ``seeds`` is ``[(node_id, score)]`` best first; ``neighbors_fn`` is the
297
- store's ``neighbors``. Returns ``(candidates, report)`` where each
298
- candidate carries its originating seed and a decayed score, and the report
299
- states how many seeds were walked, how many candidates were kept, and
300
- whether the cap bit. A neighbour lookup that fails is skipped and counted,
301
- never raised: expansion is an optimization, not a dependency.
302
- """
303
- known = {str(item) for item in (exclude or ())}
304
- cap = max(0, int(cap))
305
- candidates: List[Dict[str, Any]] = []
306
- failures = 0
307
- for seed_id, seed_score in seeds:
308
- if len(candidates) >= cap:
309
- break
310
- try:
311
- payload = neighbors_fn(seed_id) or {}
312
- except Exception: # noqa: BLE001 — a bad seed must not fail the search
313
- failures += 1
314
- continue
315
- for node in payload.get("neighbors") or []:
316
- node_id = str(node.get("id") or "")
317
- if not node_id or node_id in known:
318
- continue
319
- known.add(node_id)
320
- candidates.append(
321
- {
322
- "node": node,
323
- "seed": str(seed_id),
324
- "score": round(float(seed_score) * EXPANSION_DECAY, 6),
325
- }
326
- )
327
- if len(candidates) >= cap:
328
- break
329
- report = {
330
- "enabled": True,
331
- "seeds": len(seeds),
332
- "added": len(candidates),
333
- "cap": cap,
334
- "truncated": len(candidates) >= cap,
335
- "failed_seeds": failures,
336
- }
337
- return candidates, report
338
-
339
-
340
- def fusion_profile(
341
- query: Any,
342
- *,
343
- overrides: Optional[Mapping[str, Mapping[str, float]]] = None,
344
- strategies: Optional[Mapping[str, str]] = None,
345
- ) -> Dict[str, Any]:
346
- """Classify ``query`` and return its resolved fusion weights.
347
-
348
- Returns ``{"query_class": str, "weights": {keyword, vector, graph},
349
- "alpha": float, "strategy": "alpha"|"rrf"}``. ``weights`` feeds the
350
- three-channel service fusion; ``alpha`` feeds the two-channel graph-layer
351
- fusion; ``strategy`` selects *how* those channels combine and is
352
- ``"alpha"`` for every class unless configured otherwise.
353
- """
354
- query_class = classify_query(query)
355
- table = fusion_weight_table(overrides)
356
- resolved = table.get(query_class)
357
- if resolved is None: # defensive — classify_query only emits known classes
358
- query_class = "fact"
359
- resolved = table["fact"]
360
- return {
361
- "query_class": query_class,
362
- "weights": {
363
- "keyword": resolved["keyword"],
364
- "vector": resolved["vector"],
365
- "graph": resolved["graph"],
366
- },
367
- "alpha": resolved["alpha"],
368
- "strategy": fusion_strategy_table(strategies)[query_class],
369
- }
370
-
371
-
372
- __all__ = [
373
- "DEFAULT_EXPANSION_CAP",
374
- "DEFAULT_EXPANSION_SEEDS",
375
- "DEFAULT_FUSION_STRATEGY",
376
- "DEFAULT_FUSION_WEIGHTS",
377
- "DEFAULT_RRF_K",
378
- "EXPANSION_DECAY",
379
- "FUSION_RRF_ENV",
380
- "FUSION_RRF_GATE",
381
- "FUSION_STRATEGIES",
382
- "FUSION_STRATEGY_ENV",
383
- "FUSION_WEIGHTS_ENV",
384
- "GRAPH_EXPANSION_ENV",
385
- "GRAPH_EXPANSION_GATE",
386
- "QUERY_CLASSES",
387
- "classify_query",
388
- "expand_with_neighbors",
389
- "fusion_profile",
390
- "fusion_strategy_table",
391
- "fusion_weight_table",
392
- "graph_expansion_enabled",
393
- "rrf_fuse",
394
- "rrf_score",
395
- ]
@@ -1,175 +0,0 @@
1
- """Device identity — the sovereignty primitive.
2
-
3
- Every Lattice installation owns an Ed25519 keypair. Exports are signed by
4
- it, peers pair against its public key, and imported knowledge records which
5
- device it came from. The private key never leaves the machine: it lives in
6
- the OS keyring when one is available, otherwise in a 0600 file under the
7
- data directory (the storage backend is reported honestly).
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import base64
13
- import hashlib
14
- import json
15
- import logging
16
- import os
17
- from pathlib import Path
18
- from typing import Any, Dict, Optional
19
-
20
- from cryptography.hazmat.primitives import serialization
21
- from cryptography.hazmat.primitives.asymmetric.ed25519 import (
22
- Ed25519PrivateKey,
23
- Ed25519PublicKey,
24
- )
25
-
26
- _KEYRING_SERVICE = "lattice-ai-device-identity"
27
- _KEYRING_ENTRY = "ed25519-private-key"
28
-
29
-
30
- def _b64(data: bytes) -> str:
31
- return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
32
-
33
-
34
- def _unb64(text: str) -> bytes:
35
- padded = text + "=" * (-len(text) % 4)
36
- return base64.urlsafe_b64decode(padded.encode("ascii"))
37
-
38
-
39
- def _keyring_opt_in() -> bool:
40
- """Keyring storage is opt-in (LATTICEAI_DEVICE_KEY_KEYRING=1).
41
-
42
- OS keychain access can block or prompt during startup/tests; the default
43
- is a 0600 file under the data dir, and ``describe()`` reports which
44
- backend holds the key — no silent security theater either way.
45
- """
46
- return os.getenv("LATTICEAI_DEVICE_KEY_KEYRING", "").strip() in {"1", "true", "yes"}
47
-
48
-
49
- class DeviceIdentity:
50
- """Loads-or-creates the installation's Ed25519 keypair."""
51
-
52
- def __init__(self, data_dir: Path, *, use_keyring: Optional[bool] = None):
53
- if use_keyring is None:
54
- use_keyring = _keyring_opt_in()
55
- self._data_dir = Path(data_dir)
56
- self._key_file = self._data_dir / "device_identity.key"
57
- self._private: Ed25519PrivateKey
58
- self.storage: str # "keyring" | "file"
59
- self._load_or_create(use_keyring)
60
-
61
- # ── key material ───────────────────────────────────────────────────────
62
- def _load_or_create(self, use_keyring: bool) -> None:
63
- raw: Optional[bytes] = None
64
- backend = "file"
65
- if use_keyring:
66
- try:
67
- import keyring
68
-
69
- stored = keyring.get_password(_KEYRING_SERVICE, _KEYRING_ENTRY)
70
- if stored:
71
- raw = _unb64(stored)
72
- backend = "keyring"
73
- except Exception as exc:
74
- logging.debug("device identity: keyring unavailable (%s)", exc)
75
- if raw is None and self._key_file.exists():
76
- raw = _unb64(self._key_file.read_text().strip())
77
- backend = "file"
78
- if raw is None:
79
- key = Ed25519PrivateKey.generate()
80
- raw = key.private_bytes(
81
- serialization.Encoding.Raw,
82
- serialization.PrivateFormat.Raw,
83
- serialization.NoEncryption(),
84
- )
85
- backend = self._persist_new(raw, use_keyring)
86
- self._private = Ed25519PrivateKey.from_private_bytes(raw)
87
- self.storage = backend
88
-
89
- def _persist_new(self, raw: bytes, use_keyring: bool) -> str:
90
- if use_keyring:
91
- try:
92
- import keyring
93
-
94
- keyring.set_password(_KEYRING_SERVICE, _KEYRING_ENTRY, _b64(raw))
95
- return "keyring"
96
- except Exception as exc:
97
- logging.debug("device identity: keyring store failed (%s); using file", exc)
98
- self._data_dir.mkdir(parents=True, exist_ok=True)
99
- self._key_file.write_text(_b64(raw))
100
- os.chmod(self._key_file, 0o600)
101
- return "file"
102
-
103
- # ── public surface ─────────────────────────────────────────────────────
104
- @property
105
- def public_key_b64(self) -> str:
106
- return _b64(
107
- self._private.public_key().public_bytes(
108
- serialization.Encoding.Raw, serialization.PublicFormat.Raw
109
- )
110
- )
111
-
112
- @property
113
- def fingerprint(self) -> str:
114
- """Short human-comparable id: sha256 of the raw public key."""
115
- raw = self._private.public_key().public_bytes(
116
- serialization.Encoding.Raw, serialization.PublicFormat.Raw
117
- )
118
- digest = hashlib.sha256(raw).hexdigest()
119
- return ":".join(digest[i : i + 4] for i in range(0, 16, 4))
120
-
121
- def describe(self) -> Dict[str, Any]:
122
- return {
123
- "fingerprint": self.fingerprint,
124
- "public_key": self.public_key_b64,
125
- "algorithm": "ed25519",
126
- "storage": self.storage,
127
- }
128
-
129
- # ── signing ────────────────────────────────────────────────────────────
130
- def sign(self, payload: bytes) -> str:
131
- return _b64(self._private.sign(payload))
132
-
133
- def sign_manifest(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
134
- """Detached signature over the canonical JSON of a manifest."""
135
- canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False).encode("utf-8")
136
- return {
137
- "algorithm": "ed25519",
138
- "public_key": self.public_key_b64,
139
- "fingerprint": self.fingerprint,
140
- "signature": self.sign(canonical),
141
- }
142
-
143
-
144
- def fingerprint_of(public_key_b64: str) -> str:
145
- """Human-comparable fingerprint of an Ed25519 public key.
146
-
147
- Raises ValueError when the input is not a valid key — the pairing flow
148
- uses this as its validation gate.
149
- """
150
- raw = _unb64(public_key_b64)
151
- Ed25519PublicKey.from_public_bytes(raw) # validates; raises on garbage
152
- digest = hashlib.sha256(raw).hexdigest()
153
- return ":".join(digest[i : i + 4] for i in range(0, 16, 4))
154
-
155
-
156
- def verify_signature(public_key_b64: str, payload: bytes, signature_b64: str) -> bool:
157
- """True iff ``signature`` is valid for ``payload`` under the given key."""
158
- try:
159
- key = Ed25519PublicKey.from_public_bytes(_unb64(public_key_b64))
160
- key.verify(_unb64(signature_b64), payload)
161
- return True
162
- except Exception:
163
- return False
164
-
165
-
166
- def verify_manifest(manifest: Dict[str, Any], signature_block: Dict[str, Any]) -> bool:
167
- canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False).encode("utf-8")
168
- return verify_signature(
169
- str(signature_block.get("public_key") or ""),
170
- canonical,
171
- str(signature_block.get("signature") or ""),
172
- )
173
-
174
-
175
- __all__ = ["DeviceIdentity", "verify_signature", "verify_manifest"]