ltcai 10.3.0 → 10.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 (143) hide show
  1. package/README.md +37 -36
  2. package/docs/CHANGELOG.md +77 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MYPY_BACKLOG.md +59 -92
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/SURFACE_PARITY.md +33 -6
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/context.py +23 -3
  14. package/lattice_brain/graph/_kg_common.py +164 -9
  15. package/lattice_brain/graph/_kg_contract.py +223 -0
  16. package/lattice_brain/graph/_kg_fsutil.py +1 -1
  17. package/lattice_brain/graph/curator.py +4 -4
  18. package/lattice_brain/graph/discovery.py +12 -1
  19. package/lattice_brain/graph/discovery_index.py +12 -1
  20. package/lattice_brain/graph/documents.py +17 -2
  21. package/lattice_brain/graph/ingest.py +12 -1
  22. package/lattice_brain/graph/proactive.py +9 -5
  23. package/lattice_brain/graph/projection.py +12 -1
  24. package/lattice_brain/graph/provenance.py +12 -1
  25. package/lattice_brain/graph/retrieval.py +12 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +12 -1
  27. package/lattice_brain/graph/retrieval_reads.py +12 -1
  28. package/lattice_brain/graph/retrieval_vector.py +12 -1
  29. package/lattice_brain/graph/schema.py +6 -2
  30. package/lattice_brain/graph/write_master.py +12 -1
  31. package/lattice_brain/ingestion.py +1 -1
  32. package/lattice_brain/quality.py +5 -5
  33. package/lattice_brain/runtime/contracts.py +2 -1
  34. package/lattice_brain/runtime/multi_agent.py +1 -1
  35. package/lattice_brain/workflow.py +10 -7
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/admin.py +2 -2
  38. package/latticeai/api/chat.py +19 -15
  39. package/latticeai/api/chat_agent_http.py +19 -7
  40. package/latticeai/api/chat_helpers.py +9 -8
  41. package/latticeai/api/chat_stream.py +2 -2
  42. package/latticeai/api/computer_use.py +2 -2
  43. package/latticeai/api/hooks.py +4 -2
  44. package/latticeai/api/local_files.py +8 -4
  45. package/latticeai/api/mcp.py +2 -2
  46. package/latticeai/api/models.py +5 -5
  47. package/latticeai/api/permissions.py +2 -2
  48. package/latticeai/api/search.py +5 -3
  49. package/latticeai/api/security_dashboard.py +3 -5
  50. package/latticeai/api/setup.py +3 -3
  51. package/latticeai/api/static_routes.py +7 -2
  52. package/latticeai/api/tools.py +9 -4
  53. package/latticeai/api/workspace.py +25 -22
  54. package/latticeai/app_factory.py +132 -1322
  55. package/latticeai/cli/entrypoint.py +1 -1
  56. package/latticeai/core/agent.py +28 -14
  57. package/latticeai/core/audit.py +5 -3
  58. package/latticeai/core/embedding_providers.py +19 -6
  59. package/latticeai/core/legacy_compatibility.py +1 -1
  60. package/latticeai/core/marketplace.py +2 -2
  61. package/latticeai/core/mcp_catalog.py +3 -1
  62. package/latticeai/core/model_compat.py +1 -1
  63. package/latticeai/core/plugins.py +1 -1
  64. package/latticeai/core/project_sessions.py +1 -1
  65. package/latticeai/core/run_explain.py +6 -4
  66. package/latticeai/core/workspace_os.py +0 -4
  67. package/latticeai/core/workspace_os_constants.py +1 -1
  68. package/latticeai/core/workspace_os_state.py +1 -1
  69. package/latticeai/core/workspace_snapshots.py +3 -3
  70. package/latticeai/integrations/telegram_bot.py +52 -3
  71. package/latticeai/models/router.py +61 -34
  72. package/latticeai/runtime/access_runtime.py +3 -1
  73. package/latticeai/runtime/build_phases.py +1417 -0
  74. package/latticeai/runtime/review_wiring.py +6 -1
  75. package/latticeai/runtime/runtime_context.py +276 -0
  76. package/latticeai/runtime/sso_config_runtime.py +1 -1
  77. package/latticeai/runtime/stages.py +8 -2
  78. package/latticeai/services/app_context.py +23 -0
  79. package/latticeai/services/architecture_readiness.py +1 -1
  80. package/latticeai/services/brain_intelligence.py +4 -2
  81. package/latticeai/services/cloud_streaming.py +5 -3
  82. package/latticeai/services/command_center.py +1 -1
  83. package/latticeai/services/evidence_actions.py +2 -1
  84. package/latticeai/services/hybrid_chat.py +1 -1
  85. package/latticeai/services/local_knowledge.py +25 -9
  86. package/latticeai/services/memory_service.py +8 -4
  87. package/latticeai/services/model_catalog.py +12 -12
  88. package/latticeai/services/model_engines.py +15 -17
  89. package/latticeai/services/model_loading.py +1 -1
  90. package/latticeai/services/model_runtime.py +31 -33
  91. package/latticeai/services/multimodal_streaming.py +10 -4
  92. package/latticeai/services/openai_compatible_adapter.py +4 -3
  93. package/latticeai/services/product_readiness.py +1 -1
  94. package/latticeai/services/run_executor.py +12 -3
  95. package/latticeai/services/search_service.py +1 -1
  96. package/latticeai/services/triggers.py +1 -1
  97. package/latticeai/setup/auto_setup.py +1 -1
  98. package/latticeai/setup/wizard.py +12 -3
  99. package/latticeai/tools/computer.py +3 -1
  100. package/latticeai/tools/documents.py +2 -2
  101. package/package.json +1 -1
  102. package/scripts/check_current_release_docs.mjs +1 -1
  103. package/src-tauri/Cargo.lock +1 -1
  104. package/src-tauri/Cargo.toml +1 -1
  105. package/src-tauri/tauri.conf.json +1 -1
  106. package/static/app/asset-manifest.json +36 -36
  107. package/static/app/assets/{Act-DrKIGZeJ.js → Act-BK__Y5VE.js} +1 -1
  108. package/static/app/assets/{AdminConsole-CEV1VefA.js → AdminConsole-ZrTyN8_F.js} +1 -1
  109. package/static/app/assets/{Brain-BZSXnwWU.js → Brain-CYHDXpPB.js} +1 -1
  110. package/static/app/assets/{BrainHome-CEBd86k6.js → BrainHome-bbJulb5g.js} +1 -1
  111. package/static/app/assets/{BrainSignals-O4s6R7av.js → BrainSignals-CesSPtUP.js} +1 -1
  112. package/static/app/assets/{Capture-AGbOxsg-.js → Capture-p5upXmz2.js} +1 -1
  113. package/static/app/assets/{CommandPalette-CeqBjCn2.js → CommandPalette-0zkyC0RY.js} +1 -1
  114. package/static/app/assets/{Library-CXnbNiQW.js → Library-Dt-vABI2.js} +1 -1
  115. package/static/app/assets/{LivingBrain-D9Iei2ZJ.js → LivingBrain-Bz-h6LBH.js} +1 -1
  116. package/static/app/assets/{ProductFlow-JlXiDfMM.js → ProductFlow-DAZ7DJCE.js} +1 -1
  117. package/static/app/assets/{ReviewCard-QQly2bTV.js → ReviewCard-Bv0WGbY6.js} +1 -1
  118. package/static/app/assets/{System-Bp9ZY4ER.js → System-BhrJQSD_.js} +1 -1
  119. package/static/app/assets/{activity-CxRPOQ2F.js → activity-AolpZJCH.js} +1 -1
  120. package/static/app/assets/{bot-1bcsWB2s.js → bot-CsVsT2nd.js} +1 -1
  121. package/static/app/assets/{brain-CxaeKCRM.js → brain-vuppco5y.js} +1 -1
  122. package/static/app/assets/{button-ZYFqGan7.js → button-1BP1PJZ3.js} +1 -1
  123. package/static/app/assets/{circle-pause-B5NOIqvk.js → circle-pause-5paeCOtq.js} +1 -1
  124. package/static/app/assets/{circle-play-BiJVFXf7.js → circle-play-By8mFYRV.js} +1 -1
  125. package/static/app/assets/{cpu-Vvq0OKTr.js → cpu-DvUQxdKe.js} +1 -1
  126. package/static/app/assets/{download-SEcXzkx1.js → download-CFqYG90a.js} +1 -1
  127. package/static/app/assets/{folder-open-Dw1ff_Qa.js → folder-open-CVSnp-Dp.js} +1 -1
  128. package/static/app/assets/{hard-drive-B00fR7_r.js → hard-drive-Ckr4Vd6-.js} +1 -1
  129. package/static/app/assets/{index-Dm9VXl1o.js → index--0zKF5Ow.js} +3 -3
  130. package/static/app/assets/{input-DtdWYQEa.js → input-D-JKquXU.js} +1 -1
  131. package/static/app/assets/{network-CpjBu0kI.js → network-DB88FZ2T.js} +1 -1
  132. package/static/app/assets/{primitives-CwsudBwc.js → primitives-D8U9OaIo.js} +1 -1
  133. package/static/app/assets/search-B8jsVTw_.js +1 -0
  134. package/static/app/assets/{shield-alert-CpjBc-gQ.js → shield-alert-DnsvO79P.js} +1 -1
  135. package/static/app/assets/{textarea-DUPyjHQs.js → textarea-4RchwbTa.js} +1 -1
  136. package/static/app/assets/{useFocusTrap-BWcMtCWH.js → useFocusTrap-CFJjy7cS.js} +1 -1
  137. package/static/app/assets/{useQuery-Dg5fPqTS.js → useQuery-3Reu-A4a.js} +1 -1
  138. package/static/app/assets/{users-BgcdTQGM.js → users-BTxYksxa.js} +1 -1
  139. package/static/app/assets/{utils-a3YXFtEO.js → utils-D02Vf-fw.js} +1 -1
  140. package/static/app/assets/{workspace-BMUEiXUm.js → workspace-CSJNgX-C.js} +1 -1
  141. package/static/app/index.html +3 -3
  142. package/static/sw.js +1 -1
  143. package/static/app/assets/search-CwEJWBEA.js +0 -1
@@ -10,60 +10,25 @@ and no file is created until ``create_app``/``build_runtime`` is called.
10
10
  ``build_runtime`` returns the full constructed namespace (every name the
11
11
  legacy module-level assembly exposed); ``latticeai.server_app`` proxies it
12
12
  lazily via module ``__getattr__`` for backwards compatibility.
13
+
14
+ The assembly itself lives in :mod:`latticeai.runtime.build_phases` as ten
15
+ ordered phases sharing a :class:`~latticeai.runtime.runtime_context.RuntimeContext`.
16
+ This module is now only the *orchestrator*: run the phases, then select the
17
+ explicit compatibility surface. It used to be a single 1,300-line ``_build``
18
+ closure, which worked because closures resolve free variables at call time —
19
+ the RuntimeContext preserves exactly that property while naming the state.
13
20
  """
14
21
 
15
22
  from __future__ import annotations
16
23
 
17
24
  import threading
18
25
  from dataclasses import dataclass
19
- from typing import TYPE_CHECKING, Any, Dict, List, Optional
26
+ from typing import TYPE_CHECKING, Any, Dict, Optional
20
27
 
21
- from latticeai.core.quiet import quiet
22
- from latticeai.runtime.access_runtime import build_access_runtime
23
- from latticeai.runtime.audit_runtime import build_audit_runtime
24
- from latticeai.runtime.bootstrap import build_session_runtime
25
- from latticeai.runtime.brain_runtime import build_brain_runtime
26
- from latticeai.runtime.chat_wiring import (
27
- build_chat_agent_runtime_from_context,
28
- build_interaction_contexts,
29
- maybe_build_telegram_chat_mirror,
30
- )
31
- from latticeai.runtime.config_runtime import build_config_runtime
32
- from latticeai.runtime.context_runtime import build_context_runtime
33
- from latticeai.runtime.history_runtime import build_history_query_runtime
34
- from latticeai.runtime.history_writer import HistoryWriterDeps, write_chat_turn
35
- from latticeai.runtime.hooks_runtime import (
36
- bind_builtin_hook_runners,
37
- bind_trigger_hook_runner,
38
- build_hooks_runtime,
39
- )
40
- from latticeai.runtime.lifespan_runtime import build_lifespan_runtime
41
- from latticeai.runtime.model_wiring import (
42
- configure_model_runtime_from_context,
43
- register_model_runtime_routers,
44
- )
28
+ from latticeai.runtime.build_phases import BUILD_PHASES
45
29
  from latticeai.runtime.namespace_runtime import RuntimeBundle, build_runtime_namespace
46
- from latticeai.runtime.network_config_runtime import build_vpc_runtime
47
- from latticeai.runtime.persistence_runtime import build_persistence_runtime
48
- from latticeai.runtime.platform_runtime_wiring import build_platform_automation_runtime
49
- from latticeai.runtime.platform_services_runtime import (
50
- build_brain_network,
51
- )
52
- from latticeai.runtime.review_wiring import build_review_run_now_runner
53
- from latticeai.runtime.router_registration import (
54
- build_auth_admin_security_router_bundle,
55
- build_router_bundle,
56
- build_static_routes_bundle,
57
- register_foundation_routers,
58
- register_health_and_model_routers,
59
- register_interaction_routers,
60
- register_platform_feature_routers,
61
- register_review_and_brain_tail_routers,
62
- )
63
- from latticeai.runtime.security_runtime import build_security_runtime
64
- from latticeai.runtime.sso_config_runtime import build_sso_config_runtime
65
- from latticeai.runtime.user_key_runtime import build_user_key_runtime
66
- from latticeai.runtime.web_runtime import build_web_runtime
30
+ from latticeai.runtime.router_registration import build_router_bundle
31
+ from latticeai.runtime.runtime_context import RuntimeContext
67
32
 
68
33
  if TYPE_CHECKING: # imports for annotations only — keep module import light
69
34
  from fastapi import FastAPI
@@ -71,70 +36,19 @@ if TYPE_CHECKING: # imports for annotations only — keep module import light
71
36
  from latticeai.core.config import Config
72
37
 
73
38
 
74
- def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
75
- """The legacy ``server_app`` assembly, moved verbatim into function scope.
39
+ def _legacy_exports(ctx: RuntimeContext) -> Dict[str, Any]:
40
+ """The explicit, allowlisted compatibility surface for ``server_app``.
76
41
 
77
- Heavy imports (mlx, the LLM router, knowledge graph, MCP registry, …) are
78
- deliberately *inside* this function so that importing the module performs
79
- no GPU init, no singleton construction, and no filesystem writes.
42
+ Two kinds of name appear here: values the phases produced (read off the
43
+ context) and pass-through re-exports of module functions that historical
44
+ callers reached through ``server_app``. Keeping them in one function makes
45
+ the surface countable — ``namespace_runtime`` rejects anything not on its
46
+ allowlist.
80
47
  """
81
- import logging
82
- import os
83
- import threading
84
- from pathlib import Path
85
-
86
- try:
87
- import mlx.core as mx
88
- mx.set_default_device(mx.gpu)
89
- print("✅ MLX Metal context initialized in main thread.")
90
- except Exception as e:
91
- print(f"⚠️ MLX Metal context unavailable: {e}")
92
- mx = None
93
- import uvicorn
94
- from fastapi import HTTPException, Request
95
- from pydantic import BaseModel
96
-
97
- from lattice_brain.graph.runtime import set_llm_router
98
- from lattice_brain.graph.schema import set_embed_dim
99
- from lattice_brain.ingestion import IngestionItem
100
- from lattice_brain.storage import storage_from_env
101
- from latticeai.api.admin import create_admin_router
102
- from latticeai.api.agent_registry import create_agent_registry_router
103
- from latticeai.api.agents import create_agents_router
104
- from latticeai.api.auth import create_auth_router
105
- from latticeai.api.automation_intelligence import (
106
- create_automation_intelligence_router,
107
- )
108
- from latticeai.api.brain_intelligence import create_brain_intelligence_router
109
- from latticeai.api.browser import create_browser_router
110
- from latticeai.api.change_proposals import create_change_proposals_router
111
- from latticeai.api.chat import build_recent_chat_context, create_chat_router
112
- from latticeai.api.command_center import create_command_center_router
113
- from latticeai.api.garden import create_garden_router
114
- from latticeai.api.health import create_health_router
115
- from latticeai.api.hooks import create_hooks_router
116
- from latticeai.api.invitations import create_invitations_router
117
- from latticeai.api.marketplace import create_marketplace_router
118
- from latticeai.api.memory import create_memory_router
119
- from latticeai.api.models import create_models_router
120
- from latticeai.api.network import create_network_router
121
-
122
- # ── v2 Agentic Workspace Platform layers ─────────────────────────────────────
123
- from latticeai.api.plugins import create_plugins_router
124
- from latticeai.api.portability import create_portability_router
125
- from latticeai.api.realtime import create_realtime_router
126
- from latticeai.api.search import create_search_router
127
48
  from latticeai.api.security_dashboard import (
128
49
  create_security_router as _create_security_router,
129
50
  )
130
- from latticeai.api.setup import create_setup_router
131
- from latticeai.api.static_routes import create_static_routes_router
132
- from latticeai.api.tools import create_tools_router
133
- from latticeai.api.workflow_designer import create_workflow_designer_router
134
- from latticeai.api.workspace import (
135
- _workspace_scope_from_request,
136
- create_workspace_router,
137
- )
51
+ from latticeai.api.workspace import _workspace_scope_from_request
138
52
  from latticeai.core.audit import (
139
53
  build_admin_audit_report as _build_admin_audit_report,
140
54
  )
@@ -144,1250 +58,125 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
144
58
  from latticeai.core.audit import (
145
59
  classify_sensitive_message as _classify_sensitive_message,
146
60
  )
147
- from latticeai.core.audit import (
148
- get_audit_log as _get_audit_log, # noqa: F401 - explicit legacy server_app export
149
- )
150
- from latticeai.core.embedding_providers import (
151
- resolve_embedder,
152
- resolve_embedding_profile,
153
- )
154
- from latticeai.core.enterprise import (
155
- capability_registry,
156
- )
157
- from latticeai.core.mcp_registry import (
158
- SKILLS_DIR,
159
- _fetch_skills_marketplace,
160
- create_mcp_install_state,
161
- install_skill,
162
- )
61
+ from latticeai.core.audit import get_audit_log as _get_audit_log
62
+ from latticeai.core.mcp_registry import _fetch_skills_marketplace
163
63
  from latticeai.core.model_compat import (
164
64
  list_cached_profiles as _list_compat_profiles,
165
65
  )
166
- from latticeai.core.policy import policy_matrix
167
- from latticeai.core.product_hardening import build_product_hardening_status
168
- from latticeai.core.security import (
169
- bytes_match_extension as _bytes_match_extension_impl,
170
- )
171
66
  from latticeai.core.security import (
172
67
  check_ip_rate_limit as _check_ip_rate_limit,
173
68
  )
174
- from latticeai.core.security import (
175
- client_ip as _client_ip_impl,
176
- )
177
- from latticeai.core.security import (
178
- enforce_rate_limit as _enforce_rate_limit,
179
- )
180
- from latticeai.core.security import (
181
- hash_password,
182
- verify_password,
183
- )
184
- from latticeai.core.security import (
185
- host_is_loopback as _host_is_loopback_impl,
186
- )
69
+ from latticeai.core.security import hash_password, verify_password
187
70
  from latticeai.core.security import (
188
71
  redact_secret_text as _redact_secret_text,
189
72
  )
190
- from latticeai.core.tool_registry import (
191
- TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF, # noqa: F401
192
- )
193
- from latticeai.core.users import (
194
- ensure_user_identity,
195
- load_users_file,
196
- migrate_knowledge_graph_identity,
197
- save_users_file,
198
- )
199
- from latticeai.core.users import (
200
- user_id_for_email as _user_id_for_email,
201
- )
202
- from latticeai.core.workspace_os import (
203
- WORKSPACE_OS_VERSION,
204
- remove_skill_directory,
205
- )
206
- from latticeai.models.router import LLMRouter, normalize_branding
207
- from latticeai.services.app_context import AppContext
208
- from latticeai.services.change_proposals import ChangeProposalService
209
- from latticeai.services.chat_service import ChatService
210
- from latticeai.services.command_center import CommandCenterService
73
+ from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF
74
+ from latticeai.core.users import user_id_for_email as _user_id_for_email
211
75
  from latticeai.services.model_runtime import (
212
- CLOUD_VERIFY_TTL_SECONDS,
213
76
  ENGINE_MODEL_CATALOG,
214
- LOCAL_SERVER_PROCESSES,
215
- MODEL_ENGINE_ALIASES,
216
- build_model_runtime,
217
- download_hf_model,
218
- ensure_ollama_server,
219
77
  filter_lower_family_versions,
220
- local_binary,
221
78
  normalize_local_model_request,
222
- sse_event,
223
- )
224
- from latticeai.services.p_reinforce import PReinforceGardener
225
-
226
- # The aliased names below form the explicit, allowlisted compatibility
227
- # surface consumed by historical ``server_app`` callers.
228
- from latticeai.services.tool_dispatch import ( # noqa: F401
229
- LOCAL_WRITE_BLOCKED_PREFIXES as _LOCAL_WRITE_BLOCKED_PREFIXES,
230
79
  )
231
80
  from latticeai.services.tool_dispatch import (
232
- TOOL_GOVERNANCE,
233
- build_agent_runtime,
234
- configure_tool_dispatch,
235
- get_tool_permission,
81
+ LOCAL_WRITE_BLOCKED_PREFIXES as _LOCAL_WRITE_BLOCKED_PREFIXES,
236
82
  )
83
+ from latticeai.services.tool_dispatch import TOOL_GOVERNANCE
237
84
  from latticeai.services.tool_dispatch import (
238
85
  TOOL_GOVERNANCE_DEFAULT as _TOOL_GOVERNANCE_DEFAULT,
239
86
  )
240
- from latticeai.services.tool_dispatch import (
241
- agent_risk as _agent_risk,
242
- )
243
- from latticeai.services.tool_dispatch import (
244
- check_tool_role as _check_tool_role,
245
- )
246
- from latticeai.services.tool_dispatch import (
247
- tool_response as _tool_response,
248
- )
249
- from latticeai.setup.wizard import get_recommendations, scan_environment
250
- from latticeai.tools import (
251
- ensure_agent_root,
252
- execute_tool,
253
- knowledge_save,
254
- resolve_workspace_path,
255
- )
256
-
257
- try:
258
- import keyring
259
- except Exception:
260
- keyring = None
261
-
262
-
263
- # ── App-level config — parsed once, in one place (latticeai.core.config) ──────
264
- # The module-level names below are kept as a compatibility surface for the rest
265
- # of server.py; all of them are now derived from a single CONFIG instance.
266
- _config_runtime = build_config_runtime(config)
267
- CONFIG = _config_runtime["CONFIG"]
268
- APP_VERSION = WORKSPACE_OS_VERSION
269
-
270
- # Forwarded headers (X-Forwarded-For / CF-Connecting-IP) are only honoured for
271
- # IP rate limiting when the direct peer is one of these trusted proxies. Empty by
272
- # default (local-first): the peer address is used and client-supplied headers are
273
- # ignored, so per-IP rate limits cannot be spoofed.
274
- APP_MODE = _config_runtime["APP_MODE"]
275
- IS_PUBLIC_MODE = _config_runtime["IS_PUBLIC_MODE"]
276
- DEFAULT_HOST = _config_runtime["DEFAULT_HOST"]
277
- DEFAULT_PORT = _config_runtime["DEFAULT_PORT"]
278
- def _host_is_loopback(host: str) -> bool:
279
- return _host_is_loopback_impl(host)
280
-
281
- ENABLE_TELEGRAM = _config_runtime["ENABLE_TELEGRAM"]
282
- ENABLE_GRAPH = _config_runtime["ENABLE_GRAPH"]
283
- AUTOLOAD_MODELS = _config_runtime["AUTOLOAD_MODELS"]
284
- MODEL_IDLE_UNLOAD_SECONDS = _config_runtime["MODEL_IDLE_UNLOAD_SECONDS"]
285
- ALLOW_MODEL_DOWNLOADS = _config_runtime["ALLOW_MODEL_DOWNLOADS"]
286
- MODEL_DOWNLOAD_TIMEOUT = _config_runtime["MODEL_DOWNLOAD_TIMEOUT"]
287
- ALLOW_LOCAL_MODELS = _config_runtime["ALLOW_LOCAL_MODELS"]
288
- REQUIRE_AUTH = _config_runtime["REQUIRE_AUTH"]
289
- ALLOW_PLAINTEXT_API_KEYS = _config_runtime["ALLOW_PLAINTEXT_API_KEYS"]
290
- CORS_ALLOW_NETWORK = _config_runtime["CORS_ALLOW_NETWORK"]
291
- CORS_EXTRA_ORIGINS = _config_runtime["CORS_EXTRA_ORIGINS"]
292
- PUBLIC_MODEL = _config_runtime["PUBLIC_MODEL"]
293
- LOCAL_MODEL = _config_runtime["LOCAL_MODEL"]
294
- LOCAL_DRAFT_MODEL = _config_runtime["LOCAL_DRAFT_MODEL"]
295
-
296
- _security_runtime = build_security_runtime(CONFIG)
297
-
298
- # ── SSO / OIDC config ─────────────────────────────────────────────────────────
299
- SSO_DISCOVERY_URL = _security_runtime["SSO_DISCOVERY_URL"]
300
- SSO_CLIENT_ID = _security_runtime["SSO_CLIENT_ID"]
301
- SSO_CLIENT_SECRET = _security_runtime["SSO_CLIENT_SECRET"]
302
- SSO_REDIRECT_URI = _security_runtime["SSO_REDIRECT_URI"]
303
- SSO_PROVIDER_NAME = _security_runtime["SSO_PROVIDER_NAME"]
304
- INVITE_CODE = _security_runtime["INVITE_CODE"]
305
- INVITE_COOKIE_SECRET = _security_runtime["INVITE_COOKIE_SECRET"]
306
- INVITE_GATE_ENABLED = _security_runtime["INVITE_GATE_ENABLED"]
307
- SECURE_COOKIES = _security_runtime["SECURE_COOKIES"]
308
-
309
- # SSO config + discovery seam is built below, after SSO_FILE is known
310
- # (latticeai.runtime.sso_config_runtime).
311
-
312
- # ── Password hashing — used directly from latticeai.core.security ──────────────
313
- # (hash_password / verify_password are imported above; no local wrapper needed)
314
- def verify_and_migrate_password(email: str, plain: str, stored: str, users: Dict) -> bool:
315
- """평문 비밀번호를 투명하게 해시로 마이그레이션. 마이그레이션 발생 시 audit log 남김."""
316
- if ":" in stored and len(stored) > 64:
317
- return verify_password(plain, stored)
318
- if plain == stored:
319
- users[email]["password"] = hash_password(plain)
320
- save_users(users)
321
- try:
322
- append_audit_event("password_migrated_from_plaintext", user_email=email)
323
- except Exception as e:
324
- logging.warning("audit log failed on password migration: %s", e)
325
- logging.info("Migrated plaintext password to scrypt hash for %s", email)
326
- return True
327
- return False
328
-
329
- # ── Session store — delegated to latticeai.runtime.bootstrap ──────────────────
330
- def _check_rate_limit(ip: str, action: str, max_calls: int, window_secs: float) -> None:
331
- _check_ip_rate_limit(ip, action, max_calls=max_calls, window_secs=window_secs)
332
-
333
- def _client_ip(request: Request) -> str:
334
- return _client_ip_impl(request)
335
-
336
- def user_id_for_email(email: Optional[str]) -> Optional[str]:
337
- return _user_id_for_email(load_users(), email)
338
-
339
- # Session token lifecycle (store + create/get/invalidate closures) lives in
340
- # the bootstrap seam; user_id_for_email is injected as the subject resolver.
341
- _session_runtime = build_session_runtime(user_id_resolver=user_id_for_email)
342
- _SESSION_TTL = _session_runtime["_SESSION_TTL"]
343
- _session_store = _session_runtime["_session_store"]
344
- create_session = _session_runtime["create_session"]
345
- get_session_email = _session_runtime["get_session_email"]
346
- invalidate_session = _session_runtime["invalidate_session"]
347
-
348
- # ── User Management Logic ──────────────────────────────────────────────────
349
- BASE_DIR = Path(__file__).resolve().parent.parent
350
- DATA_DIR = CONFIG.data_dir
351
- DATA_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
352
- try:
353
- DATA_DIR.chmod(0o700)
354
- except OSError:
355
- quiet()
356
- STATIC_DIR = CONFIG.static_dir
357
-
358
- USERS_FILE = DATA_DIR / "users.json"
359
- HISTORY_FILE = DATA_DIR / "chat_history.json"
360
- VPC_FILE = DATA_DIR / "vpc_config.json"
361
- AUDIT_FILE = DATA_DIR / "audit_log.json"
362
- SSO_FILE = DATA_DIR / "sso_config.json"
363
-
364
- # MCP state extracted to mcp_registry.create_mcp_install_state (server decomp)
365
- _mcp_state = create_mcp_install_state(DATA_DIR)
366
- load_mcp_installs = _mcp_state["load_mcp_installs"]
367
- mcp_public_item = _mcp_state["mcp_public_item"]
368
- recommend_mcps = _mcp_state["recommend_mcps"]
369
- install_mcp = _mcp_state["install_mcp"]
370
-
371
- # Resolve the configured embedding provider once at startup. Degrades to the
372
- # offline hash fallback when the requested provider is unavailable, while
373
- # recording the requested-vs-active provider for the Embeddings status surface.
374
- try:
375
- EMBEDDING_PROFILE = resolve_embedding_profile(CONFIG.embedding_profile)
376
- except ValueError as exc:
377
- logging.warning("Embedding profile ignored: %s", exc)
378
- EMBEDDING_PROFILE = {}
379
- _embedding_provider = CONFIG.embedding_provider
380
- _embedding_model = CONFIG.embedding_model or str(EMBEDDING_PROFILE.get("model") or "")
381
- _embedding_dim = CONFIG.embedding_dim or int(EMBEDDING_PROFILE.get("dimensions") or 0)
382
- if CONFIG.embedding_profile and CONFIG.embedding_provider in {"", "hash", "local", "fallback"}:
383
- _embedding_provider = str(EMBEDDING_PROFILE.get("provider") or CONFIG.embedding_provider)
384
-
385
- EMBEDDER = resolve_embedder(
386
- _embedding_provider,
387
- model=_embedding_model,
388
- base_url=CONFIG.embedding_base_url,
389
- api_key=CONFIG.embedding_api_key,
390
- dim=_embedding_dim,
391
- timeout=CONFIG.embedding_timeout,
392
- extra={"target": CONFIG.embedding_custom_target},
393
- probe=_embedding_provider not in {"", "hash", "local", "fallback"},
394
- )
395
- set_embed_dim(int(getattr(EMBEDDER, "dim", None) or _embedding_dim or 384))
396
- if EMBEDDER.fell_back:
397
- logging.warning("Embedding provider %s unavailable: %s", EMBEDDER.requested, EMBEDDER.detail)
398
- STORAGE_ENGINE = storage_from_env(os.environ, data_dir=DATA_DIR) if ENABLE_GRAPH else None
399
- _brain_runtime = build_brain_runtime(
400
- data_dir=DATA_DIR,
401
- history_file=HISTORY_FILE,
402
- enable_graph=ENABLE_GRAPH,
403
- embedder=EMBEDDER,
404
- storage_engine=STORAGE_ENGINE,
405
- )
406
- KNOWLEDGE_GRAPH = _brain_runtime["KNOWLEDGE_GRAPH"]
407
- # ── v4 durable conversation store: unbounded episodic memory in the same
408
- # SQLite file as the graph (kg_portability backup/restore covers it for
409
- # free). Legacy chat_history.json is imported once, idempotently, and the
410
- # file is left untouched on disk as the import source.
411
- CONVERSATIONS = _brain_runtime["CONVERSATIONS"]
412
-
413
- def save_to_history(
414
- role: str,
415
- message: str,
416
- user_email: Optional[str] = None,
417
- user_nickname: Optional[str] = None,
418
- source: Optional[str] = None,
419
- conversation_id: Optional[str] = None,
420
- workspace_id: Optional[str] = None,
421
- ) -> None:
422
- """Persist one chat turn. Logic lives in runtime/history_writer.py.
423
-
424
- This stays a closure on purpose: `append_audit_event`,
425
- `classify_sensitive_message` and `redact_secret_text` are bound further
426
- down this composition root, and a closure resolves them at call time.
427
- Hoisting the construction instead would reorder the whole function.
428
- """
429
- write_chat_turn(
430
- role,
431
- message,
432
- user_email=user_email,
433
- user_nickname=user_nickname,
434
- source=source,
435
- conversation_id=conversation_id,
436
- workspace_id=workspace_id,
437
- deps=HistoryWriterDeps(
438
- conversations=CONVERSATIONS,
439
- append_audit_event=append_audit_event,
440
- classify_sensitive_message=classify_sensitive_message,
441
- redact_secret_text=redact_secret_text,
442
- normalize_branding=normalize_branding,
443
- ingestion_pipeline=INGESTION_PIPELINE,
444
- ingestion_item_factory=IngestionItem,
445
- enable_graph=ENABLE_GRAPH,
446
- knowledge_graph=KNOWLEDGE_GRAPH,
447
- ),
448
- )
449
-
450
-
451
-
452
- # Hooks registry is constructed here (ahead of the watcher) so folder-watch
453
- # reindexes can fire the pre_index/post_index lifecycle hooks. The registry
454
- # + watcher pair is assembled behind the hooks_runtime seam.
455
- _hooks_runtime = build_hooks_runtime(
456
- data_dir=DATA_DIR,
457
- enable_graph=ENABLE_GRAPH,
458
- knowledge_graph_getter=lambda: KNOWLEDGE_GRAPH,
459
- )
460
- HOOKS_REGISTRY = _hooks_runtime["HOOKS_REGISTRY"]
461
- LOCAL_KG_WATCHER = _hooks_runtime["LOCAL_KG_WATCHER"]
462
- # ── Persistence/service graph: workspace store, realtime feed, plugin/memory
463
- # registries, ingestion pipeline, device identity, and portability services.
464
- _persistence_runtime = build_persistence_runtime(
465
- data_dir=DATA_DIR,
466
- base_dir=BASE_DIR,
467
- enable_graph=ENABLE_GRAPH,
468
- knowledge_graph=KNOWLEDGE_GRAPH,
469
- hooks_registry=HOOKS_REGISTRY,
470
- history_file=HISTORY_FILE,
471
- conversations=CONVERSATIONS,
472
- user_id_for_email=user_id_for_email,
473
- audit=lambda action, detail, user: append_audit_event(action, user_email=user, **detail),
474
- )
475
- REALTIME_BUS = _persistence_runtime["REALTIME_BUS"]
476
- WORKSPACE_OS = _persistence_runtime["WORKSPACE_OS"]
477
- WORKSPACE_SERVICE = _persistence_runtime["WORKSPACE_SERVICE"]
478
- INVITATION_STORE = _persistence_runtime["INVITATION_STORE"]
479
- PLUGIN_REGISTRY = _persistence_runtime["PLUGIN_REGISTRY"]
480
- TEMPLATE_CATALOG = _persistence_runtime["TEMPLATE_CATALOG"]
481
- AGENT_REGISTRY = _persistence_runtime["AGENT_REGISTRY"]
482
- MEMORY_SERVICE = _persistence_runtime["MEMORY_SERVICE"]
483
- BRAIN_INTELLIGENCE = _persistence_runtime["BRAIN_INTELLIGENCE"]
484
- AUTOMATION_INTELLIGENCE = _persistence_runtime["AUTOMATION_INTELLIGENCE"]
485
- INGESTION_PIPELINE = _persistence_runtime["INGESTION_PIPELINE"]
486
- DEVICE_IDENTITY = _persistence_runtime["DEVICE_IDENTITY"]
487
- KG_PORTABILITY = _persistence_runtime["KG_PORTABILITY"]
488
- FUNNEL_METRICS = _persistence_runtime["FUNNEL_METRICS"]
489
-
490
- def _require_graph():
491
- if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
492
- raise HTTPException(status_code=404, detail="지식 그래프가 비활성화되어 있습니다. LATTICEAI_ENABLE_GRAPH=true 설정 후 다시 시도해 주세요.")
493
-
494
- class UserRegister(BaseModel):
495
- email: str
496
- password: str
497
- name: str
498
- nickname: str
499
-
500
- class UserLogin(BaseModel):
501
- email: str
502
- password: str
503
-
504
- class AdminUserUpdate(BaseModel):
505
- role: Optional[str] = None
506
- disabled: Optional[bool] = None
507
-
508
- class VpcConfigUpdate(BaseModel):
509
- provider: Optional[str] = None
510
- region: Optional[str] = None
511
- cidr_block: Optional[str] = None
512
- private_subnets: Optional[List[str]] = None
513
- endpoint: Optional[str] = None
514
- vpn_status: Optional[str] = None
515
- peering_status: Optional[str] = None
516
- notes: Optional[str] = None
517
-
518
- class SsoConfigUpdate(BaseModel):
519
- enabled: Optional[bool] = None
520
- provider_name: Optional[str] = None
521
- discovery_url: Optional[str] = None
522
- client_id: Optional[str] = None
523
- client_secret: Optional[str] = None
524
- redirect_uri: Optional[str] = None
525
- scopes: Optional[str] = None
526
-
527
- # SSO config + OIDC discovery seam. One closure owns config load/save and the
528
- # discovery cache, so save_sso_config now actually invalidates discovery.
529
- _sso_runtime = build_sso_config_runtime(
530
- sso_file=SSO_FILE,
531
- discovery_url=SSO_DISCOVERY_URL,
532
- client_id=SSO_CLIENT_ID,
533
- client_secret=SSO_CLIENT_SECRET,
534
- redirect_uri=SSO_REDIRECT_URI,
535
- provider_name=SSO_PROVIDER_NAME,
536
- logging=logging,
537
- )
538
- _sso_env_defaults = _sso_runtime["_sso_env_defaults"]
539
- get_sso_settings = _sso_runtime["get_sso_settings"]
540
- public_sso_config = _sso_runtime["public_sso_config"]
541
- save_sso_config = _sso_runtime["save_sso_config"]
542
- _get_sso_discovery = _sso_runtime["_get_sso_discovery"]
543
- _sso_states = _sso_runtime["_sso_states"]
544
-
545
- # MCP/skill request models moved to latticeai.api.mcp (v1.3.0).
546
- # VPC network-profile config → latticeai.runtime.network_config_runtime.
547
- _vpc_runtime = build_vpc_runtime(vpc_file=VPC_FILE, logging=logging)
548
- load_vpc_config = _vpc_runtime["load_vpc_config"]
549
- save_vpc_config = _vpc_runtime["save_vpc_config"]
550
-
551
-
552
- def load_users():
553
- users = load_users_file(USERS_FILE)
554
- email_to_id = {
555
- email: user.get("id")
556
- for email, user in users.items()
557
- if isinstance(user, dict) and user.get("id")
558
- }
559
- try:
560
- migrate_knowledge_graph_identity(DATA_DIR / "knowledge_graph.sqlite", email_to_id)
561
- except Exception as exc:
562
- logging.warning("knowledge graph identity migration skipped: %s", exc)
563
- try:
564
- WORKSPACE_OS.migrate_workspace_identities(email_to_id)
565
- except Exception as exc:
566
- logging.warning("workspace identity migration skipped: %s", exc)
567
- return users
568
-
569
- def save_users(users):
570
- save_users_file(USERS_FILE, users)
571
-
572
-
573
- _history_lock = threading.Lock()
574
-
575
- # audit build moved after redact_secret_text is defined (see below)
576
-
577
-
578
-
579
- def redact_secret_text(text: str) -> str:
580
- return _redact_secret_text(text)
581
-
582
- # Build audit now that redact is available
583
- _audit_rt = build_audit_runtime(
584
- audit_file=AUDIT_FILE,
585
- logging=logging,
586
- redact_fn=redact_secret_text,
587
- )
588
- get_audit_log = _audit_rt["get_audit_log"]
589
- append_audit_event = _audit_rt["append_audit_event"]
590
-
591
- # History query/clear seam (scope resolution, get_history, grouping, clears)
592
- # → latticeai.runtime.history_runtime. save_to_history (the write path) stays
593
- # inline above because it is bound to redaction/audit/ingestion.
594
- _history_query_runtime = build_history_query_runtime(
595
- conversations=CONVERSATIONS,
596
- workspace_service=WORKSPACE_SERVICE,
597
- require_auth=REQUIRE_AUTH,
598
- logging=logging,
599
- )
600
- _history_allowed_workspaces_for = _history_query_runtime["_history_allowed_workspaces_for"]
601
- _history_include_legacy_global = _history_query_runtime["_history_include_legacy_global"]
602
- get_history = _history_query_runtime["get_history"]
603
- conversation_title = _history_query_runtime["conversation_title"]
604
- group_history_conversations = _history_query_runtime["group_history_conversations"]
605
- get_conversation_messages = _history_query_runtime["get_conversation_messages"]
606
- clear_history = _history_query_runtime["clear_history"]
607
- clear_conversation = _history_query_runtime["clear_conversation"]
608
-
609
- _access_runtime = build_access_runtime(
610
- config=CONFIG,
611
- require_auth=REQUIRE_AUTH,
612
- http_exception=HTTPException,
613
- request_type=Request,
614
- load_users=load_users,
615
- get_session_email=get_session_email,
616
- user_id_for_email=_user_id_for_email,
617
- )
618
- get_user_role = _access_runtime["get_user_role"]
619
- _extract_bearer_token = _access_runtime["_extract_bearer_token"]
620
- get_current_user = _access_runtime["get_current_user"]
621
- require_user = _access_runtime["require_user"]
622
- require_admin = _access_runtime["require_admin"]
623
- public_user = _access_runtime["public_user"]
624
-
625
-
626
- # ── Rate limiting & file validation — delegated to latticeai.core.security ────
627
- _RATE_LIMIT_ENABLED = CONFIG.rate_limit_enabled
628
-
629
- def enforce_rate_limit(email: str, bucket_key: str) -> None:
630
- _enforce_rate_limit(email, bucket_key, enabled=_RATE_LIMIT_ENABLED)
631
-
632
- def _bytes_match_extension(data: bytes, ext: str) -> bool:
633
- return _bytes_match_extension_impl(data, ext)
634
-
635
- # Local file-access approval lives entirely in
636
- # ``latticeai.api.permissions.PermissionGateway`` (token hash-at-rest,
637
- # persisted approval queue, Discord notifications). The historical inline
638
- # copy that used to sit here was app-dead — no route referenced it — so it
639
- # was removed to keep this factory a single wiring path.
640
-
641
- _user_key_runtime = build_user_key_runtime(
642
- load_users=load_users,
643
- save_users=save_users,
644
- ensure_user_identity=ensure_user_identity,
645
- keyring=keyring,
646
- allow_plaintext_api_keys=ALLOW_PLAINTEXT_API_KEYS,
647
- logging=logging,
648
- http_exception=HTTPException,
649
- )
650
- get_history_user = _user_key_runtime["get_history_user"]
651
- get_user_api_key = _user_key_runtime["get_user_api_key"]
652
- set_user_api_key = _user_key_runtime["set_user_api_key"]
653
-
654
- # Chat service owns persistence and trace behavior after its user-key
655
- # dependencies are available.
656
- CHAT_SERVICE = ChatService(
657
- store=WORKSPACE_OS,
658
- get_history=get_history,
659
- save_to_history=save_to_history,
660
- get_history_user=get_history_user,
661
- )
662
-
663
- # ── Sensitivity analysis — delegated to latticeai.core.audit ──────────────────
664
- def classify_sensitive_message(item: Dict, index: int) -> Dict:
665
- return _classify_sensitive_message(item, index)
666
-
667
- def build_sensitivity_report(history: List[Dict]) -> Dict:
668
- return _build_sensitivity_report(history)
669
-
670
-
671
- # ── Admin audit report — delegated to latticeai.core.audit ───────────────────
672
- def build_admin_audit_report(users: Dict, audit_events: Optional[List[Dict]] = None) -> Dict:
673
- graph_stats = None
674
- try:
675
- if ENABLE_GRAPH and KNOWLEDGE_GRAPH:
676
- graph_stats = KNOWLEDGE_GRAPH.stats()
677
- except Exception:
678
- quiet()
679
- return _build_admin_audit_report(
680
- AUDIT_FILE, users,
681
- get_user_role=get_user_role,
682
- graph_stats=graph_stats,
683
- audit_events=audit_events,
684
- )
685
-
686
- router = LLMRouter()
687
- set_llm_router(router)
688
- configure_tool_dispatch(load_users=load_users, get_user_role=get_user_role)
689
- # v4 garden absorption: the vault is the user-owned markdown mirror; the
690
- # brain is authoritative. Existing notes import idempotently at startup
691
- # (content-hash dedup — re-runs are no-ops), and garden context queries
692
- # the brain instead of rescanning the vault per chat message.
693
- gardener = PReinforceGardener(
694
- ingestion_pipeline=INGESTION_PIPELINE if ENABLE_GRAPH else None,
695
- knowledge_graph=KNOWLEDGE_GRAPH,
696
- )
697
- if ENABLE_GRAPH:
698
- try:
699
- _garden_import = gardener.import_vault()
700
- if _garden_import.get("failed"):
701
- logging.warning("garden vault import: %s notes failed to ingest", _garden_import["failed"])
702
- except Exception as exc:
703
- logging.warning("garden vault import skipped: %s", exc)
704
-
705
- _lifespan_runtime = build_lifespan_runtime(
706
- app_mode=APP_MODE,
707
- enable_telegram=ENABLE_TELEGRAM,
708
- autoload_models=AUTOLOAD_MODELS,
709
- is_public_mode=IS_PUBLIC_MODE,
710
- public_model=PUBLIC_MODEL,
711
- allow_local_models=ALLOW_LOCAL_MODELS,
712
- local_model=LOCAL_MODEL,
713
- local_draft_model=LOCAL_DRAFT_MODEL,
714
- model_idle_unload_seconds=MODEL_IDLE_UNLOAD_SECONDS,
715
- model_router=router,
716
- local_kg_watcher=LOCAL_KG_WATCHER,
717
- local_server_processes=LOCAL_SERVER_PROCESSES,
718
- logger=logging,
719
- )
720
- _spawn = _lifespan_runtime["_spawn"]
721
- lifespan = _lifespan_runtime["lifespan"]
722
-
723
- _web_runtime = build_web_runtime(
724
- app_mode=APP_MODE,
725
- app_version=APP_VERSION,
726
- lifespan=lifespan,
727
- default_host=DEFAULT_HOST,
728
- default_port=DEFAULT_PORT,
729
- cors_extra_origins=CORS_EXTRA_ORIGINS,
730
- cors_allow_network=CORS_ALLOW_NETWORK,
731
- static_dir=STATIC_DIR,
732
- )
733
- app = _web_runtime["app"]
734
- ensure_agent_root()
735
-
736
- OPEN_REGISTRATION = CONFIG.open_registration
737
- _model_runtime_service = configure_model_runtime_from_context(
738
- build_model_runtime=build_model_runtime,
739
- router=router,
740
- APP_MODE=APP_MODE,
741
- DEFAULT_HOST=DEFAULT_HOST,
742
- DEFAULT_PORT=DEFAULT_PORT,
743
- DATA_DIR=DATA_DIR,
744
- BASE_DIR=BASE_DIR,
745
- ENABLE_TELEGRAM=ENABLE_TELEGRAM,
746
- ENABLE_GRAPH=ENABLE_GRAPH,
747
- AUTOLOAD_MODELS=AUTOLOAD_MODELS,
748
- MODEL_IDLE_UNLOAD_SECONDS=MODEL_IDLE_UNLOAD_SECONDS,
749
- ALLOW_MODEL_DOWNLOADS=ALLOW_MODEL_DOWNLOADS,
750
- MODEL_DOWNLOAD_TIMEOUT=MODEL_DOWNLOAD_TIMEOUT,
751
- ALLOW_LOCAL_MODELS=ALLOW_LOCAL_MODELS,
752
- REQUIRE_AUTH=REQUIRE_AUTH,
753
- INVITE_GATE_ENABLED=INVITE_GATE_ENABLED,
754
- ALLOW_PLAINTEXT_API_KEYS=ALLOW_PLAINTEXT_API_KEYS,
755
- CORS_ALLOW_NETWORK=CORS_ALLOW_NETWORK,
756
- PUBLIC_MODEL=PUBLIC_MODEL,
757
- LOCAL_MODEL=LOCAL_MODEL,
758
- IS_PUBLIC_MODE=IS_PUBLIC_MODE,
759
- keyring=keyring,
760
- get_current_user=get_current_user,
761
- get_user_api_key=get_user_api_key,
762
- )
763
- _static_routes_bundle = build_static_routes_bundle(
764
- create_static_routes_router=create_static_routes_router,
765
- static_dir=STATIC_DIR,
766
- invite_gate_enabled=INVITE_GATE_ENABLED,
767
- invite_code=INVITE_CODE,
768
- invite_cookie_secret=INVITE_COOKIE_SECRET,
769
- secure_cookies=SECURE_COOKIES,
770
- app_mode=APP_MODE,
771
- model_router=router,
772
- require_user=require_user,
773
- )
774
- STATIC_ROUTES = _static_routes_bundle["STATIC_ROUTES"]
775
- ui_file_response = _static_routes_bundle["ui_file_response"]
776
- local_sysinfo = _static_routes_bundle["local_sysinfo"]
777
- invite_authorized = _static_routes_bundle["invite_authorized"]
778
-
779
- # ── Auth & Admin routers (latticeai.api) ─────────────────────────────────────
780
- _foundation_router_bundle = build_auth_admin_security_router_bundle(
781
- create_auth_router=create_auth_router,
782
- load_users=load_users,
783
- save_users=save_users,
784
- hash_password=hash_password,
785
- verify_and_migrate_password=verify_and_migrate_password,
786
- create_session=create_session,
787
- get_session_email=get_session_email,
788
- invalidate_session=invalidate_session,
789
- extract_bearer_token=_extract_bearer_token,
790
- get_user_role=get_user_role,
791
- require_user=require_user,
792
- check_ip_rate_limit=_check_rate_limit,
793
- client_ip=_client_ip,
794
- get_sso_settings=get_sso_settings,
795
- get_sso_discovery=_get_sso_discovery,
796
- public_sso_config=public_sso_config,
797
- open_registration=OPEN_REGISTRATION,
798
- session_ttl=_SESSION_TTL,
799
- require_auth=REQUIRE_AUTH,
800
- secure_cookies=SECURE_COOKIES,
801
- invite_authorized=invite_authorized,
802
- ensure_identity=ensure_user_identity,
803
- create_admin_router=create_admin_router,
804
- require_admin=require_admin,
805
- get_history=get_history,
806
- get_audit_log=get_audit_log,
807
- audit_file=AUDIT_FILE,
808
- public_user=public_user,
809
- load_vpc_config=load_vpc_config,
810
- save_vpc_config=save_vpc_config,
811
- build_admin_audit_report=build_admin_audit_report,
812
- build_sensitivity_report=build_sensitivity_report,
813
- append_audit_event=append_audit_event,
814
- save_sso_config=save_sso_config,
815
- knowledge_graph=KNOWLEDGE_GRAPH,
816
- enable_graph=ENABLE_GRAPH,
817
- logger=logging,
818
- invite_code=INVITE_CODE,
819
- invite_gate_enabled=INVITE_GATE_ENABLED,
820
- default_port=DEFAULT_PORT,
821
- policy_matrix=policy_matrix,
822
- build_product_hardening_status=build_product_hardening_status,
823
- config=CONFIG,
824
- kg_portability=KG_PORTABILITY,
825
- device_identity=DEVICE_IDENTITY,
826
- create_invitations_router=create_invitations_router,
827
- invitation_store=INVITATION_STORE,
828
- workspace_service=WORKSPACE_SERVICE,
829
- user_id_for_email=user_id_for_email,
830
- create_security_router=_create_security_router,
831
- classify_sensitive_message=classify_sensitive_message,
832
- )
833
- auth_router = _foundation_router_bundle["auth_router"]
834
- admin_router = _foundation_router_bundle["admin_router"]
835
- invitations_router = _foundation_router_bundle["invitations_router"]
836
- security_router = _foundation_router_bundle["security_router"]
837
- _graph_stats_safe = _foundation_router_bundle["_graph_stats_safe"]
838
- _product_hardening_status = _foundation_router_bundle["_product_hardening_status"]
839
- _security_audit_events_safe = _foundation_router_bundle["_security_audit_events_safe"]
840
- _security_list_uploaded_files = _foundation_router_bundle["_security_list_uploaded_files"]
841
-
842
- # ── Static UI/status routes moved to latticeai.api.static_routes ──
843
-
844
- # ── Request / Response Models ──────────────────────────────────────────────────
845
-
846
- # ── Workspace OS API ──────────────────────────────────────────────────────────
847
-
848
- def _workspace_settings_payload() -> Dict:
849
- return {
850
- "mode": APP_MODE,
851
- "host": DEFAULT_HOST,
852
- "port": DEFAULT_PORT,
853
- "require_auth": REQUIRE_AUTH,
854
- "enable_graph": ENABLE_GRAPH,
855
- "allow_local_models": ALLOW_LOCAL_MODELS,
856
- "static_dir": str(STATIC_DIR),
857
- "data_dir": str(DATA_DIR),
858
- }
859
-
860
-
861
- def _workspace_models_payload() -> Dict:
862
- return {
863
- "current_model": router.current_model_id,
864
- "loaded_models": router.loaded_model_ids,
865
- "public_model": PUBLIC_MODEL,
866
- "local_model": LOCAL_MODEL,
867
- "local_draft_model": LOCAL_DRAFT_MODEL,
868
- }
869
-
870
-
871
- def _workspace_graph():
872
- return KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None
873
-
874
-
875
- _context_runtime = build_context_runtime(
876
- graph_store=_workspace_graph(),
877
- ingestion_pipeline=INGESTION_PIPELINE,
878
- memory_service=MEMORY_SERVICE,
879
- gardener=gardener,
880
- require_auth=REQUIRE_AUTH,
881
- allowed_scopes_for_user=lambda user_email: PLATFORM.allowed_scopes(user_email),
882
- )
883
- SEARCH_SERVICE = _context_runtime["SEARCH_SERVICE"]
884
- BRAIN_MEMORY = _context_runtime["BRAIN_MEMORY"]
885
- CONTEXT_ASSEMBLER = _context_runtime["CONTEXT_ASSEMBLER"]
886
- ARTIFACT_LEDGER = _context_runtime["ARTIFACT_LEDGER"]
887
- _scoped_hybrid_search = _context_runtime["_scoped_hybrid_search"]
888
-
889
-
890
- # ── Telegram chat mirror: registered only when ENABLE_TELEGRAM is truthy.
891
- # latticeai.api.chat no longer imports telegram_bot (a 45KB module that
892
- # mutates os.environ at import); it calls this injected callback instead.
893
- on_chat_message = maybe_build_telegram_chat_mirror(
894
- enable_telegram=ENABLE_TELEGRAM,
895
- spawn=_spawn,
896
- )
897
-
898
- def _recent_chat_context(
899
- limit: int = 10,
900
- include_image_missing_replies: bool = True,
901
- user_email: Optional[str] = None,
902
- conversation_id: Optional[str] = None,
903
- workspace_id: Optional[str] = None,
904
- ) -> str:
905
- return build_recent_chat_context(
906
- get_history=get_history,
907
- limit=limit,
908
- include_image_missing_replies=include_image_missing_replies,
909
- user_email=user_email,
910
- conversation_id=conversation_id,
911
- workspace_id=workspace_id,
912
- )
913
-
914
- CHAT_AGENT_RUNTIME = build_chat_agent_runtime_from_context(
915
- build_agent_runtime=build_agent_runtime,
916
- model_router=router,
917
- execute_tool=execute_tool,
918
- recent_chat_context=_recent_chat_context,
919
- clear_history=clear_history,
920
- knowledge_save=knowledge_save,
921
- audit=append_audit_event,
922
- hooks=HOOKS_REGISTRY,
923
- brain_memory=BRAIN_MEMORY,
924
- )
925
-
926
- # ── Typed dependency context (latticeai.services.app_context) ────────────────
927
- # One context object replaces the historical 25-30-kwarg router wiring.
928
- context = AppContext(
929
- config=CONFIG,
930
- data_dir=DATA_DIR,
931
- static_dir=STATIC_DIR,
932
- base_dir=BASE_DIR,
933
- skills_dir=SKILLS_DIR,
934
- model_router=router,
935
- workspace_store=WORKSPACE_OS,
936
- workspace_service=WORKSPACE_SERVICE,
937
- knowledge_graph=KNOWLEDGE_GRAPH,
938
- local_kg_watcher=LOCAL_KG_WATCHER,
939
- chat_service=CHAT_SERVICE,
940
- context_assembler=CONTEXT_ASSEMBLER,
941
- artifact_ledger=ARTIFACT_LEDGER,
942
- brain_memory=BRAIN_MEMORY,
943
- ingestion_pipeline=INGESTION_PIPELINE if ENABLE_GRAPH else None,
944
- chat_agent_runtime=CHAT_AGENT_RUNTIME,
945
- gardener=gardener,
946
- hooks=HOOKS_REGISTRY,
947
- realtime_bus=REALTIME_BUS,
948
- capability_registry=capability_registry,
949
- require_user=require_user,
950
- require_admin=require_admin,
951
- get_current_user=get_current_user,
952
- load_users=load_users,
953
- get_user_role=get_user_role,
954
- enforce_rate_limit=enforce_rate_limit,
955
- allowed_workspaces_for=_history_allowed_workspaces_for,
956
- append_audit_event=append_audit_event,
957
- get_audit_log=get_audit_log,
958
- get_history=get_history,
959
- get_history_user=get_history_user,
960
- save_to_history=save_to_history,
961
- clear_history=clear_history,
962
- clear_conversation=clear_conversation,
963
- group_history_conversations=group_history_conversations,
964
- get_conversation_messages=get_conversation_messages,
965
- conversation_title=conversation_title,
966
- enable_graph=ENABLE_GRAPH,
967
- require_graph=_require_graph,
968
- workspace_graph=_workspace_graph,
969
- graph_stats=_graph_stats_safe,
970
- workspace_models=_workspace_models_payload,
971
- workspace_settings=_workspace_settings_payload,
972
- scan_environment=scan_environment,
973
- local_sysinfo=local_sysinfo,
974
- get_recommendations=get_recommendations,
975
- fetch_skills_marketplace=_fetch_skills_marketplace,
976
- install_skill=install_skill,
977
- remove_skill_directory=remove_skill_directory,
978
- redact_secret_text=redact_secret_text,
979
- ui_file_response=ui_file_response,
980
- public_model=PUBLIC_MODEL,
981
- local_model=LOCAL_MODEL or "",
982
- on_chat_message=on_chat_message,
983
- funnel_metrics=FUNNEL_METRICS,
984
- )
985
- app.state.context = context
986
-
987
- register_foundation_routers(
988
- app,
989
- static_router=STATIC_ROUTES.router,
990
- auth_router=auth_router,
991
- admin_router=admin_router,
992
- invitations_router=invitations_router,
993
- security_router=security_router,
994
- create_workspace_router=create_workspace_router,
995
- context=context,
996
- )
997
-
998
-
999
- # ── v2 Agentic Workspace Platform: cross-system wiring ───────────────────────
1000
- _platform_automation_runtime = build_platform_automation_runtime(
1001
- model_router=router,
1002
- workspace_store=WORKSPACE_OS,
1003
- workspace_service=WORKSPACE_SERVICE,
1004
- plugin_registry=PLUGIN_REGISTRY,
1005
- get_current_user=get_current_user,
1006
- workspace_graph=_workspace_graph,
1007
- workspace_scope_from_request=_workspace_scope_from_request,
1008
- get_tool_permission=get_tool_permission,
1009
- hooks=HOOKS_REGISTRY,
1010
- agent_registry=AGENT_REGISTRY,
1011
- data_dir=DATA_DIR,
1012
- append_audit_event=append_audit_event,
1013
- memory_service=MEMORY_SERVICE,
1014
- tz_name=getattr(CONFIG, "timezone", None),
1015
- )
1016
- _llm_generate_sync = _platform_automation_runtime["_llm_generate_sync"]
1017
- PLATFORM = _platform_automation_runtime["PLATFORM"]
1018
- _automation_runtime = _platform_automation_runtime["_automation_runtime"]
1019
- REVIEW_QUEUE = _platform_automation_runtime["REVIEW_QUEUE"]
1020
- TRIGGER_SERVICE = _platform_automation_runtime["TRIGGER_SERVICE"]
1021
- AGENT_RUNTIME = _platform_automation_runtime["AGENT_RUNTIME"]
1022
- RUN_EXECUTOR = _platform_automation_runtime["RUN_EXECUTOR"]
1023
- bind_trigger_hook_runner(registry=HOOKS_REGISTRY, trigger_service=TRIGGER_SERVICE)
1024
- app.state.run_executor = RUN_EXECUTOR
1025
- app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
1026
- TRIGGER_SERVICE.start()
1027
-
1028
- # ── Hooks dispatch: bind real built-in runners ───────────────────────────────
1029
- bind_builtin_hook_runners(
1030
- registry=HOOKS_REGISTRY,
1031
- append_audit_event=append_audit_event,
1032
- get_tool_permission=get_tool_permission,
1033
- classify_sensitive_message=classify_sensitive_message,
1034
- )
1035
-
1036
- register_platform_feature_routers(
1037
- app,
1038
- create_plugins_router=create_plugins_router,
1039
- plugin_registry=PLUGIN_REGISTRY,
1040
- require_user=require_user,
1041
- require_admin=require_admin,
1042
- append_audit_event=append_audit_event,
1043
- platform=PLATFORM,
1044
- ui_file_response=ui_file_response,
1045
- static_dir=STATIC_DIR,
1046
- create_workflow_designer_router=create_workflow_designer_router,
1047
- store=WORKSPACE_OS,
1048
- get_current_user=get_current_user,
1049
- workspace_graph=_workspace_graph,
1050
- hooks=HOOKS_REGISTRY,
1051
- run_executor=RUN_EXECUTOR,
1052
- trigger_service=TRIGGER_SERVICE,
1053
- create_agents_router=create_agents_router,
1054
- agent_runtime=AGENT_RUNTIME,
1055
- create_marketplace_router=create_marketplace_router,
1056
- template_catalog=TEMPLATE_CATALOG,
1057
- create_realtime_router=create_realtime_router,
1058
- realtime_bus=REALTIME_BUS,
1059
- )
1060
- app.include_router(
1061
- create_automation_intelligence_router(
1062
- service=AUTOMATION_INTELLIGENCE,
1063
- store=WORKSPACE_OS,
1064
- require_user=require_user,
1065
- gate_read=PLATFORM.gate_read,
1066
- gate_write=PLATFORM.gate_write,
1067
- append_audit_event=append_audit_event,
1068
- workspace_graph=_workspace_graph,
1069
- run_executor=RUN_EXECUTOR,
1070
- review_queue=REVIEW_QUEUE,
1071
- )
1072
- )
1073
- # UX funnel metrics (backlog #16): admin-only runtime counters.
1074
- from latticeai.api.funnel_metrics import create_funnel_metrics_router
1075
-
1076
- app.include_router(
1077
- create_funnel_metrics_router(
1078
- service=FUNNEL_METRICS,
1079
- require_admin=require_admin,
1080
- )
1081
- )
1082
- COMMAND_CENTER = CommandCenterService(
1083
- conversation_store=CONVERSATIONS,
1084
- knowledge_graph=KNOWLEDGE_GRAPH,
1085
- store=WORKSPACE_OS,
1086
- search_service=SEARCH_SERVICE,
1087
- brain_intelligence=BRAIN_INTELLIGENCE,
1088
- automation_intelligence=AUTOMATION_INTELLIGENCE,
1089
- review_queue=REVIEW_QUEUE,
1090
- enable_graph=ENABLE_GRAPH,
1091
- )
1092
- app.include_router(
1093
- create_command_center_router(
1094
- service=COMMAND_CENTER,
1095
- require_user=require_user,
1096
- gate_read=PLATFORM.gate_read,
1097
- )
1098
- )
1099
- CHANGE_PROPOSALS = ChangeProposalService(
1100
- review_queue=REVIEW_QUEUE,
1101
- resolve_path=resolve_workspace_path,
1102
- audit=append_audit_event,
1103
- )
1104
- # Proposal-first mutations: the agent loop consults the governor so
1105
- # additive creates run with minimal friction while changes/deletions of
1106
- # existing files are staged for review instead of applied.
1107
- CHAT_AGENT_RUNTIME.deps.change_governor = CHANGE_PROPOSALS
1108
- app.include_router(
1109
- create_change_proposals_router(
1110
- service=CHANGE_PROPOSALS,
1111
- require_user=require_user,
1112
- gate_read=PLATFORM.gate_read,
1113
- gate_write=PLATFORM.gate_write,
1114
- )
1115
- )
1116
-
1117
-
1118
- # ── Health & Info ──────────────────────────────────────────────────────────────
1119
-
1120
- # ── Model runtime/provider helpers moved to latticeai.services.model_runtime ──
1121
- # ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
1122
- # /health, /mode, /runtime_features, /engines(GET) now live in the health router.
1123
- # Heavier engine mutation endpoints remain below in server_app.
1124
- _model_runtime = register_model_runtime_routers(
1125
- app=app,
1126
- create_health_router=create_health_router,
1127
- create_models_router=create_models_router,
1128
- register_health_and_model_routers=register_health_and_model_routers,
1129
- model_router=router,
1130
- runtime_service=_model_runtime_service,
1131
- runtime_features=_model_runtime_service.runtime_features,
1132
- is_public_mode=IS_PUBLIC_MODE,
1133
- engine_status=_model_runtime_service.engine_status,
1134
- get_current_user=get_current_user,
1135
- require_auth=REQUIRE_AUTH,
1136
- app_version=APP_VERSION,
1137
- app_mode=APP_MODE,
1138
- require_user=require_user,
1139
- require_admin=require_admin,
1140
- load_users=load_users,
1141
- get_user_role=get_user_role,
1142
- install_engine=_model_runtime_service.install_engine,
1143
- verify_cloud_models=_model_runtime_service.verify_cloud_models,
1144
- normalize_local_model_request=normalize_local_model_request,
1145
- download_hf_model=download_hf_model,
1146
- prepare_and_load_model=_model_runtime_service.prepare_and_load_model,
1147
- prepare_and_load_model_stream=_model_runtime_service.prepare_and_load_model_stream,
1148
- sse_event=sse_event,
1149
- ensure_ollama_server=ensure_ollama_server,
1150
- local_binary=local_binary,
1151
- filter_lower_family_versions=filter_lower_family_versions,
1152
- list_compat_profiles=_list_compat_profiles,
1153
- set_user_api_key=set_user_api_key,
1154
- engine_model_catalog=ENGINE_MODEL_CATALOG,
1155
- model_engine_aliases=MODEL_ENGINE_ALIASES,
1156
- cloud_verify_ttl_seconds=CLOUD_VERIFY_TTL_SECONDS,
1157
- allow_local_models=ALLOW_LOCAL_MODELS,
1158
- )
1159
-
1160
-
1161
- # ── Chat / Completion ──────────────────────────────────────────────────────────
1162
-
1163
- def _embedding_info() -> dict:
1164
- from latticeai.core.embedding_providers import (
1165
- PROVIDER_TYPES,
1166
- embedding_provider_profiles,
1167
- )
1168
- info = EMBEDDER.as_dict()
1169
- info["available_providers"] = list(PROVIDER_TYPES)
1170
- info["profile"] = CONFIG.embedding_profile or ""
1171
- info["profiles"] = embedding_provider_profiles()
1172
- return info
1173
-
1174
-
1175
- def _allowed_workspaces_for(user):
1176
- # No-auth local mode is single-user: no scoping. With auth, scope
1177
- # reads to the caller's memberships (legacy-global rows require an
1178
- # explicit maintenance-only compatibility opt-in).
1179
- if not REQUIRE_AUTH or not user:
1180
- return None
1181
- return PLATFORM.allowed_scopes(user)
1182
-
1183
- # Evidence → action (v9.9.6): an answer's citations become ready-to-send,
1184
- # evidence-scoped follow-ups. Deterministic composition only — execution
1185
- # stays on the chat/file-generation path.
1186
- from latticeai.api.evidence_actions import create_evidence_actions_router
1187
- from latticeai.services.evidence_actions import EvidenceActionService
1188
-
1189
- EVIDENCE_ACTIONS_SERVICE = EvidenceActionService(
1190
- node_reader=getattr(KNOWLEDGE_GRAPH, "get_node", None)
1191
- )
1192
- app.include_router(
1193
- create_evidence_actions_router(
1194
- service=EVIDENCE_ACTIONS_SERVICE,
1195
- require_user=require_user,
1196
- allowed_workspaces_for=_allowed_workspaces_for,
1197
- )
1198
- )
1199
-
1200
- # Voice memo capture (v9.9.7): the shortest path from a thought to the
1201
- # Brain. Transcription is an optional local port — absent, the memo is
1202
- # still stored and the response says it is not searchable.
1203
- from latticeai.api.voice_capture import create_voice_capture_router
1204
- from latticeai.services.voice_capture import VoiceCaptureService
1205
-
1206
- VOICE_CAPTURE = VoiceCaptureService(
1207
- pipeline=INGESTION_PIPELINE if ENABLE_GRAPH else None,
1208
- transcriber=None,
1209
- )
1210
- app.include_router(
1211
- create_voice_capture_router(
1212
- service=VOICE_CAPTURE,
1213
- require_user=require_user,
1214
- gate_write=PLATFORM.gate_write,
1215
- append_audit_event=append_audit_event,
1216
- )
1217
- )
1218
-
1219
- # Multi-turn project loop (v9.9.6): work that spans several runs keeps its
1220
- # files, open TODOs, and last honest verification in one project session.
1221
- from latticeai.api.project_sessions import create_project_sessions_router
1222
- from latticeai.core.project_sessions import ProjectSessionStore
1223
-
1224
- PROJECT_SESSIONS = ProjectSessionStore(DATA_DIR / "project_sessions")
1225
- app.include_router(
1226
- create_project_sessions_router(
1227
- store=PROJECT_SESSIONS,
1228
- require_user=require_user,
1229
- gate_read=PLATFORM.gate_read,
1230
- gate_write=PLATFORM.gate_write,
1231
- )
1232
- )
1233
-
1234
- tool_router_context, interaction_router_context = build_interaction_contexts(
1235
- config=CONFIG,
1236
- ingestion_pipeline=INGESTION_PIPELINE,
1237
- data_dir=DATA_DIR,
1238
- static_dir=STATIC_DIR,
1239
- model_router=router,
1240
- require_user=require_user,
1241
- require_admin=require_admin,
1242
- get_current_user=get_current_user,
1243
- clear_history=clear_history,
1244
- append_audit_event=append_audit_event,
1245
- enforce_rate_limit=enforce_rate_limit,
1246
- bytes_match_extension=_bytes_match_extension,
1247
- classify_sensitive_message=classify_sensitive_message,
1248
- save_to_history=save_to_history,
1249
- enable_graph=ENABLE_GRAPH,
1250
- knowledge_graph=KNOWLEDGE_GRAPH,
1251
- require_graph=_require_graph,
1252
- local_kg_watcher=LOCAL_KG_WATCHER,
1253
- load_mcp_installs=load_mcp_installs,
1254
- recommend_mcps=recommend_mcps,
1255
- install_mcp=install_mcp,
1256
- mcp_public_item=mcp_public_item,
1257
- hooks=HOOKS_REGISTRY,
1258
- workspace_service=WORKSPACE_SERVICE,
1259
- chat_context=context,
1260
- search_service=SEARCH_SERVICE,
1261
- allowed_workspaces_for=_allowed_workspaces_for,
1262
- embedding_info=_embedding_info,
1263
- agent_registry=AGENT_REGISTRY,
1264
- memory_service=MEMORY_SERVICE,
1265
- platform=PLATFORM,
1266
- active_model_getter=lambda: router.current_model_id or "",
1267
- brain_intelligence=BRAIN_INTELLIGENCE,
1268
- )
1269
- register_interaction_routers(
1270
- app,
1271
- interaction_context=interaction_router_context,
1272
- create_chat_router=create_chat_router,
1273
- create_search_router=create_search_router,
1274
- create_tools_router=create_tools_router,
1275
- create_hooks_router=create_hooks_router,
1276
- create_agent_registry_router=create_agent_registry_router,
1277
- create_memory_router=create_memory_router,
1278
- create_brain_intelligence_router=create_brain_intelligence_router,
1279
- )
1280
-
1281
- from latticeai.api.review_queue import create_review_queue_router
1282
- run_review_item = build_review_run_now_runner(PLATFORM, HTTPException)
1283
-
1284
- register_review_and_brain_tail_routers(
1285
- app,
1286
- create_review_queue_router=create_review_queue_router,
1287
- review_queue=REVIEW_QUEUE,
1288
- require_user=require_user,
1289
- gate_read=PLATFORM.gate_read,
1290
- gate_write=PLATFORM.gate_write,
1291
- run_review_item=run_review_item,
1292
- append_audit_event=append_audit_event,
1293
- # Approving a change_proposal from the Review Center applies the
1294
- # staged content through the same service the agent governor uses.
1295
- change_proposals=CHANGE_PROPOSALS,
1296
- create_browser_router=create_browser_router,
1297
- ingestion_pipeline=INGESTION_PIPELINE,
1298
- workspace_service=WORKSPACE_SERVICE,
1299
- create_portability_router=create_portability_router,
1300
- kg_portability=KG_PORTABILITY,
1301
- require_admin=require_admin,
1302
- build_brain_network=build_brain_network,
1303
- device_identity=DEVICE_IDENTITY,
1304
- data_dir=DATA_DIR,
1305
- create_network_router=create_network_router,
1306
- create_garden_router=create_garden_router,
1307
- gardener=gardener,
1308
- create_setup_router=create_setup_router,
1309
- model_router=router,
1310
- knowledge_graph=KNOWLEDGE_GRAPH,
1311
- )
1312
-
1313
- # ── Entry Point ────────────────────────────────────────────────────────────────
87
+ from latticeai.services.tool_dispatch import agent_risk as _agent_risk
88
+ from latticeai.services.tool_dispatch import check_tool_role as _check_tool_role
89
+ from latticeai.services.tool_dispatch import tool_response as _tool_response
90
+
91
+ return {
92
+ # ── pass-through re-exports ──────────────────────────────────────
93
+ "ENGINE_MODEL_CATALOG": ENGINE_MODEL_CATALOG,
94
+ "TOOL_GOVERNANCE": TOOL_GOVERNANCE,
95
+ "filter_lower_family_versions": filter_lower_family_versions,
96
+ "hash_password": hash_password,
97
+ "normalize_local_model_request": normalize_local_model_request,
98
+ "verify_password": verify_password,
99
+ "_LOCAL_WRITE_BLOCKED_PREFIXES": _LOCAL_WRITE_BLOCKED_PREFIXES,
100
+ "_TOOL_CATALOG_BRIEF": _TOOL_CATALOG_BRIEF,
101
+ "_TOOL_GOVERNANCE_DEFAULT": _TOOL_GOVERNANCE_DEFAULT,
102
+ "_agent_risk": _agent_risk,
103
+ "_build_admin_audit_report": _build_admin_audit_report,
104
+ "_build_sensitivity_report": _build_sensitivity_report,
105
+ "_check_ip_rate_limit": _check_ip_rate_limit,
106
+ "_check_tool_role": _check_tool_role,
107
+ "_classify_sensitive_message": _classify_sensitive_message,
108
+ "_create_security_router": _create_security_router,
109
+ "_fetch_skills_marketplace": _fetch_skills_marketplace,
110
+ "_get_audit_log": _get_audit_log,
111
+ "_list_compat_profiles": _list_compat_profiles,
112
+ "_redact_secret_text": _redact_secret_text,
113
+ "_tool_response": _tool_response,
114
+ "_user_id_for_email": _user_id_for_email,
115
+ "_workspace_scope_from_request": _workspace_scope_from_request,
116
+ # ── values the phases produced ───────────────────────────────────
117
+ "enforce_rate_limit": ctx.enforce_rate_limit,
118
+ "_RATE_LIMIT_ENABLED": ctx._RATE_LIMIT_ENABLED,
119
+ "_SESSION_TTL": ctx._SESSION_TTL,
120
+ "_allowed_workspaces_for": ctx._allowed_workspaces_for,
121
+ "_bytes_match_extension": ctx._bytes_match_extension,
122
+ "_check_rate_limit": ctx._check_rate_limit,
123
+ "_client_ip": ctx._client_ip,
124
+ "_embedding_info": ctx._embedding_info,
125
+ "_get_sso_discovery": ctx._get_sso_discovery,
126
+ "_graph_stats_safe": ctx._graph_stats_safe,
127
+ "_host_is_loopback": ctx._host_is_loopback,
128
+ "_llm_generate_sync": ctx._llm_generate_sync,
129
+ "_product_hardening_status": ctx._product_hardening_status,
130
+ "_recent_chat_context": ctx._recent_chat_context,
131
+ "_require_graph": ctx._require_graph,
132
+ "_scoped_hybrid_search": ctx._scoped_hybrid_search,
133
+ "_security_audit_events_safe": ctx._security_audit_events_safe,
134
+ "_security_list_uploaded_files": ctx._security_list_uploaded_files,
135
+ "_spawn": ctx._spawn,
136
+ "_workspace_graph": ctx._workspace_graph,
137
+ "_workspace_models_payload": ctx._workspace_models_payload,
138
+ "_workspace_settings_payload": ctx._workspace_settings_payload,
139
+ }
140
+
141
+
142
+ def build_context(config: "Optional[Config]" = None) -> RuntimeContext:
143
+ """Run every build phase in order and return the populated context.
144
+
145
+ Exposed separately from :func:`build_runtime` so tests can inspect the
146
+ assembly (which phase produced what) without going through the legacy
147
+ namespace adapter.
148
+ """
149
+ ctx = RuntimeContext(config)
150
+ for phase in BUILD_PHASES:
151
+ phase(ctx)
152
+ return ctx
1314
153
 
1315
- def main() -> None:
1316
- print(f"🧠 Lattice AI Server starting in {APP_MODE} mode on http://{DEFAULT_HOST}:{DEFAULT_PORT}")
1317
- uvicorn.run(app, host=DEFAULT_HOST, port=DEFAULT_PORT, log_level="info")
1318
154
 
1319
- # ── Constructed-namespace export (consumed by AppRuntime) ────────────────
1320
- # The five stages are typed and the compatibility surface is enumerated;
1321
- # assembly locals never escape the composition root.
1322
- router_bundle = build_router_bundle(app, context)
155
+ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
156
+ """Assemble the application and return its explicit export namespace."""
157
+ ctx = build_context(config)
1323
158
  runtime_bundle = RuntimeBundle(
1324
- app=app,
1325
- CONFIG=CONFIG,
1326
- KNOWLEDGE_GRAPH=KNOWLEDGE_GRAPH,
1327
- INGESTION_PIPELINE=INGESTION_PIPELINE,
1328
- AGENT_RUNTIME=AGENT_RUNTIME,
1329
- HOOKS_REGISTRY=HOOKS_REGISTRY,
1330
- REVIEW_QUEUE=REVIEW_QUEUE,
1331
- AGENT_REGISTRY=AGENT_REGISTRY,
1332
- model_router=router,
159
+ app=ctx.app,
160
+ CONFIG=ctx.CONFIG,
161
+ KNOWLEDGE_GRAPH=ctx.KNOWLEDGE_GRAPH,
162
+ INGESTION_PIPELINE=ctx.INGESTION_PIPELINE,
163
+ AGENT_RUNTIME=ctx.AGENT_RUNTIME,
164
+ HOOKS_REGISTRY=ctx.HOOKS_REGISTRY,
165
+ REVIEW_QUEUE=ctx.REVIEW_QUEUE,
166
+ AGENT_REGISTRY=ctx.AGENT_REGISTRY,
167
+ model_router=ctx.model_router,
1333
168
  build_runtime=build_runtime,
1334
169
  get_shared_runtime=get_shared_runtime,
1335
170
  create_app=create_app,
1336
- config_runtime=_config_runtime,
1337
- security_runtime=_security_runtime,
1338
- brain_runtime=_brain_runtime,
1339
- model_runtime=_model_runtime,
1340
- router_bundle=router_bundle,
171
+ config_runtime=ctx.config_runtime,
172
+ security_runtime=ctx.security_runtime,
173
+ brain_runtime=ctx.brain_runtime,
174
+ model_runtime=ctx.model_runtime,
175
+ router_bundle=build_router_bundle(ctx.app, ctx.app_context),
1341
176
  )
1342
177
  return build_runtime_namespace(
1343
178
  runtime_bundle=runtime_bundle,
1344
- legacy_exports={
1345
- "ENGINE_MODEL_CATALOG": ENGINE_MODEL_CATALOG,
1346
- "TOOL_GOVERNANCE": TOOL_GOVERNANCE,
1347
- "enforce_rate_limit": enforce_rate_limit,
1348
- "filter_lower_family_versions": filter_lower_family_versions,
1349
- "hash_password": hash_password,
1350
- "normalize_local_model_request": normalize_local_model_request,
1351
- "verify_password": verify_password,
1352
- "_LOCAL_WRITE_BLOCKED_PREFIXES": _LOCAL_WRITE_BLOCKED_PREFIXES,
1353
- "_RATE_LIMIT_ENABLED": _RATE_LIMIT_ENABLED,
1354
- "_SESSION_TTL": _SESSION_TTL,
1355
- "_TOOL_CATALOG_BRIEF": _TOOL_CATALOG_BRIEF,
1356
- "_TOOL_GOVERNANCE_DEFAULT": _TOOL_GOVERNANCE_DEFAULT,
1357
- "_agent_risk": _agent_risk,
1358
- "_allowed_workspaces_for": _allowed_workspaces_for,
1359
- "_build_admin_audit_report": _build_admin_audit_report,
1360
- "_build_sensitivity_report": _build_sensitivity_report,
1361
- "_bytes_match_extension": _bytes_match_extension,
1362
- "_check_ip_rate_limit": _check_ip_rate_limit,
1363
- "_check_rate_limit": _check_rate_limit,
1364
- "_check_tool_role": _check_tool_role,
1365
- "_classify_sensitive_message": _classify_sensitive_message,
1366
- "_client_ip": _client_ip,
1367
- "_create_security_router": _create_security_router,
1368
- "_embedding_info": _embedding_info,
1369
- "_fetch_skills_marketplace": _fetch_skills_marketplace,
1370
- "_get_audit_log": _get_audit_log,
1371
- "_get_sso_discovery": _get_sso_discovery,
1372
- "_graph_stats_safe": _graph_stats_safe,
1373
- "_host_is_loopback": _host_is_loopback,
1374
- "_list_compat_profiles": _list_compat_profiles,
1375
- "_llm_generate_sync": _llm_generate_sync,
1376
- "_product_hardening_status": _product_hardening_status,
1377
- "_recent_chat_context": _recent_chat_context,
1378
- "_redact_secret_text": _redact_secret_text,
1379
- "_require_graph": _require_graph,
1380
- "_scoped_hybrid_search": _scoped_hybrid_search,
1381
- "_security_audit_events_safe": _security_audit_events_safe,
1382
- "_security_list_uploaded_files": _security_list_uploaded_files,
1383
- "_spawn": _spawn,
1384
- "_tool_response": _tool_response,
1385
- "_user_id_for_email": _user_id_for_email,
1386
- "_workspace_graph": _workspace_graph,
1387
- "_workspace_models_payload": _workspace_models_payload,
1388
- "_workspace_scope_from_request": _workspace_scope_from_request,
1389
- "_workspace_settings_payload": _workspace_settings_payload,
1390
- },
179
+ legacy_exports=_legacy_exports(ctx),
1391
180
  )
1392
181
 
1393
182
 
@@ -1408,6 +197,11 @@ class AppRuntime:
1408
197
  (``app``, ``KNOWLEDGE_GRAPH``, ``load_users``, …) as attributes.
1409
198
  """
1410
199
 
200
+ # Declared because they are bound dynamically from the namespace dict;
201
+ # these two are the ones this module itself reads back.
202
+ app: Any
203
+ CONFIG: Any
204
+
1411
205
  def __init__(self, namespace: Dict[str, Any]) -> None:
1412
206
  self._legacy_namespace = LegacyRuntimeNamespace(namespace)
1413
207
  self._legacy_namespace.bind(self)
@@ -1441,4 +235,20 @@ def create_app(config: "Optional[Config]" = None) -> "FastAPI":
1441
235
 
1442
236
 
1443
237
  def main() -> None:
1444
- get_shared_runtime().main()
238
+ """Serve the shared runtime (``python -m latticeai.server_app``).
239
+
240
+ This used to call ``get_shared_runtime().main()``, but ``main`` was a local
241
+ inside the old ``_build`` closure and was never on the export allowlist, so
242
+ the call raised ``AttributeError`` — the module entrypoint was broken.
243
+ Serving from here needs no export at all.
244
+ """
245
+ import uvicorn
246
+
247
+ runtime = get_shared_runtime()
248
+ config = runtime.CONFIG
249
+ host, port = config.host, config.port
250
+ print(
251
+ f"🧠 Lattice AI Server starting in {config.app_mode} mode "
252
+ f"on http://{host}:{port}"
253
+ )
254
+ uvicorn.run(runtime.app, host=host, port=port, log_level="info")