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,319 +0,0 @@
1
- """Setup wizard and OS permission routes."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any, Dict, List, Optional
6
-
7
- from fastapi import APIRouter, HTTPException, Request
8
- from fastapi.responses import StreamingResponse
9
- from pydantic import BaseModel
10
-
11
- from lattice_brain.ingestion import IngestionItem
12
- from latticeai.core.messages import http_error, resolve_language
13
- from latticeai.models.router import parse_model_ref
14
- from latticeai.services.process_audit import command_plan
15
- from latticeai.setup.auto_setup import (
16
- plan as auto_setup_plan,
17
- )
18
- from latticeai.setup.auto_setup import (
19
- preset as auto_setup_preset,
20
- )
21
- from latticeai.setup.auto_setup import (
22
- probe as auto_setup_probe,
23
- )
24
- from latticeai.setup.auto_setup import (
25
- recommend as auto_setup_recommend,
26
- )
27
- from latticeai.setup.auto_setup import (
28
- verify as auto_setup_verify,
29
- )
30
- from latticeai.setup.demo_corpus import (
31
- DEMO_DOCUMENTS,
32
- DEMO_METADATA_FLAG,
33
- DEMO_URI_PREFIX,
34
- demo_source_uri,
35
- suggested_questions,
36
- )
37
- from latticeai.setup.wizard import (
38
- get_recommendations,
39
- install_stream,
40
- open_url,
41
- scan_environment,
42
- )
43
-
44
-
45
- class SetupInstallRequest(BaseModel):
46
- items: List[Dict]
47
- confirmation_token: Optional[str] = None
48
-
49
-
50
- class DemoCorpusRequest(BaseModel):
51
- workspace_id: Optional[str] = None
52
-
53
-
54
- def create_setup_router(
55
- *,
56
- model_router,
57
- require_user,
58
- ingestion_pipeline=None,
59
- knowledge_graph=None,
60
- workspace_service=None,
61
- ) -> APIRouter:
62
- api_router = APIRouter()
63
- router = model_router
64
-
65
- # ── Setup Wizard ─────────────────────────────────────────────────────────────
66
-
67
- def setup_auto_state() -> Dict[str, Any]:
68
- """Return the PPT-aligned zero-config setup state used by setup UI/API."""
69
- profile = auto_setup_probe()
70
- recommendation = auto_setup_recommend(profile)
71
- install_plan = auto_setup_plan(profile, recommendation)
72
- return {
73
- "probe": profile.to_json(),
74
- "recommend": recommendation.to_json(),
75
- "plan": install_plan.to_json(),
76
- "verify": auto_setup_verify(profile, recommendation),
77
- "preset": auto_setup_preset(profile, recommendation),
78
- }
79
-
80
-
81
- def primary_setup_model(recs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
82
- models = recs.get("models") if isinstance(recs, dict) else None
83
- if not isinstance(models, list):
84
- return None
85
- candidates = [
86
- item for item in models
87
- if isinstance(item, dict) and not item.get("disabled") and (item.get("model_id") or (item.get("action") or {}).get("model_id"))
88
- ]
89
- if not candidates:
90
- return None
91
- return next((item for item in candidates if item.get("checked")), candidates[0])
92
-
93
-
94
- @api_router.get("/setup/scan")
95
- async def setup_scan(request: Request):
96
- """환경 감지 및 맞춤 추천 반환."""
97
- require_user(request)
98
- env = scan_environment()
99
- recs = get_recommendations(env)
100
- zero_config = setup_auto_state()
101
- primary_model = primary_setup_model(recs)
102
- if primary_model:
103
- model_id = primary_model.get("model_id") or (primary_model.get("action") or {}).get("model_id")
104
- model_provider, provider_model = parse_model_ref(str(model_id))
105
- primary_runtime = "mlx" if model_provider == "local_mlx" else model_provider
106
- zero_config.setdefault("recommend", {})["model_id"] = model_id
107
- zero_config["recommend"]["runtime"] = primary_runtime
108
- rationale = [
109
- item for item in zero_config["recommend"].get("rationale", [])
110
- if not (isinstance(item, str) and item.startswith("RAM ") and "→" in item)
111
- ]
112
- rationale.append(f"실제 다운로드 및 로드 가능한 {primary_runtime} 모델 → {model_id}")
113
- zero_config["recommend"]["rationale"] = rationale
114
- if isinstance(zero_config.get("plan"), dict):
115
- if model_provider == "ollama":
116
- command = ["ollama", "pull", provider_model]
117
- elif model_provider in {"vllm", "lmstudio", "llamacpp"}:
118
- command = ["lattice-ai", "models", "load", str(model_id)]
119
- else:
120
- command = ["huggingface-cli", "download", str(model_id), "--quiet"]
121
- step_plan = command_plan(
122
- command,
123
- name=f"weights:{model_id}",
124
- purpose="auto_setup_install",
125
- metadata={"model_id": str(model_id)},
126
- )
127
- zero_config["plan"]["steps"] = [{
128
- "name": f"weights:{model_id}",
129
- "why": "추론에 사용할 모델 가중치",
130
- "command": command,
131
- "requires_admin": False,
132
- "command_plan": step_plan,
133
- "confirmation_token": step_plan["confirmation_token"],
134
- }]
135
- if isinstance(zero_config.get("preset"), dict):
136
- zero_config["preset"].setdefault("model", {})["id"] = model_id
137
- zero_config["preset"]["model"]["runtime"] = primary_runtime
138
- env["zero_config"] = zero_config
139
- recs.setdefault("summary", {})["zero_config"] = zero_config["recommend"]
140
- recs["install_plan"] = zero_config["plan"]
141
- recs["preset"] = zero_config["preset"]
142
- return {"environment": env, "recommendations": recs, "zero_config": zero_config}
143
-
144
- @api_router.get("/setup/auto")
145
- async def setup_auto(request: Request):
146
- """PPT-aligned zero-config setup pipeline: probe → recommend → plan → verify → preset."""
147
- require_user(request)
148
- return setup_auto_state()
149
-
150
- @api_router.post("/setup/install")
151
- async def setup_install(req: SetupInstallRequest, request: Request):
152
- """선택된 항목을 순서대로 설치 · 로드하는 SSE 스트림."""
153
- user_email = require_user(request)
154
- async def _gen():
155
- async for chunk in install_stream(
156
- req.items,
157
- router,
158
- confirmation_token=req.confirmation_token,
159
- user_email=user_email,
160
- ):
161
- yield chunk
162
- return StreamingResponse(_gen(), media_type="text/event-stream",
163
- headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
164
-
165
- @api_router.post("/setup/open-auth/{mcp_id}")
166
- async def setup_open_auth(mcp_id: str, request: Request):
167
- require_user(request)
168
- """MCP 인증 페이지를 브라우저에서 자동으로 엽니다."""
169
- auth_urls: Dict[str, str] = {
170
- "github": "https://github.com/apps",
171
- "google-drive": "https://chatgpt.com/connectors",
172
- "slack": "https://chatgpt.com/connectors",
173
- "chrome": "https://chatgpt.com/connectors",
174
- "computer-use": "https://chatgpt.com/connectors",
175
- "figma": "https://chatgpt.com/connectors",
176
- "notion": "https://chatgpt.com/connectors",
177
- "linear": "https://chatgpt.com/connectors",
178
- "gmail": "https://chatgpt.com/connectors",
179
- "google-calendar": "https://chatgpt.com/connectors",
180
- "outlook-email": "https://chatgpt.com/connectors",
181
- "outlook-calendar": "https://chatgpt.com/connectors",
182
- "teams": "https://chatgpt.com/connectors",
183
- "sharepoint": "https://chatgpt.com/connectors",
184
- "canva": "https://chatgpt.com/connectors",
185
- }
186
- url = auth_urls.get(mcp_id)
187
- if not url:
188
- raise http_error(404, "mcp.unknown_id", resolve_language(request), mcp_id=mcp_id)
189
- open_url(url)
190
- return {"status": "ok", "opened": url, "mcp_id": mcp_id}
191
-
192
-
193
- # ── First Value Loop demo corpus (backlog #3, review §3.3 P0) ────────────
194
-
195
- def _require_demo_pipeline(request: Request):
196
- if ingestion_pipeline is None or not ingestion_pipeline.available():
197
- raise http_error(503, "capture.ingestion_disabled", resolve_language(request))
198
- if knowledge_graph is None:
199
- raise http_error(503, "common.graph_disabled", resolve_language(request))
200
-
201
- def _demo_workspace(request: Request, body_workspace: Optional[str], user: str) -> Optional[str]:
202
- header = request.headers.get("X-Workspace-Id")
203
- header = header.strip() if header and header.strip() else None
204
- supplied = [value for value in (body_workspace, header) if value]
205
- if len(set(supplied)) > 1:
206
- raise http_error(403, "common.workspace_mismatch", resolve_language(request))
207
- requested = supplied[0] if supplied else None
208
- if workspace_service is None:
209
- return requested
210
- try:
211
- return workspace_service.resolve_write_scope(requested, user or None)
212
- except PermissionError as exc:
213
- raise HTTPException(status_code=403, detail=str(exc)) from exc
214
-
215
- @api_router.get("/api/setup/demo-corpus")
216
- async def demo_corpus_status(request: Request):
217
- """Whether the demo corpus is installed + the suggestion chips."""
218
- require_user(request)
219
- _require_demo_pipeline(request)
220
- installed = knowledge_graph.find_documents_by_uri_prefix(DEMO_URI_PREFIX)
221
- return {
222
- "installed": bool(installed),
223
- "documents": installed,
224
- "document_count": len(installed),
225
- "suggested_questions": suggested_questions(),
226
- }
227
-
228
- @api_router.post("/api/setup/demo-corpus")
229
- async def demo_corpus_install(request: Request, req: Optional[DemoCorpusRequest] = None):
230
- """One-click ingest of the 3 built-in demo documents (idempotent).
231
-
232
- Every document goes through the normal IngestionPipeline door with
233
- ``demo://`` provenance so it is honest (real nodes, real chunks, real
234
- provenance) and removable via DELETE. Re-POSTing reports duplicates
235
- instead of duplicating.
236
- """
237
- user = require_user(request)
238
- _require_demo_pipeline(request)
239
- workspace_id = _demo_workspace(request, req.workspace_id if req else None, user)
240
- results = []
241
- ingested = 0
242
- duplicates = 0
243
- failed = 0
244
- for doc in DEMO_DOCUMENTS:
245
- result = ingestion_pipeline.ingest(
246
- IngestionItem(
247
- source_type="note",
248
- title=doc["title"],
249
- text=doc["text"],
250
- source_uri=demo_source_uri(doc["id"]),
251
- owner=user or None,
252
- workspace_id=workspace_id,
253
- metadata={DEMO_METADATA_FLAG: True, "demo_id": doc["id"]},
254
- ),
255
- user_email=user or None,
256
- )
257
- if result.status != "ok":
258
- failed += 1
259
- elif result.duplicate:
260
- duplicates += 1
261
- else:
262
- ingested += 1
263
- results.append({
264
- "demo_id": doc["id"],
265
- "title": doc["title"],
266
- "source_uri": demo_source_uri(doc["id"]),
267
- "status": result.status,
268
- "node_id": result.node_id,
269
- "duplicate": result.duplicate,
270
- "chunk_count": result.chunk_count,
271
- "detail": result.detail,
272
- })
273
- status = "ok" if failed == 0 else ("partial" if (ingested + duplicates) else "failed")
274
- return {
275
- "status": status,
276
- "ingested": ingested,
277
- "duplicates": duplicates,
278
- "failed": failed,
279
- "documents": results,
280
- "suggested_questions": suggested_questions(),
281
- }
282
-
283
- @api_router.delete("/api/setup/demo-corpus")
284
- async def demo_corpus_remove(request: Request):
285
- """Remove every demo document (node + chunks + edges + orphan source)."""
286
- require_user(request)
287
- _require_demo_pipeline(request)
288
- installed = knowledge_graph.find_documents_by_uri_prefix(DEMO_URI_PREFIX)
289
- removed = []
290
- for doc in installed:
291
- outcome = knowledge_graph.delete_document_tree(doc["id"])
292
- removed.append({
293
- "node_id": doc["id"],
294
- "title": doc.get("title"),
295
- "source_uri": doc.get("source_uri"),
296
- "status": outcome.get("status"),
297
- "removed_nodes": outcome.get("removed_nodes", 0),
298
- })
299
- return {
300
- "status": "ok",
301
- "removed_count": len(removed),
302
- "removed": removed,
303
- }
304
-
305
- @api_router.post("/permissions/open/{permission_id}")
306
- async def open_permission_settings(permission_id: str, request: Request):
307
- require_user(request)
308
- """macOS 권한 설정 화면을 엽니다."""
309
- urls = {
310
- "accessibility": "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
311
- "automation": "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation",
312
- "screen": "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
313
- }
314
- url = urls.get(permission_id)
315
- if not url:
316
- raise http_error(404, "setup.unknown_permission", resolve_language(request))
317
- open_url(url)
318
- return {"status": "ok", "opened": url, "permission": permission_id}
319
- return api_router
@@ -1,354 +0,0 @@
1
- """Static UI and lightweight status routes."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import hashlib
7
- import hmac
8
- import re
9
- import secrets
10
- import subprocess
11
- import time
12
- from dataclasses import dataclass
13
- from pathlib import Path
14
- from typing import Any, Callable, Dict, Optional
15
-
16
- from fastapi import APIRouter, Cookie, HTTPException, Request
17
- from fastapi.responses import FileResponse, HTMLResponse
18
-
19
- from latticeai.api.ui_redirects import app_redirect
20
- from latticeai.core.quiet import quiet
21
-
22
- PRODUCTION_CSP = (
23
- "default-src 'self'; "
24
- "script-src 'self'; "
25
- "style-src 'self' 'unsafe-inline'; "
26
- "img-src 'self' data: blob: http://127.0.0.1:*; "
27
- "font-src 'self' data:; "
28
- # No `ws:`: this product has no WebSocket endpoint anywhere. The live
29
- # surfaces (chat, agent steps, index progress) are server-sent events, read
30
- # with fetch/ReadableStream, which `connect-src` already covers over http.
31
- "connect-src 'self' http://127.0.0.1:*; "
32
- "frame-src 'none'; "
33
- "object-src 'none'; "
34
- "base-uri 'none'; "
35
- "form-action 'self'; "
36
- "frame-ancestors 'none'"
37
- )
38
-
39
- INVITE_COOKIE_NAME = "lattice_invite"
40
- INVITE_COOKIE_TTL_SECONDS = 60 * 60 * 24 * 7
41
-
42
- # Host-capacity plain-language judgment for System basic mode (layout rebuild).
43
- # Thresholds live here so the frontend never re-derives copy from raw percents.
44
- SYSINFO_READINESS_ROOMY_MAX = 55.0
45
- SYSINFO_READINESS_TIGHT_MAX = 80.0
46
-
47
-
48
- def _probe_host_capacity() -> Dict[str, Any]:
49
- """Sample CPU / RAM / GPU on this machine. Blocking — call off the loop.
50
-
51
- Extracted from the route so the subprocess sampling can be handed to a
52
- worker thread, and so the parsing is testable without an HTTP request.
53
- """
54
- result: Dict[str, Any] = {
55
- "cpu_pct": 0.0,
56
- "ram_pct": 0.0,
57
- "gpu_mem_pct": 0.0,
58
- "gpu_mem_gb": 0.0,
59
- "readiness": "roomy",
60
- }
61
- try:
62
- # CPU
63
- top_out = subprocess.run(["top", "-l", "1", "-n", "0"], capture_output=True, text=True, timeout=4).stdout
64
- for line in top_out.splitlines():
65
- if "CPU usage" in line:
66
- m = re.search(r"([\d.]+)% user.*?([\d.]+)% sys", line)
67
- if m:
68
- result["cpu_pct"] = round(float(m.group(1)) + float(m.group(2)), 1)
69
- # RAM
70
- vm_out = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=4).stdout
71
- pages: dict = {}
72
- for line in vm_out.splitlines():
73
- for key in ["Pages free", "Pages active", "Pages inactive", "Pages wired down", "Pages occupied by compressor"]:
74
- if line.startswith(key):
75
- m = re.search(r"(\d+)", line)
76
- if m:
77
- pages[key] = int(m.group(1))
78
- total = sum(pages.values())
79
- used = total - pages.get("Pages free", 0)
80
- result["ram_pct"] = round(used / total * 100, 1) if total else 0.0
81
- # GPU (MLX / Apple Silicon unified memory)
82
- try:
83
- import mlx.core as _mx
84
- hw_out = subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, timeout=2).stdout
85
- total_bytes = int(hw_out.strip())
86
- gpu_bytes = _mx.get_active_memory() + _mx.get_cache_memory()
87
- result["gpu_mem_gb"] = round(gpu_bytes / (1024 ** 3), 2)
88
- result["gpu_mem_pct"] = round(gpu_bytes / total_bytes * 100, 1) if total_bytes else 0.0
89
- except Exception:
90
- quiet()
91
- except Exception as e:
92
- result["error"] = str(e)
93
- result["readiness"] = host_capacity_readiness(
94
- cpu_pct=float(result.get("cpu_pct") or 0.0),
95
- ram_pct=float(result.get("ram_pct") or 0.0),
96
- gpu_mem_pct=float(result.get("gpu_mem_pct") or 0.0),
97
- )
98
- return result
99
-
100
-
101
- def host_capacity_readiness(
102
- *,
103
- cpu_pct: float = 0.0,
104
- ram_pct: float = 0.0,
105
- gpu_mem_pct: float = 0.0,
106
- ) -> str:
107
- """Map host telemetry to a single plain-language readiness bucket.
108
-
109
- Returns one of ``roomy`` / ``tight`` / ``low``. The product uses this so
110
- basic-mode System copy and advanced-mode numbers share one judgment.
111
- """
112
- load = max(float(cpu_pct or 0.0), float(ram_pct or 0.0), float(gpu_mem_pct or 0.0))
113
- if load <= SYSINFO_READINESS_ROOMY_MAX:
114
- return "roomy"
115
- if load <= SYSINFO_READINESS_TIGHT_MAX:
116
- return "tight"
117
- return "low"
118
-
119
-
120
- def _sign_invite_cookie(secret: str, *, now: Optional[int] = None) -> str:
121
- """Create an expiring, nonce-bearing invite-gate capability cookie."""
122
-
123
- issued_at = int(time.time() if now is None else now)
124
- expires_at = issued_at + INVITE_COOKIE_TTL_SECONDS
125
- nonce = secrets.token_urlsafe(24)
126
- payload = f"{expires_at}.{nonce}"
127
- signature = hmac.new(
128
- str(secret).encode("utf-8"),
129
- payload.encode("utf-8"),
130
- hashlib.sha256,
131
- ).hexdigest()
132
- return f"v1.{payload}.{signature}"
133
-
134
-
135
- def _verify_invite_cookie(
136
- value: Optional[str],
137
- secret: str,
138
- *,
139
- now: Optional[int] = None,
140
- ) -> bool:
141
- """Verify version, expiry and HMAC without trusting client claims."""
142
-
143
- if not value or not secret:
144
- return False
145
- try:
146
- version, raw_expiry, nonce, supplied_signature = value.split(".", 3)
147
- expires_at = int(raw_expiry)
148
- except (TypeError, ValueError):
149
- return False
150
- if version != "v1" or not nonce or expires_at <= int(time.time() if now is None else now):
151
- return False
152
- payload = f"{expires_at}.{nonce}"
153
- expected_signature = hmac.new(
154
- str(secret).encode("utf-8"),
155
- payload.encode("utf-8"),
156
- hashlib.sha256,
157
- ).hexdigest()
158
- return hmac.compare_digest(supplied_signature, expected_signature)
159
-
160
-
161
- def _invite_denied_response() -> HTMLResponse:
162
- return HTMLResponse(
163
- content="""
164
- <body style="background:#0f1115; color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; font-family:sans-serif;">
165
- <div style="background:#16191f; padding:40px; border-radius:24px; border:1px solid rgba(255,255,255,0.1); text-align:center; box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
166
- <div style="font-size:48px; margin-bottom:20px;">🔒</div>
167
- <h1 style="color:#378ADD; margin:0; font-size:24px;">Invitation Required</h1>
168
- <p style="color:#94a3b8; margin:20px 0; line-height:1.6;">이 서비스는 비공개로 운영되고 있습니다.<br>선생님께 받은 <b>초대용 전용 링크</b>를 통해 접속해 주세요.</p>
169
- <div style="margin-top:30px; padding-top:20px; border-top:1px solid rgba(255,255,255,0.05); font-size:11px; color:rgba(255,255,255,0.2); letter-spacing:1px;">LATTICE AI</div>
170
- </div>
171
- </body>
172
- """,
173
- status_code=403,
174
- )
175
-
176
-
177
- def ui_file_response(path: Path) -> FileResponse:
178
- response = FileResponse(path)
179
- response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
180
- response.headers["Pragma"] = "no-cache"
181
- response.headers["Expires"] = "0"
182
- response.headers["Content-Security-Policy"] = PRODUCTION_CSP
183
- return response
184
-
185
- @dataclass(frozen=True)
186
- class StaticRoutesBundle:
187
- router: APIRouter
188
- ui_file_response: Callable[[Path], FileResponse]
189
- local_sysinfo: Callable[[Request], object]
190
- invite_authorized: Callable[[Request], bool]
191
-
192
-
193
- def create_static_routes_router(
194
- *,
195
- static_dir: Path,
196
- invite_gate_enabled: bool,
197
- invite_code: str,
198
- app_mode: str,
199
- model_router,
200
- require_user,
201
- invite_cookie_secret: str = "",
202
- secure_cookies: bool = False,
203
- ) -> StaticRoutesBundle:
204
- api_router = APIRouter()
205
- STATIC_DIR = static_dir
206
- INVITE_GATE_ENABLED = invite_gate_enabled
207
- INVITE_CODE = invite_code
208
- INVITE_COOKIE_SECRET = invite_cookie_secret or secrets.token_urlsafe(48)
209
- SECURE_COOKIES = bool(secure_cookies)
210
- APP_MODE = app_mode
211
- router = model_router
212
-
213
- def invite_authorized(request: Request) -> bool:
214
- """Verify the request's signed invite claim at every gated boundary."""
215
-
216
- if not INVITE_GATE_ENABLED:
217
- return True
218
- return _verify_invite_cookie(
219
- request.cookies.get(INVITE_COOKIE_NAME),
220
- INVITE_COOKIE_SECRET,
221
- )
222
-
223
- @api_router.get("/")
224
- async def root(
225
- request: Request,
226
- code: Optional[str] = None,
227
- invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
228
- ):
229
- """로그인/회원가입 페이지. 초대 게이트 활성화 시 코드 검증 후 진입."""
230
- if not INVITE_GATE_ENABLED:
231
- return app_redirect("account", request)
232
-
233
- # 1. 유효한 서버 서명 쿠키가 있는 경우
234
- if invite_authorized(request):
235
- return app_redirect("account")
236
-
237
- # 2. 초대 코드가 일치하는 경우 (최초 진입)
238
- if INVITE_CODE and code and secrets.compare_digest(code, INVITE_CODE):
239
- # Do not retain the invitation code in the redirect URL/history.
240
- response = app_redirect("account")
241
- response.set_cookie(
242
- key=INVITE_COOKIE_NAME,
243
- value=_sign_invite_cookie(INVITE_COOKIE_SECRET),
244
- httponly=True,
245
- secure=SECURE_COOKIES,
246
- samesite="lax",
247
- max_age=INVITE_COOKIE_TTL_SECONDS,
248
- path="/",
249
- )
250
- return response
251
-
252
- # 3. 인증 실패 시 차단 화면
253
- return _invite_denied_response()
254
-
255
-
256
- @api_router.get("/account")
257
- async def account_page(
258
- request: Request,
259
- invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
260
- ):
261
- """Direct login/register page route used by logout and manual navigation."""
262
- if INVITE_GATE_ENABLED and not invite_authorized(request):
263
- return _invite_denied_response()
264
- return app_redirect("account")
265
-
266
-
267
- @api_router.get("/manifest.json")
268
- async def manifest():
269
- p = STATIC_DIR / "manifest.json"
270
- if not p.exists():
271
- raise HTTPException(status_code=404)
272
- return FileResponse(str(p), media_type="application/manifest+json")
273
-
274
- @api_router.api_route("/favicon.ico", methods=["GET", "HEAD"], include_in_schema=False)
275
- async def favicon():
276
- ico = STATIC_DIR / "favicon.ico"
277
- png = STATIC_DIR / "icons" / "favicon-32.png"
278
- if ico.exists():
279
- return FileResponse(str(ico), media_type="image/x-icon")
280
- if png.exists():
281
- return FileResponse(str(png), media_type="image/png")
282
- raise HTTPException(status_code=404)
283
-
284
-
285
- @api_router.get("/sw.js")
286
- async def service_worker():
287
- p = STATIC_DIR / "sw.js"
288
- if not p.exists():
289
- raise HTTPException(status_code=404)
290
- resp = FileResponse(str(p), media_type="application/javascript")
291
- resp.headers["Service-Worker-Allowed"] = "/"
292
- return resp
293
-
294
-
295
- @api_router.get("/chat")
296
- async def chat_page(request: Request):
297
- return app_redirect("chat", request)
298
-
299
-
300
- @api_router.get("/app")
301
- async def app_shell(
302
- request: Request,
303
- invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
304
- ):
305
- """React desktop single-page workspace shell."""
306
- if INVITE_GATE_ENABLED and not invite_authorized(request):
307
- return _invite_denied_response()
308
- page = STATIC_DIR / "app" / "index.html"
309
- if not page.exists():
310
- raise HTTPException(status_code=404, detail="React shell not found.")
311
- return ui_file_response(page)
312
-
313
-
314
- @api_router.get("/admin")
315
- async def admin_page(request: Request):
316
- return app_redirect("admin/users", request)
317
-
318
- # /workspace and /onboarding UI pages are served by the workspace router
319
- # (latticeai.api.workspace), included below after its dependencies are defined.
320
-
321
- @api_router.get("/status")
322
- async def status(request: Request):
323
- """서버 상태 및 현재 로드된 모델 정보를 반환합니다."""
324
- require_user(request)
325
- return {
326
- "message": "🧠 Lattice AI MLX Server is running!",
327
- "status": "online",
328
- "mode": APP_MODE,
329
- "loaded_model": router._current or "None"
330
- }
331
-
332
-
333
- @api_router.get("/local/sysinfo")
334
- async def local_sysinfo(request: Request):
335
- """CPU / RAM / GPU(MLX) usage plus a plain-language readiness bucket.
336
-
337
- ``readiness`` is ``roomy`` | ``tight`` | ``low`` so basic System copy
338
- does not re-interpret raw percents on the client.
339
-
340
- The probe itself is three subprocesses — ``top -l 1`` alone samples for
341
- about a second — so it runs on a worker thread. Run inline it held the
342
- event loop for the whole sample, and this route is hit on the System
343
- screen *and* during first-run analysis, i.e. exactly while an answer is
344
- streaming: one status poll used to stall every open stream.
345
- """
346
- require_user(request)
347
- return await asyncio.to_thread(_probe_host_capacity)
348
-
349
- return StaticRoutesBundle(
350
- api_router,
351
- ui_file_response,
352
- local_sysinfo,
353
- invite_authorized,
354
- )