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