ltcai 11.2.0 → 11.4.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 (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -0,0 +1,555 @@
1
+ """Phases 1-4: platform, configuration, identity, brain.
2
+
3
+ The first half of the build order — everything that must exist before a
4
+ FastAPI application object is worth creating. Split out of the single
5
+ ``build_phases`` module in v11.3.0; the package ``__init__`` still owns the
6
+ :data:`~latticeai.runtime.build_phases.BUILD_PHASES` order and re-exports every
7
+ phase under its historical name.
8
+
9
+ Every heavy import lives *inside* a phase, never at module scope
10
+ (``tests/unit/test_runtime_context.py`` enforces that for each submodule).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from latticeai.core.quiet import quiet
19
+ from latticeai.runtime.runtime_context import RuntimeContext
20
+
21
+
22
+ # ── phase 1: platform ────────────────────────────────────────────────────────
23
+ def phase_platform(ctx: RuntimeContext) -> None:
24
+ """Select the MLX Metal device in the main thread.
25
+
26
+ Kept first and alone: it is the one step that talks to hardware, and it
27
+ must happen on the main thread before any worker touches MLX.
28
+ """
29
+ ctx.enter("platform")
30
+ try:
31
+ import mlx.core as mx
32
+
33
+ mx.set_default_device(mx.gpu) # type: ignore[arg-type]
34
+ print("✅ MLX Metal context initialized in main thread.")
35
+ except Exception as exc:
36
+ print(f"⚠️ MLX Metal context unavailable: {exc}")
37
+ mx = None # type: ignore[assignment]
38
+ ctx.set(mx=mx)
39
+
40
+
41
+ # ── phase 2: configuration and paths ─────────────────────────────────────────
42
+ def phase_config(ctx: RuntimeContext) -> None:
43
+ """Parse configuration once and lay out the data directory."""
44
+ ctx.enter("config")
45
+
46
+ from latticeai.core.security import host_is_loopback as _host_is_loopback_impl
47
+ from latticeai.core.workspace_os import WORKSPACE_OS_VERSION
48
+ from latticeai.runtime.config_runtime import build_config_runtime
49
+ from latticeai.runtime.security_runtime import build_security_runtime
50
+
51
+ try:
52
+ import keyring
53
+ except Exception:
54
+ keyring = None # type: ignore[assignment]
55
+
56
+ config_runtime = build_config_runtime(ctx.config_arg)
57
+ ctx.set(config_runtime=config_runtime, keyring=keyring)
58
+ ctx.adopt(
59
+ config_runtime,
60
+ "CONFIG",
61
+ "APP_MODE",
62
+ "IS_PUBLIC_MODE",
63
+ "DEFAULT_HOST",
64
+ "DEFAULT_PORT",
65
+ "ENABLE_TELEGRAM",
66
+ "ENABLE_GRAPH",
67
+ "AUTOLOAD_MODELS",
68
+ "MODEL_IDLE_UNLOAD_SECONDS",
69
+ "ALLOW_MODEL_DOWNLOADS",
70
+ "MODEL_DOWNLOAD_TIMEOUT",
71
+ "ALLOW_LOCAL_MODELS",
72
+ "REQUIRE_AUTH",
73
+ "ALLOW_PLAINTEXT_API_KEYS",
74
+ "CORS_ALLOW_NETWORK",
75
+ "CORS_EXTRA_ORIGINS",
76
+ "CSRF_TRUSTED_ORIGINS",
77
+ "PUBLIC_MODEL",
78
+ "LOCAL_MODEL",
79
+ "LOCAL_DRAFT_MODEL",
80
+ )
81
+ ctx.set(
82
+ APP_VERSION=WORKSPACE_OS_VERSION,
83
+ OPEN_REGISTRATION=ctx.CONFIG.open_registration,
84
+ _RATE_LIMIT_ENABLED=ctx.CONFIG.rate_limit_enabled,
85
+ # Forwarded headers (X-Forwarded-For / CF-Connecting-IP) are honoured
86
+ # for IP rate limiting only when the direct peer is a trusted proxy.
87
+ _host_is_loopback=lambda host: _host_is_loopback_impl(host),
88
+ )
89
+
90
+ security_runtime = build_security_runtime(ctx.CONFIG)
91
+ ctx.set(security_runtime=security_runtime)
92
+ ctx.adopt(
93
+ security_runtime,
94
+ "SSO_DISCOVERY_URL",
95
+ "SSO_CLIENT_ID",
96
+ "SSO_CLIENT_SECRET",
97
+ "SSO_REDIRECT_URI",
98
+ "SSO_PROVIDER_NAME",
99
+ "INVITE_CODE",
100
+ "INVITE_COOKIE_SECRET",
101
+ "INVITE_GATE_ENABLED",
102
+ "SECURE_COOKIES",
103
+ )
104
+
105
+ from pathlib import Path
106
+
107
+ base_dir = Path(__file__).resolve().parent.parent.parent
108
+ data_dir = ctx.CONFIG.data_dir
109
+ data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
110
+ try:
111
+ data_dir.chmod(0o700)
112
+ except OSError:
113
+ quiet()
114
+
115
+ ctx.set(
116
+ BASE_DIR=base_dir,
117
+ DATA_DIR=data_dir,
118
+ STATIC_DIR=ctx.CONFIG.static_dir,
119
+ USERS_FILE=data_dir / "users.json",
120
+ HISTORY_FILE=data_dir / "chat_history.json",
121
+ VPC_FILE=data_dir / "vpc_config.json",
122
+ AUDIT_FILE=data_dir / "audit_log.json",
123
+ SSO_FILE=data_dir / "sso_config.json",
124
+ )
125
+
126
+
127
+ # ── phase 3: identity, audit, access ─────────────────────────────────────────
128
+ def phase_identity(ctx: RuntimeContext) -> None:
129
+ """Users, sessions, audit, access control, API keys, SSO/VPC config."""
130
+ ctx.enter("identity")
131
+
132
+ from fastapi import HTTPException, Request
133
+
134
+ from latticeai.core.mcp_registry import create_mcp_install_state
135
+ from latticeai.core.security import (
136
+ check_ip_rate_limit as _check_ip_rate_limit,
137
+ )
138
+ from latticeai.core.security import (
139
+ client_ip as _client_ip_impl,
140
+ )
141
+ from latticeai.core.security import (
142
+ enforce_rate_limit as _enforce_rate_limit,
143
+ )
144
+ from latticeai.core.security import hash_password, verify_password
145
+ from latticeai.core.security import (
146
+ redact_secret_text as _redact_secret_text,
147
+ )
148
+ from latticeai.core.users import (
149
+ ensure_user_identity,
150
+ load_users_file,
151
+ migrate_knowledge_graph_identity,
152
+ save_users_file,
153
+ )
154
+ from latticeai.core.users import user_id_for_email as _user_id_for_email
155
+ from latticeai.runtime.access_runtime import build_access_runtime
156
+ from latticeai.runtime.audit_runtime import build_audit_runtime
157
+ from latticeai.runtime.bootstrap import build_session_runtime
158
+ from latticeai.runtime.network_config_runtime import build_vpc_runtime
159
+ from latticeai.runtime.sso_config_runtime import build_sso_config_runtime
160
+ from latticeai.runtime.user_key_runtime import build_user_key_runtime
161
+
162
+ # MCP install state (extracted to mcp_registry during the server decomp).
163
+ mcp_state = create_mcp_install_state(ctx.DATA_DIR)
164
+ ctx.adopt(
165
+ mcp_state,
166
+ "load_mcp_installs",
167
+ "mcp_public_item",
168
+ "recommend_mcps",
169
+ "install_mcp",
170
+ )
171
+
172
+ def load_users() -> Dict[str, Any]:
173
+ users = load_users_file(ctx.USERS_FILE)
174
+ email_to_id = {
175
+ email: str(user["id"])
176
+ for email, user in users.items()
177
+ if isinstance(user, dict) and user.get("id")
178
+ }
179
+ try:
180
+ migrate_knowledge_graph_identity(
181
+ ctx.DATA_DIR / "knowledge_graph.sqlite", email_to_id
182
+ )
183
+ except Exception as exc:
184
+ logging.warning("knowledge graph identity migration skipped: %s", exc)
185
+ try:
186
+ # WORKSPACE_OS is built in the `brain` phase; resolving it here (at
187
+ # call time) is why this stays a closure over ctx.
188
+ ctx.WORKSPACE_OS.migrate_workspace_identities(email_to_id)
189
+ except Exception as exc:
190
+ logging.warning("workspace identity migration skipped: %s", exc)
191
+ return users
192
+
193
+ def save_users(users: Dict[str, Any]) -> None:
194
+ save_users_file(ctx.USERS_FILE, users)
195
+
196
+ def user_id_for_email(email: Optional[str]) -> Optional[str]:
197
+ return _user_id_for_email(load_users(), email)
198
+
199
+ def verify_and_migrate_password(
200
+ email: str, plain: str, stored: str, users: Dict
201
+ ) -> bool:
202
+ """평문 비밀번호를 투명하게 해시로 마이그레이션. 마이그레이션 발생 시 audit log 남김."""
203
+ if ":" in stored and len(stored) > 64:
204
+ return verify_password(plain, stored)
205
+ if plain == stored:
206
+ users[email]["password"] = hash_password(plain)
207
+ save_users(users)
208
+ try:
209
+ ctx.append_audit_event(
210
+ "password_migrated_from_plaintext", user_email=email
211
+ )
212
+ except Exception as exc:
213
+ logging.warning("audit log failed on password migration: %s", exc)
214
+ logging.info("Migrated plaintext password to scrypt hash for %s", email)
215
+ return True
216
+ return False
217
+
218
+ def redact_secret_text(text: str) -> str:
219
+ return _redact_secret_text(text)
220
+
221
+ def _check_rate_limit(
222
+ ip: str, action: str, max_calls: int, window_secs: float
223
+ ) -> None:
224
+ _check_ip_rate_limit(ip, action, max_calls=max_calls, window_secs=window_secs)
225
+
226
+ def _client_ip(request: Request) -> str:
227
+ return _client_ip_impl(request)
228
+
229
+ def enforce_rate_limit(email: str, bucket_key: str) -> None:
230
+ _enforce_rate_limit(email, bucket_key, enabled=ctx._RATE_LIMIT_ENABLED)
231
+
232
+ ctx.set(
233
+ load_users=load_users,
234
+ save_users=save_users,
235
+ user_id_for_email=user_id_for_email,
236
+ verify_and_migrate_password=verify_and_migrate_password,
237
+ redact_secret_text=redact_secret_text,
238
+ _check_rate_limit=_check_rate_limit,
239
+ _client_ip=_client_ip,
240
+ enforce_rate_limit=enforce_rate_limit,
241
+ )
242
+
243
+ # Session token lifecycle; user_id_for_email is the injected subject resolver.
244
+ session_runtime = build_session_runtime(user_id_resolver=user_id_for_email)
245
+ ctx.adopt(
246
+ session_runtime,
247
+ "_SESSION_TTL",
248
+ "_session_store",
249
+ "create_session",
250
+ "get_session_email",
251
+ "invalidate_session",
252
+ )
253
+
254
+ # Audit is built after redaction is available, because every event is
255
+ # redacted before it is written.
256
+ audit_runtime = build_audit_runtime(
257
+ audit_file=ctx.AUDIT_FILE,
258
+ logging=logging,
259
+ redact_fn=redact_secret_text,
260
+ )
261
+ ctx.adopt(audit_runtime, "get_audit_log", "append_audit_event")
262
+
263
+ access_runtime = build_access_runtime(
264
+ config=ctx.CONFIG,
265
+ require_auth=ctx.REQUIRE_AUTH,
266
+ http_exception=HTTPException,
267
+ request_type=Request,
268
+ load_users=load_users,
269
+ get_session_email=ctx.get_session_email,
270
+ user_id_for_email=_user_id_for_email,
271
+ )
272
+ ctx.adopt(
273
+ access_runtime,
274
+ "get_user_role",
275
+ "_extract_bearer_token",
276
+ "get_current_user",
277
+ "require_user",
278
+ "require_admin",
279
+ "public_user",
280
+ )
281
+
282
+ user_key_runtime = build_user_key_runtime(
283
+ load_users=load_users,
284
+ save_users=save_users,
285
+ ensure_user_identity=ensure_user_identity,
286
+ keyring=ctx.keyring,
287
+ allow_plaintext_api_keys=ctx.ALLOW_PLAINTEXT_API_KEYS,
288
+ logging=logging,
289
+ http_exception=HTTPException,
290
+ )
291
+ ctx.adopt(
292
+ user_key_runtime, "get_history_user", "get_user_api_key", "set_user_api_key"
293
+ )
294
+
295
+ # SSO config + OIDC discovery share one closure, so saving the config
296
+ # actually invalidates the discovery cache.
297
+ sso_runtime = build_sso_config_runtime(
298
+ sso_file=ctx.SSO_FILE,
299
+ discovery_url=ctx.SSO_DISCOVERY_URL,
300
+ client_id=ctx.SSO_CLIENT_ID,
301
+ client_secret=ctx.SSO_CLIENT_SECRET,
302
+ redirect_uri=ctx.SSO_REDIRECT_URI,
303
+ provider_name=ctx.SSO_PROVIDER_NAME,
304
+ logging=logging,
305
+ )
306
+ ctx.adopt(
307
+ sso_runtime,
308
+ "_sso_env_defaults",
309
+ "get_sso_settings",
310
+ "public_sso_config",
311
+ "save_sso_config",
312
+ "_get_sso_discovery",
313
+ "_sso_states",
314
+ )
315
+
316
+ vpc_runtime = build_vpc_runtime(vpc_file=ctx.VPC_FILE, logging=logging)
317
+ ctx.adopt(vpc_runtime, "load_vpc_config", "save_vpc_config")
318
+
319
+
320
+ # ── phase 4: brain and persistence ───────────────────────────────────────────
321
+ def phase_brain(ctx: RuntimeContext) -> None:
322
+ """Embedder, knowledge graph, conversations, hooks, persistence, history."""
323
+ ctx.enter("brain")
324
+
325
+ import os
326
+
327
+ from fastapi import HTTPException
328
+
329
+ from lattice_brain.graph.schema import set_embed_dim
330
+ from lattice_brain.ingestion import IngestionItem
331
+ from lattice_brain.storage import storage_from_env
332
+ from latticeai.core.audit import (
333
+ build_admin_audit_report as _build_admin_audit_report,
334
+ )
335
+ from latticeai.core.audit import (
336
+ build_sensitivity_report as _build_sensitivity_report,
337
+ )
338
+ from latticeai.core.audit import (
339
+ classify_sensitive_message as _classify_sensitive_message,
340
+ )
341
+ from latticeai.core.embedding_providers import (
342
+ resolve_embedder,
343
+ resolve_embedding_profile,
344
+ )
345
+ from latticeai.core.security import (
346
+ bytes_match_extension as _bytes_match_extension_impl,
347
+ )
348
+ from latticeai.models.router import normalize_branding
349
+ from latticeai.runtime.brain_runtime import build_brain_runtime
350
+ from latticeai.runtime.history_runtime import build_history_query_runtime
351
+ from latticeai.runtime.history_writer import HistoryWriterDeps, write_chat_turn
352
+ from latticeai.runtime.hooks_runtime import build_hooks_runtime
353
+ from latticeai.runtime.persistence_runtime import build_persistence_runtime
354
+
355
+ # Resolve the configured embedding provider once. Degrades to the offline
356
+ # hash fallback when unavailable, recording requested-vs-active provider.
357
+ try:
358
+ embedding_profile = resolve_embedding_profile(ctx.CONFIG.embedding_profile)
359
+ except ValueError as exc:
360
+ logging.warning("Embedding profile ignored: %s", exc)
361
+ embedding_profile = {}
362
+
363
+ provider = ctx.CONFIG.embedding_provider
364
+ model = ctx.CONFIG.embedding_model or str(embedding_profile.get("model") or "")
365
+ dim = ctx.CONFIG.embedding_dim or int(embedding_profile.get("dimensions") or 0)
366
+ if ctx.CONFIG.embedding_profile and provider in {"", "hash", "local", "fallback"}:
367
+ provider = str(embedding_profile.get("provider") or provider)
368
+
369
+ embedder = resolve_embedder(
370
+ provider,
371
+ model=model,
372
+ base_url=ctx.CONFIG.embedding_base_url,
373
+ api_key=ctx.CONFIG.embedding_api_key,
374
+ dim=dim,
375
+ timeout=ctx.CONFIG.embedding_timeout,
376
+ extra={"target": ctx.CONFIG.embedding_custom_target},
377
+ probe=provider not in {"", "hash", "local", "fallback"},
378
+ )
379
+ set_embed_dim(int(getattr(embedder, "dim", None) or dim or 384))
380
+ if embedder.fell_back:
381
+ logging.warning(
382
+ "Embedding provider %s unavailable: %s", embedder.requested, embedder.detail
383
+ )
384
+
385
+ storage_engine = (
386
+ storage_from_env(os.environ, data_dir=ctx.DATA_DIR) if ctx.ENABLE_GRAPH else None
387
+ )
388
+ ctx.set(
389
+ EMBEDDING_PROFILE=embedding_profile,
390
+ EMBEDDER=embedder,
391
+ STORAGE_ENGINE=storage_engine,
392
+ )
393
+
394
+ brain_runtime = build_brain_runtime(
395
+ data_dir=ctx.DATA_DIR,
396
+ history_file=ctx.HISTORY_FILE,
397
+ enable_graph=ctx.ENABLE_GRAPH,
398
+ embedder=embedder,
399
+ storage_engine=storage_engine,
400
+ )
401
+ ctx.set(brain_runtime=brain_runtime)
402
+ # CONVERSATIONS is the v4 durable episodic store: unbounded history in the
403
+ # same SQLite file as the graph, so backup/restore covers it for free.
404
+ ctx.adopt(brain_runtime, "KNOWLEDGE_GRAPH", "CONVERSATIONS")
405
+
406
+ def save_to_history(
407
+ role: str,
408
+ message: str,
409
+ user_email: Optional[str] = None,
410
+ user_nickname: Optional[str] = None,
411
+ source: Optional[str] = None,
412
+ conversation_id: Optional[str] = None,
413
+ workspace_id: Optional[str] = None,
414
+ ) -> None:
415
+ """Persist one chat turn. Logic lives in runtime/history_writer.py.
416
+
417
+ The dependency bundle is built per call because INGESTION_PIPELINE is
418
+ published later in this same phase; resolving through ctx keeps the
419
+ ordering contract in one place instead of reordering the phase.
420
+ """
421
+ write_chat_turn(
422
+ role,
423
+ message,
424
+ user_email=user_email,
425
+ user_nickname=user_nickname,
426
+ source=source,
427
+ conversation_id=conversation_id,
428
+ workspace_id=workspace_id,
429
+ deps=HistoryWriterDeps(
430
+ conversations=ctx.CONVERSATIONS,
431
+ append_audit_event=ctx.append_audit_event,
432
+ classify_sensitive_message=ctx.classify_sensitive_message,
433
+ redact_secret_text=ctx.redact_secret_text,
434
+ normalize_branding=normalize_branding,
435
+ ingestion_pipeline=ctx.INGESTION_PIPELINE,
436
+ ingestion_item_factory=IngestionItem,
437
+ enable_graph=ctx.ENABLE_GRAPH,
438
+ knowledge_graph=ctx.KNOWLEDGE_GRAPH,
439
+ ),
440
+ )
441
+
442
+ def classify_sensitive_message(item: Dict, index: int) -> Dict:
443
+ return _classify_sensitive_message(item, index)
444
+
445
+ def build_sensitivity_report(history: List[Dict]) -> Dict:
446
+ return _build_sensitivity_report(history)
447
+
448
+ def build_admin_audit_report(
449
+ users: Dict, audit_events: Optional[List[Dict]] = None
450
+ ) -> Dict:
451
+ graph_stats = None
452
+ try:
453
+ if ctx.ENABLE_GRAPH and ctx.KNOWLEDGE_GRAPH:
454
+ graph_stats = ctx.KNOWLEDGE_GRAPH.stats()
455
+ except Exception:
456
+ quiet()
457
+ return _build_admin_audit_report(
458
+ ctx.AUDIT_FILE,
459
+ users,
460
+ get_user_role=ctx.get_user_role,
461
+ graph_stats=graph_stats,
462
+ audit_events=audit_events,
463
+ )
464
+
465
+ def _bytes_match_extension(data: bytes, ext: str) -> bool:
466
+ return _bytes_match_extension_impl(data, ext)
467
+
468
+ def _require_graph() -> None:
469
+ if not ctx.ENABLE_GRAPH or ctx.KNOWLEDGE_GRAPH is None:
470
+ raise HTTPException(
471
+ status_code=404,
472
+ detail=(
473
+ "지식 그래프가 비활성화되어 있습니다. "
474
+ "LATTICEAI_ENABLE_GRAPH=true 설정 후 다시 시도해 주세요."
475
+ ),
476
+ )
477
+
478
+ def _workspace_graph() -> Any:
479
+ return (
480
+ ctx.KNOWLEDGE_GRAPH
481
+ if (ctx.ENABLE_GRAPH and ctx.KNOWLEDGE_GRAPH)
482
+ else None
483
+ )
484
+
485
+ ctx.set(
486
+ save_to_history=save_to_history,
487
+ classify_sensitive_message=classify_sensitive_message,
488
+ build_sensitivity_report=build_sensitivity_report,
489
+ build_admin_audit_report=build_admin_audit_report,
490
+ _bytes_match_extension=_bytes_match_extension,
491
+ _require_graph=_require_graph,
492
+ _workspace_graph=_workspace_graph,
493
+ )
494
+
495
+ # The hooks registry is built ahead of the watcher so folder-watch
496
+ # reindexes can fire the pre_index/post_index lifecycle hooks.
497
+ hooks_runtime = build_hooks_runtime(
498
+ data_dir=ctx.DATA_DIR,
499
+ enable_graph=ctx.ENABLE_GRAPH,
500
+ knowledge_graph_getter=lambda: ctx.KNOWLEDGE_GRAPH,
501
+ )
502
+ ctx.adopt(hooks_runtime, "HOOKS_REGISTRY", "LOCAL_KG_WATCHER")
503
+
504
+ persistence_runtime = build_persistence_runtime(
505
+ data_dir=ctx.DATA_DIR,
506
+ base_dir=ctx.BASE_DIR,
507
+ enable_graph=ctx.ENABLE_GRAPH,
508
+ knowledge_graph=ctx.KNOWLEDGE_GRAPH,
509
+ hooks_registry=ctx.HOOKS_REGISTRY,
510
+ history_file=ctx.HISTORY_FILE,
511
+ conversations=ctx.CONVERSATIONS,
512
+ user_id_for_email=ctx.user_id_for_email,
513
+ audit=lambda action, detail, user: ctx.append_audit_event(
514
+ action, user_email=user, **detail
515
+ ),
516
+ )
517
+ ctx.adopt(
518
+ persistence_runtime,
519
+ "REALTIME_BUS",
520
+ "WORKSPACE_OS",
521
+ "WORKSPACE_SERVICE",
522
+ "INVITATION_STORE",
523
+ "PLUGIN_REGISTRY",
524
+ "TEMPLATE_CATALOG",
525
+ "AGENT_REGISTRY",
526
+ "MEMORY_SERVICE",
527
+ "BRAIN_INTELLIGENCE",
528
+ "AUTOMATION_INTELLIGENCE",
529
+ "INGESTION_PIPELINE",
530
+ "MULTIMODAL_PORTS",
531
+ "DEVICE_IDENTITY",
532
+ "KG_PORTABILITY",
533
+ "FUNNEL_METRICS",
534
+ )
535
+
536
+ # History reads/clears. The write path (save_to_history above) stays a
537
+ # closure because it is bound to redaction/audit/ingestion.
538
+ history_query_runtime = build_history_query_runtime(
539
+ conversations=ctx.CONVERSATIONS,
540
+ workspace_service=ctx.WORKSPACE_SERVICE,
541
+ require_auth=ctx.REQUIRE_AUTH,
542
+ logging=logging,
543
+ )
544
+ ctx.adopt(
545
+ history_query_runtime,
546
+ "_history_allowed_workspaces_for",
547
+ "_history_include_legacy_global",
548
+ "get_history",
549
+ "conversation_title",
550
+ "group_history_conversations",
551
+ "get_conversation_messages",
552
+ "clear_history",
553
+ "clear_conversation",
554
+ )
555
+