ltcai 8.9.0 → 9.1.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 (208) hide show
  1. package/README.md +81 -58
  2. package/auto_setup.py +7 -904
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +221 -238
  6. package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  13. package/docs/TRUST_MODEL.md +5 -2
  14. package/docs/WHY_LATTICE.md +15 -10
  15. package/docs/WORKFLOW_DESIGNER.md +22 -0
  16. package/docs/kg-schema.md +13 -2
  17. package/docs/mcp-tools.md +17 -6
  18. package/docs/privacy.md +19 -3
  19. package/docs/public-deploy.md +32 -3
  20. package/docs/security-model.md +46 -11
  21. package/lattice_brain/__init__.py +1 -1
  22. package/lattice_brain/archive.py +4 -14
  23. package/lattice_brain/context.py +66 -9
  24. package/lattice_brain/embeddings.py +38 -2
  25. package/lattice_brain/graph/_kg_common.py +27 -462
  26. package/lattice_brain/graph/_kg_constants.py +243 -0
  27. package/lattice_brain/graph/_kg_fsutil.py +293 -0
  28. package/lattice_brain/graph/discovery.py +0 -948
  29. package/lattice_brain/graph/discovery_index.py +1126 -0
  30. package/lattice_brain/graph/documents.py +44 -9
  31. package/lattice_brain/graph/ingest.py +47 -20
  32. package/lattice_brain/graph/provenance.py +13 -6
  33. package/lattice_brain/graph/retrieval.py +141 -610
  34. package/lattice_brain/graph/retrieval_docgen.py +243 -0
  35. package/lattice_brain/graph/retrieval_vector.py +460 -0
  36. package/lattice_brain/graph/store.py +6 -0
  37. package/lattice_brain/ingestion.py +69 -4
  38. package/lattice_brain/portability.py +29 -23
  39. package/lattice_brain/quality.py +98 -4
  40. package/lattice_brain/runtime/agent_runtime.py +169 -7
  41. package/lattice_brain/runtime/hooks.py +30 -13
  42. package/lattice_brain/runtime/multi_agent.py +27 -8
  43. package/lattice_brain/runtime/statuses.py +10 -0
  44. package/lattice_brain/utils.py +46 -0
  45. package/lattice_brain/workflow.py +1 -5
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agent_registry.py +10 -8
  49. package/latticeai/api/agents.py +22 -3
  50. package/latticeai/api/auth.py +78 -16
  51. package/latticeai/api/browser.py +303 -34
  52. package/latticeai/api/chat.py +355 -952
  53. package/latticeai/api/chat_agent_http.py +356 -0
  54. package/latticeai/api/chat_contracts.py +54 -0
  55. package/latticeai/api/chat_documents.py +256 -0
  56. package/latticeai/api/chat_helpers.py +250 -0
  57. package/latticeai/api/chat_history.py +99 -0
  58. package/latticeai/api/chat_intents.py +302 -0
  59. package/latticeai/api/chat_stream.py +115 -0
  60. package/latticeai/api/computer_use.py +211 -40
  61. package/latticeai/api/health.py +9 -3
  62. package/latticeai/api/hooks.py +17 -6
  63. package/latticeai/api/knowledge_graph.py +78 -14
  64. package/latticeai/api/local_files.py +7 -2
  65. package/latticeai/api/marketplace.py +11 -0
  66. package/latticeai/api/mcp.py +102 -23
  67. package/latticeai/api/models.py +72 -33
  68. package/latticeai/api/network.py +5 -5
  69. package/latticeai/api/permissions.py +70 -29
  70. package/latticeai/api/plugins.py +21 -7
  71. package/latticeai/api/portability.py +5 -5
  72. package/latticeai/api/realtime.py +33 -5
  73. package/latticeai/api/setup.py +2 -2
  74. package/latticeai/api/static_routes.py +123 -24
  75. package/latticeai/api/tools.py +97 -14
  76. package/latticeai/api/workflow_designer.py +37 -0
  77. package/latticeai/api/workspace.py +2 -1
  78. package/latticeai/app_factory.py +185 -405
  79. package/latticeai/core/agent.py +19 -9
  80. package/latticeai/core/agent_registry.py +1 -5
  81. package/latticeai/core/config.py +23 -3
  82. package/latticeai/core/context_builder.py +21 -3
  83. package/latticeai/core/invitations.py +1 -4
  84. package/latticeai/core/io_utils.py +45 -0
  85. package/latticeai/core/legacy_compatibility.py +24 -3
  86. package/latticeai/core/local_embeddings.py +2 -4
  87. package/latticeai/core/marketplace.py +33 -2
  88. package/latticeai/core/mcp_catalog.py +450 -0
  89. package/latticeai/core/mcp_registry.py +2 -441
  90. package/latticeai/core/plugins.py +15 -0
  91. package/latticeai/core/policy.py +1 -1
  92. package/latticeai/core/realtime.py +38 -15
  93. package/latticeai/core/sessions.py +7 -2
  94. package/latticeai/core/timeutil.py +10 -0
  95. package/latticeai/core/tool_registry.py +90 -18
  96. package/latticeai/core/users.py +5 -14
  97. package/latticeai/core/workspace_graph_trace.py +31 -5
  98. package/latticeai/core/workspace_memory.py +3 -1
  99. package/latticeai/core/workspace_os.py +18 -7
  100. package/latticeai/core/workspace_os_utils.py +2 -21
  101. package/latticeai/core/workspace_permissions.py +2 -1
  102. package/latticeai/core/workspace_plugins.py +1 -1
  103. package/latticeai/core/workspace_runs.py +14 -5
  104. package/latticeai/core/workspace_skills.py +1 -1
  105. package/latticeai/core/workspace_snapshots.py +2 -1
  106. package/latticeai/core/workspace_timeline.py +2 -1
  107. package/latticeai/integrations/telegram_bot.py +96 -40
  108. package/latticeai/models/model_providers.py +111 -0
  109. package/latticeai/models/router.py +189 -173
  110. package/latticeai/runtime/access_runtime.py +62 -4
  111. package/latticeai/runtime/audit_runtime.py +27 -16
  112. package/latticeai/runtime/automation_runtime.py +22 -7
  113. package/latticeai/runtime/brain_runtime.py +19 -7
  114. package/latticeai/runtime/chat_wiring.py +2 -0
  115. package/latticeai/runtime/config_runtime.py +58 -26
  116. package/latticeai/runtime/context_runtime.py +16 -4
  117. package/latticeai/runtime/history_runtime.py +163 -0
  118. package/latticeai/runtime/hooks_runtime.py +1 -1
  119. package/latticeai/runtime/model_wiring.py +33 -5
  120. package/latticeai/runtime/namespace_runtime.py +163 -0
  121. package/latticeai/runtime/network_config_runtime.py +56 -0
  122. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  123. package/latticeai/runtime/review_wiring.py +1 -1
  124. package/latticeai/runtime/router_registration.py +34 -1
  125. package/latticeai/runtime/security_runtime.py +110 -13
  126. package/latticeai/runtime/sso_config_runtime.py +128 -0
  127. package/latticeai/runtime/stages.py +27 -0
  128. package/latticeai/runtime/user_key_runtime.py +106 -0
  129. package/latticeai/server_app.py +5 -1
  130. package/latticeai/services/architecture_readiness.py +74 -5
  131. package/latticeai/services/brain_automation.py +3 -26
  132. package/latticeai/services/chat_service.py +205 -15
  133. package/latticeai/services/local_knowledge.py +423 -0
  134. package/latticeai/services/memory_service.py +268 -21
  135. package/latticeai/services/model_engines.py +48 -33
  136. package/latticeai/services/model_errors.py +17 -0
  137. package/latticeai/services/model_loading.py +28 -25
  138. package/latticeai/services/model_runtime.py +228 -162
  139. package/latticeai/services/p_reinforce.py +22 -3
  140. package/latticeai/services/platform_runtime.py +92 -24
  141. package/latticeai/services/product_readiness.py +19 -17
  142. package/latticeai/services/review_queue.py +76 -11
  143. package/latticeai/services/router_context.py +1 -0
  144. package/latticeai/services/run_executor.py +25 -9
  145. package/latticeai/services/search_service.py +203 -28
  146. package/latticeai/services/setup_detection.py +80 -0
  147. package/latticeai/services/tool_dispatch.py +28 -5
  148. package/latticeai/services/triggers.py +53 -4
  149. package/latticeai/services/upload_service.py +13 -2
  150. package/latticeai/setup/__init__.py +25 -0
  151. package/latticeai/setup/auto_setup.py +857 -0
  152. package/latticeai/setup/wizard.py +1264 -0
  153. package/latticeai/tools/__init__.py +278 -0
  154. package/{tools → latticeai/tools}/commands.py +67 -7
  155. package/{tools → latticeai/tools}/computer.py +1 -1
  156. package/{tools → latticeai/tools}/documents.py +1 -1
  157. package/{tools → latticeai/tools}/filesystem.py +3 -3
  158. package/latticeai/tools/knowledge.py +178 -0
  159. package/{tools → latticeai/tools}/local_files.py +7 -1
  160. package/{tools → latticeai/tools}/network.py +0 -1
  161. package/local_knowledge_api.py +4 -342
  162. package/package.json +22 -2
  163. package/scripts/brain_quality_eval.py +3 -1
  164. package/scripts/bump_version.py +3 -0
  165. package/scripts/capture_release_evidence.mjs +180 -0
  166. package/scripts/check_current_release_docs.mjs +142 -0
  167. package/scripts/check_i18n_literals.mjs +107 -31
  168. package/scripts/check_openapi_drift.mjs +56 -0
  169. package/scripts/export_openapi.py +67 -7
  170. package/scripts/i18n_literal_allowlist.json +22 -24
  171. package/scripts/run_integration_tests.mjs +72 -10
  172. package/scripts/validate_release_artifacts.py +49 -7
  173. package/scripts/wheel_smoke.py +5 -0
  174. package/setup_wizard.py +3 -1304
  175. package/src-tauri/Cargo.lock +1 -1
  176. package/src-tauri/Cargo.toml +1 -1
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +11 -11
  179. package/static/app/assets/Act-Bzz0bUyW.js +1 -0
  180. package/static/app/assets/Brain-Dj2J20YA.js +321 -0
  181. package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
  182. package/static/app/assets/Library-B03FP1Yx.js +1 -0
  183. package/static/app/assets/System-80lHW0Ux.js +1 -0
  184. package/static/app/assets/index-BiMofBTM.js +17 -0
  185. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  186. package/static/app/assets/primitives-Q1A96_7v.js +1 -0
  187. package/static/app/assets/textarea-D13RtnTo.js +1 -0
  188. package/static/app/index.html +2 -2
  189. package/static/css/tokens.css +4 -2
  190. package/static/sw.js +1 -1
  191. package/tools/__init__.py +21 -269
  192. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  193. package/latticeai/runtime/app_context_runtime.py +0 -13
  194. package/latticeai/runtime/sso_runtime.py +0 -52
  195. package/latticeai/runtime/tail_wiring.py +0 -21
  196. package/scripts/com.pts.claudecode.discord.plist +0 -31
  197. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  198. package/scripts/start-pts-claudecode-discord.sh +0 -51
  199. package/static/app/assets/Act-fZokUnC0.js +0 -1
  200. package/static/app/assets/Brain-DtyuWubr.js +0 -321
  201. package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
  202. package/static/app/assets/Library-C9kyFkSt.js +0 -1
  203. package/static/app/assets/System-VbChmX7r.js +0 -1
  204. package/static/app/assets/index-DCh5AoXt.css +0 -2
  205. package/static/app/assets/index-DPdcPoF0.js +0 -17
  206. package/static/app/assets/primitives-DFeanEV6.js +0 -1
  207. package/static/app/assets/textarea-CD8UNKIy.js +0 -1
  208. package/tools/knowledge.py +0 -95
@@ -18,7 +18,6 @@ import threading
18
18
  from dataclasses import dataclass
19
19
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
20
20
 
21
- from latticeai.runtime.app_context_runtime import build_app_context
22
21
  from latticeai.runtime.access_runtime import build_access_runtime
23
22
  from latticeai.runtime.bootstrap import build_session_runtime
24
23
  from latticeai.runtime.brain_runtime import build_brain_runtime
@@ -34,21 +33,25 @@ from latticeai.runtime.hooks_runtime import (
34
33
  bind_trigger_hook_runner,
35
34
  build_hooks_runtime,
36
35
  )
36
+ from latticeai.runtime.history_runtime import build_history_query_runtime
37
37
  from latticeai.runtime.lifespan_runtime import build_lifespan_runtime
38
+ from latticeai.runtime.network_config_runtime import build_vpc_runtime
38
39
  from latticeai.runtime.model_wiring import (
39
40
  configure_model_runtime_from_context,
40
41
  register_model_runtime_routers,
41
42
  )
43
+ from latticeai.runtime.namespace_runtime import RuntimeBundle, build_runtime_namespace
42
44
  from latticeai.runtime.platform_services_runtime import (
43
45
  build_brain_network,
44
46
  )
45
47
  from latticeai.runtime.platform_runtime_wiring import build_platform_automation_runtime
46
48
  from latticeai.runtime.persistence_runtime import build_persistence_runtime
47
49
  from latticeai.runtime.review_wiring import build_review_run_now_runner
48
- from latticeai.runtime.sso_runtime import build_sso_runtime
50
+ from latticeai.runtime.sso_config_runtime import build_sso_config_runtime
49
51
  from latticeai.runtime.audit_runtime import build_audit_runtime
50
52
  from latticeai.runtime.router_registration import (
51
53
  build_auth_admin_security_router_bundle,
54
+ build_router_bundle,
52
55
  build_static_routes_bundle,
53
56
  register_health_and_model_routers,
54
57
  register_foundation_routers,
@@ -57,7 +60,7 @@ from latticeai.runtime.router_registration import (
57
60
  register_review_and_brain_tail_routers,
58
61
  )
59
62
  from latticeai.runtime.security_runtime import build_security_runtime
60
- from latticeai.runtime.tail_wiring import register_tail_runtime_routers
63
+ from latticeai.runtime.user_key_runtime import build_user_key_runtime
61
64
  from latticeai.runtime.web_runtime import build_web_runtime
62
65
 
63
66
  if TYPE_CHECKING: # imports for annotations only — keep module import light
@@ -73,14 +76,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
73
76
  deliberately *inside* this function so that importing the module performs
74
77
  no GPU init, no singleton construction, and no filesystem writes.
75
78
  """
76
- import hashlib
77
- import json
78
79
  import logging
79
80
  import os
80
- import re
81
- import secrets
82
81
  import threading
83
- import time
84
82
  from pathlib import Path
85
83
 
86
84
  try:
@@ -108,7 +106,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
108
106
  enforce_rate_limit as _enforce_rate_limit,
109
107
  )
110
108
  from latticeai.core.audit import (
111
- get_audit_log as _get_audit_log, # noqa: F401 - legacy server_app attr exported via locals()
109
+ get_audit_log as _get_audit_log, # noqa: F401 - explicit legacy server_app export
112
110
  classify_sensitive_message as _classify_sensitive_message,
113
111
  build_sensitivity_report as _build_sensitivity_report,
114
112
  build_admin_audit_report as _build_admin_audit_report,
@@ -133,24 +131,19 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
133
131
  user_id_for_email as _user_id_for_email,
134
132
  )
135
133
  from latticeai.services.chat_service import ChatService
134
+ from latticeai.services.app_context import AppContext
136
135
  from latticeai.core.embedding_providers import resolve_embedder, resolve_embedding_profile
137
136
  from latticeai.services.model_runtime import (
138
137
  CLOUD_VERIFY_TTL_SECONDS,
139
138
  ENGINE_MODEL_CATALOG,
140
139
  LOCAL_SERVER_PROCESSES,
141
140
  MODEL_ENGINE_ALIASES,
142
- configure_model_runtime,
141
+ build_model_runtime,
143
142
  download_hf_model,
144
- engine_status,
145
143
  filter_lower_family_versions,
146
- install_engine,
147
144
  local_binary,
148
145
  normalize_local_model_request,
149
- prepare_and_load_model,
150
- prepare_and_load_model_stream,
151
- runtime_features,
152
146
  sse_event,
153
- verify_cloud_models,
154
147
  ensure_ollama_server,
155
148
  )
156
149
  from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
@@ -178,10 +171,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
178
171
  from lattice_brain.ingestion import IngestionItem
179
172
  from lattice_brain.storage import storage_from_env
180
173
  from latticeai.api.network import create_network_router
181
- # The aliased names below look unused but are part of the legacy
182
- # ``server_app`` attribute surface: every local is exported via
183
- # ``dict(locals())`` and reached through ``server_app.__getattr__``
184
- # (tests import _agent_risk, _LOCAL_WRITE_BLOCKED_PREFIXES, …).
174
+ # The aliased names below form the explicit, allowlisted compatibility
175
+ # surface consumed by historical ``server_app`` callers.
185
176
  from latticeai.services.tool_dispatch import ( # noqa: F401
186
177
  LOCAL_WRITE_BLOCKED_PREFIXES as _LOCAL_WRITE_BLOCKED_PREFIXES,
187
178
  TOOL_GOVERNANCE,
@@ -202,8 +193,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
202
193
  create_mcp_install_state,
203
194
  )
204
195
  from latticeai.services.p_reinforce import PReinforceGardener
205
- from setup_wizard import get_recommendations, scan_environment
206
- from tools import ensure_agent_root, execute_tool, knowledge_save
196
+ from latticeai.setup.wizard import get_recommendations, scan_environment
197
+ from latticeai.tools import ensure_agent_root, execute_tool, knowledge_save
207
198
 
208
199
  try:
209
200
  import keyring
@@ -230,7 +221,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
230
221
  def _host_is_loopback(host: str) -> bool:
231
222
  return _host_is_loopback_impl(host)
232
223
 
233
- NETWORK_EXPOSED = _config_runtime["NETWORK_EXPOSED"]
234
224
  ENABLE_TELEGRAM = _config_runtime["ENABLE_TELEGRAM"]
235
225
  ENABLE_GRAPH = _config_runtime["ENABLE_GRAPH"]
236
226
  AUTOLOAD_MODELS = _config_runtime["AUTOLOAD_MODELS"]
@@ -254,12 +244,13 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
254
244
  SSO_CLIENT_SECRET = _security_runtime["SSO_CLIENT_SECRET"]
255
245
  SSO_REDIRECT_URI = _security_runtime["SSO_REDIRECT_URI"]
256
246
  SSO_PROVIDER_NAME = _security_runtime["SSO_PROVIDER_NAME"]
247
+ INVITE_CODE = _security_runtime["INVITE_CODE"]
248
+ INVITE_COOKIE_SECRET = _security_runtime["INVITE_COOKIE_SECRET"]
249
+ INVITE_GATE_ENABLED = _security_runtime["INVITE_GATE_ENABLED"]
250
+ SECURE_COOKIES = _security_runtime["SECURE_COOKIES"]
257
251
 
258
- # SSO cache + discovery built after get_sso_settings is defined (see below)
259
- _sso_discovery_cache = None
260
- _sso_discovery_cache_url = ""
261
- _sso_states: Dict[str, float] = {}
262
- _get_sso_discovery = None # filled after def below
252
+ # SSO config + discovery seam is built below, after SSO_FILE is known
253
+ # (latticeai.runtime.sso_config_runtime).
263
254
 
264
255
  # ── Password hashing — used directly from latticeai.core.security ──────────────
265
256
  # (hash_password / verify_password are imported above; no local wrapper needed)
@@ -295,26 +286,27 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
295
286
  _session_store = _session_runtime["_session_store"]
296
287
  create_session = _session_runtime["create_session"]
297
288
  get_session_email = _session_runtime["get_session_email"]
298
- get_session_user_id = _session_runtime["get_session_user_id"]
299
289
  invalidate_session = _session_runtime["invalidate_session"]
300
290
 
301
291
  # ── User Management Logic ──────────────────────────────────────────────────
302
292
  BASE_DIR = Path(__file__).resolve().parent.parent
303
293
  DATA_DIR = CONFIG.data_dir
304
- DATA_DIR.mkdir(parents=True, exist_ok=True)
294
+ DATA_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
295
+ try:
296
+ DATA_DIR.chmod(0o700)
297
+ except OSError:
298
+ pass
305
299
  STATIC_DIR = CONFIG.static_dir
306
300
 
307
301
  USERS_FILE = DATA_DIR / "users.json"
308
302
  HISTORY_FILE = DATA_DIR / "chat_history.json"
309
303
  VPC_FILE = DATA_DIR / "vpc_config.json"
310
- MCP_FILE = DATA_DIR / "mcp_installs.json"
311
304
  AUDIT_FILE = DATA_DIR / "audit_log.json"
312
305
  SSO_FILE = DATA_DIR / "sso_config.json"
313
306
 
314
307
  # MCP state extracted to mcp_registry.create_mcp_install_state (server decomp)
315
308
  _mcp_state = create_mcp_install_state(DATA_DIR)
316
309
  load_mcp_installs = _mcp_state["load_mcp_installs"]
317
- save_mcp_installs = _mcp_state["save_mcp_installs"]
318
310
  mcp_public_item = _mcp_state["mcp_public_item"]
319
311
  recommend_mcps = _mcp_state["recommend_mcps"]
320
312
  install_mcp = _mcp_state["install_mcp"]
@@ -354,7 +346,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
354
346
  embedder=EMBEDDER,
355
347
  storage_engine=STORAGE_ENGINE,
356
348
  )
357
- BRAIN_CORE = _brain_runtime["BRAIN_CORE"]
358
349
  KNOWLEDGE_GRAPH = _brain_runtime["KNOWLEDGE_GRAPH"]
359
350
  # ── v4 durable conversation store: unbounded episodic memory in the same
360
351
  # SQLite file as the graph (kg_portability backup/restore covers it for
@@ -388,7 +379,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
388
379
  WORKSPACE_OS = _persistence_runtime["WORKSPACE_OS"]
389
380
  WORKSPACE_SERVICE = _persistence_runtime["WORKSPACE_SERVICE"]
390
381
  INVITATION_STORE = _persistence_runtime["INVITATION_STORE"]
391
- PLUGINS_DIR = _persistence_runtime["PLUGINS_DIR"]
392
382
  PLUGIN_REGISTRY = _persistence_runtime["PLUGIN_REGISTRY"]
393
383
  TEMPLATE_CATALOG = _persistence_runtime["TEMPLATE_CATALOG"]
394
384
  AGENT_REGISTRY = _persistence_runtime["AGENT_REGISTRY"]
@@ -434,89 +424,29 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
434
424
  redirect_uri: Optional[str] = None
435
425
  scopes: Optional[str] = None
436
426
 
437
- def _sso_env_defaults() -> Dict[str, object]:
438
- return {
439
- "enabled": bool(SSO_DISCOVERY_URL and SSO_CLIENT_ID and SSO_CLIENT_SECRET),
440
- "provider_name": SSO_PROVIDER_NAME,
441
- "discovery_url": SSO_DISCOVERY_URL,
442
- "client_id": SSO_CLIENT_ID,
443
- "client_secret": SSO_CLIENT_SECRET,
444
- "redirect_uri": SSO_REDIRECT_URI,
445
- "scopes": "openid email profile",
446
- }
447
-
448
- def load_sso_config() -> Dict[str, object]:
449
- config = _sso_env_defaults()
450
- if SSO_FILE.exists():
451
- try:
452
- data = json.loads(SSO_FILE.read_text(encoding="utf-8"))
453
- if isinstance(data, dict):
454
- config.update({k: v for k, v in data.items() if v is not None})
455
- except Exception as e:
456
- logging.warning("load_sso_config failed (using env/defaults): %s", e)
457
- config["provider_name"] = str(config.get("provider_name") or "SSO")
458
- config["discovery_url"] = str(config.get("discovery_url") or "")
459
- config["client_id"] = str(config.get("client_id") or "")
460
- config["client_secret"] = str(config.get("client_secret") or "")
461
- config["redirect_uri"] = str(config.get("redirect_uri") or SSO_REDIRECT_URI)
462
- config["scopes"] = str(config.get("scopes") or "openid email profile")
463
- config["enabled"] = bool(config.get("enabled")) and bool(
464
- config["discovery_url"] and config["client_id"] and config["client_secret"]
465
- )
466
- return config
467
-
468
- def get_sso_settings() -> Dict[str, object]:
469
- return load_sso_config()
470
-
471
- # Build SSO runtime (cache + helper) now that get_sso_settings exists in scope.
472
- if _get_sso_discovery is None:
473
- _sso = build_sso_runtime(
474
- get_sso_settings=get_sso_settings,
475
- logging=logging,
476
- )
477
- _sso_discovery_cache = _sso["_sso_discovery_cache"]
478
- _sso_discovery_cache_url = _sso["_sso_discovery_cache_url"]
479
- _sso_states = _sso["_sso_states"]
480
- _get_sso_discovery = _sso["_get_sso_discovery"]
481
-
482
- def public_sso_config(config: Optional[Dict[str, object]] = None) -> Dict[str, object]:
483
- cfg = config or get_sso_settings()
484
- return {
485
- "enabled": bool(cfg.get("enabled")),
486
- "provider_name": cfg.get("provider_name") or "",
487
- "discovery_url": cfg.get("discovery_url") or "",
488
- "client_id": cfg.get("client_id") or "",
489
- "redirect_uri": cfg.get("redirect_uri") or SSO_REDIRECT_URI,
490
- "scopes": cfg.get("scopes") or "openid email profile",
491
- "secret_configured": bool(cfg.get("client_secret")),
492
- }
493
-
494
- def save_sso_config(update: Dict[str, object]) -> Dict[str, object]:
495
- nonlocal _sso_discovery_cache, _sso_discovery_cache_url
496
- current = load_sso_config()
497
- if update.get("client_secret") == "":
498
- update.pop("client_secret", None)
499
- current.update({k: v for k, v in update.items() if v is not None})
500
- current["enabled"] = bool(current.get("enabled")) and bool(
501
- current.get("discovery_url") and current.get("client_id") and current.get("client_secret")
502
- )
503
- SSO_FILE.write_text(json.dumps(current, ensure_ascii=False, indent=2), encoding="utf-8")
504
- _sso_discovery_cache = None
505
- _sso_discovery_cache_url = ""
506
- return current
427
+ # SSO config + OIDC discovery seam. One closure owns config load/save and the
428
+ # discovery cache, so save_sso_config now actually invalidates discovery.
429
+ _sso_runtime = build_sso_config_runtime(
430
+ sso_file=SSO_FILE,
431
+ discovery_url=SSO_DISCOVERY_URL,
432
+ client_id=SSO_CLIENT_ID,
433
+ client_secret=SSO_CLIENT_SECRET,
434
+ redirect_uri=SSO_REDIRECT_URI,
435
+ provider_name=SSO_PROVIDER_NAME,
436
+ logging=logging,
437
+ )
438
+ _sso_env_defaults = _sso_runtime["_sso_env_defaults"]
439
+ get_sso_settings = _sso_runtime["get_sso_settings"]
440
+ public_sso_config = _sso_runtime["public_sso_config"]
441
+ save_sso_config = _sso_runtime["save_sso_config"]
442
+ _get_sso_discovery = _sso_runtime["_get_sso_discovery"]
443
+ _sso_states = _sso_runtime["_sso_states"]
507
444
 
508
445
  # MCP/skill request models moved to latticeai.api.mcp (v1.3.0).
509
- DEFAULT_VPC_CONFIG = {
510
- "provider": "AWS",
511
- "region": "ap-northeast-2",
512
- "cidr_block": "10.42.0.0/16",
513
- "private_subnets": ["10.42.10.0/24", "10.42.20.0/24"],
514
- "endpoint": "ltcai-private.local",
515
- "vpn_status": "standby",
516
- "peering_status": "not_configured",
517
- "notes": "로컬 MLX 브릿지를 프라이빗 서브넷 또는 VPN 뒤에서 운영할 때 쓰는 네트워크 프로필입니다.",
518
- "updated_at": None,
519
- }
446
+ # VPC network-profile config → latticeai.runtime.network_config_runtime.
447
+ _vpc_runtime = build_vpc_runtime(vpc_file=VPC_FILE, logging=logging)
448
+ load_vpc_config = _vpc_runtime["load_vpc_config"]
449
+ save_vpc_config = _vpc_runtime["save_vpc_config"]
520
450
 
521
451
 
522
452
  def load_users():
@@ -539,22 +469,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
539
469
  def save_users(users):
540
470
  save_users_file(USERS_FILE, users)
541
471
 
542
- def load_vpc_config() -> Dict:
543
- if not os.path.exists(VPC_FILE):
544
- return DEFAULT_VPC_CONFIG.copy()
545
- try:
546
- with open(VPC_FILE, "r", encoding="utf-8") as f:
547
- stored = json.load(f)
548
- return {**DEFAULT_VPC_CONFIG, **stored}
549
- except Exception as e:
550
- logging.warning("load_vpc_config failed (using defaults): %s", e)
551
- return DEFAULT_VPC_CONFIG.copy()
552
-
553
- def save_vpc_config(config: Dict):
554
- config["updated_at"] = datetime.now().isoformat()
555
- with open(VPC_FILE, "w", encoding="utf-8") as f:
556
- json.dump(config, f, ensure_ascii=False, indent=2)
557
-
558
472
 
559
473
  _history_lock = threading.Lock()
560
474
 
@@ -639,130 +553,23 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
639
553
  get_audit_log = _audit_rt["get_audit_log"]
640
554
  append_audit_event = _audit_rt["append_audit_event"]
641
555
 
642
- def _history_allowed_workspaces_for(user_email: Optional[str]):
643
- if not REQUIRE_AUTH or not user_email:
644
- return None
645
- try:
646
- return set(WORKSPACE_SERVICE.readable_workspaces(user_email))
647
- except Exception as exc:
648
- logging.warning("history workspace scope resolution failed for %s: %s", user_email, exc)
649
- return set()
650
-
651
- def _history_include_legacy_global(user_email: Optional[str]) -> bool:
652
- return not REQUIRE_AUTH or not user_email
653
-
654
- def get_history(
655
- user_email: Optional[str] = None,
656
- allowed_workspaces=None,
657
- include_legacy_global: Optional[bool] = None,
658
- ):
659
- try:
660
- if allowed_workspaces is None and user_email:
661
- allowed_workspaces = _history_allowed_workspaces_for(user_email)
662
- if include_legacy_global is None:
663
- include_legacy_global = _history_include_legacy_global(user_email)
664
- return CONVERSATIONS.history(
665
- user_email=user_email,
666
- allowed_workspaces=allowed_workspaces,
667
- include_legacy_global=include_legacy_global,
668
- )
669
- except Exception as e:
670
- logging.warning("get_history failed: %s", e)
671
- return []
672
-
673
- # Chat service seam: behaviour-preserving façade for history access and
674
- # Workspace-OS answer-trace recording used by the (unchanged) streaming chat path.
675
- CHAT_SERVICE = ChatService(store=WORKSPACE_OS, get_history=get_history)
676
-
677
- def conversation_title(item: Dict) -> str:
678
- content = str(item.get("content") or "").strip()
679
- content = re.sub(r"\s+", " ", content)
680
- return content[:48] or "새 대화"
681
-
682
- def group_history_conversations(history: Optional[List[Dict]] = None) -> List[Dict]:
683
- history = history if history is not None else get_history()
684
- conversations: Dict[str, Dict] = {}
685
- order: List[str] = []
686
-
687
- for index, item in enumerate(history):
688
- conv_id = item.get("conversation_id")
689
- if not conv_id:
690
- conv_id = "legacy-previous-history"
691
-
692
- if conv_id not in conversations:
693
- conversations[conv_id] = {
694
- "id": conv_id,
695
- "title": "이전 대화 기록" if conv_id == "legacy-previous-history" else conversation_title(item),
696
- "created_at": item.get("timestamp"),
697
- "updated_at": item.get("timestamp"),
698
- "message_count": 0,
699
- "last_message": "",
700
- "source": item.get("source"),
701
- }
702
- order.append(conv_id)
703
-
704
- conv = conversations[conv_id]
705
- conv["message_count"] += 1
706
- conv["updated_at"] = item.get("timestamp") or conv.get("updated_at")
707
- conv["last_message"] = conversation_title(item)
708
- if conv_id != "legacy-previous-history" and item.get("role") == "user" and (not conv.get("title") or conv["title"] == "새 대화"):
709
- conv["title"] = conversation_title(item)
710
-
711
- return sorted((conversations[key] for key in order), key=lambda item: item.get("updated_at") or "", reverse=True)
712
-
713
- def get_conversation_messages(
714
- conversation_id: str,
715
- *,
716
- user_email: Optional[str] = None,
717
- allowed_workspaces=None,
718
- include_legacy_global: Optional[bool] = None,
719
- ) -> List[Dict]:
720
- history = get_history(
721
- user_email=user_email,
722
- allowed_workspaces=allowed_workspaces,
723
- include_legacy_global=include_legacy_global,
724
- )
725
- if conversation_id == "legacy-previous-history":
726
- return [item for item in history if not item.get("conversation_id")]
727
- return [item for item in history if item.get("conversation_id") == conversation_id]
728
-
729
- def clear_history(
730
- keep_last: int = 0,
731
- *,
732
- user_email: Optional[str] = None,
733
- allowed_workspaces=None,
734
- include_legacy_global: Optional[bool] = None,
735
- ) -> Dict:
736
- if allowed_workspaces is None and user_email:
737
- allowed_workspaces = _history_allowed_workspaces_for(user_email)
738
- if include_legacy_global is None:
739
- include_legacy_global = _history_include_legacy_global(user_email)
740
- return CONVERSATIONS.clear_all(
741
- keep_last=keep_last,
742
- user_email=user_email,
743
- allowed_workspaces=allowed_workspaces,
744
- include_legacy_global=include_legacy_global,
745
- )
746
-
747
- def clear_conversation(
748
- conversation_id: str,
749
- started_at: Optional[str] = None,
750
- *,
751
- user_email: Optional[str] = None,
752
- allowed_workspaces=None,
753
- include_legacy_global: Optional[bool] = None,
754
- ) -> Dict:
755
- if allowed_workspaces is None and user_email:
756
- allowed_workspaces = _history_allowed_workspaces_for(user_email)
757
- if include_legacy_global is None:
758
- include_legacy_global = _history_include_legacy_global(user_email)
759
- return CONVERSATIONS.clear_conversation(
760
- conversation_id,
761
- started_at=started_at,
762
- user_email=user_email,
763
- allowed_workspaces=allowed_workspaces,
764
- include_legacy_global=include_legacy_global,
765
- )
556
+ # History query/clear seam (scope resolution, get_history, grouping, clears)
557
+ # latticeai.runtime.history_runtime. save_to_history (the write path) stays
558
+ # inline above because it is bound to redaction/audit/ingestion.
559
+ _history_query_runtime = build_history_query_runtime(
560
+ conversations=CONVERSATIONS,
561
+ workspace_service=WORKSPACE_SERVICE,
562
+ require_auth=REQUIRE_AUTH,
563
+ logging=logging,
564
+ )
565
+ _history_allowed_workspaces_for = _history_query_runtime["_history_allowed_workspaces_for"]
566
+ _history_include_legacy_global = _history_query_runtime["_history_include_legacy_global"]
567
+ get_history = _history_query_runtime["get_history"]
568
+ conversation_title = _history_query_runtime["conversation_title"]
569
+ group_history_conversations = _history_query_runtime["group_history_conversations"]
570
+ get_conversation_messages = _history_query_runtime["get_conversation_messages"]
571
+ clear_history = _history_query_runtime["clear_history"]
572
+ clear_conversation = _history_query_runtime["clear_conversation"]
766
573
 
767
574
  _access_runtime = build_access_runtime(
768
575
  config=CONFIG,
@@ -790,136 +597,33 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
790
597
  def _bytes_match_extension(data: bytes, ext: str) -> bool:
791
598
  return _bytes_match_extension_impl(data, ext)
792
599
 
793
- _LOCAL_APPROVAL_TTL_SECONDS = 5 * 60
794
- _local_approvals: Dict[str, Dict[str, object]] = {}
795
-
796
-
797
- def _normalize_local_path_for_approval(path: str) -> str:
798
- return str(Path(path).expanduser().resolve())
799
-
800
-
801
- def _content_fingerprint(content: str = "") -> str:
802
- return hashlib.sha256(content.encode("utf-8")).hexdigest()
803
-
804
-
805
- def _local_permission_response(path: str, action: str, user_email: str, content: str = "") -> dict:
806
- normalized = _normalize_local_path_for_approval(path)
807
- token = secrets.token_urlsafe(24)
808
- record: Dict[str, object] = {
809
- "path": normalized,
810
- "action": action,
811
- "user_email": user_email,
812
- "expires_at": time.time() + _LOCAL_APPROVAL_TTL_SECONDS,
813
- "approved": False,
814
- }
815
- if action == "write":
816
- record["content_hash"] = _content_fingerprint(content)
817
- _local_approvals[token] = record
818
- return {
819
- "permission_required": True,
820
- "path": path,
821
- "action": action,
822
- "approval_token": token,
823
- "expires_in": _LOCAL_APPROVAL_TTL_SECONDS,
824
- }
825
-
826
-
827
- def _require_local_approval(
828
- *,
829
- token: Optional[str],
830
- path: str,
831
- action: str,
832
- user_email: str,
833
- content: str = "",
834
- ) -> None:
835
- if not token:
836
- raise HTTPException(status_code=403, detail="파일 접근 승인 토큰이 필요합니다.")
837
- record = _local_approvals.get(token)
838
- if not record or float(record.get("expires_at", 0)) < time.time():
839
- raise HTTPException(status_code=403, detail="파일 접근 승인이 만료되었거나 유효하지 않습니다.")
840
- if not record.get("approved"):
841
- raise HTTPException(status_code=403, detail="파일 접근이 아직 승인되지 않았습니다.")
842
- if record.get("user_email") != user_email:
843
- raise HTTPException(status_code=403, detail="다른 사용자의 파일 접근 승인은 사용할 수 없습니다.")
844
- if record.get("path") != _normalize_local_path_for_approval(path) or record.get("action") != action:
845
- raise HTTPException(status_code=403, detail="파일 접근 승인 범위가 일치하지 않습니다.")
846
- if action == "write" and record.get("content_hash") != _content_fingerprint(content):
847
- raise HTTPException(status_code=403, detail="승인된 파일 내용과 요청 내용이 다릅니다.")
848
-
849
-
850
- def get_history_user(email: Optional[str], nickname: Optional[str] = None) -> Dict:
851
- if not email:
852
- return {"user_email": None, "user_nickname": nickname or None}
853
- users = load_users()
854
- user = users.get(email, {})
855
- return {
856
- "user_email": email,
857
- "user_nickname": nickname or user.get("nickname") or user.get("name") or email,
858
- }
600
+ # Local file-access approval lives entirely in
601
+ # ``latticeai.api.permissions.PermissionGateway`` (token hash-at-rest,
602
+ # persisted approval queue, Discord notifications). The historical inline
603
+ # copy that used to sit here was app-dead — no route referenced it — so it
604
+ # was removed to keep this factory a single wiring path.
859
605
 
860
- def get_user_api_key(email: Optional[str], provider: str) -> Optional[str]:
861
- if not email:
862
- return None
863
- keyring_key = f"{email}:{provider}"
864
- if keyring is not None:
865
- try:
866
- key = keyring.get_password("LatticeAI", keyring_key)
867
- if key:
868
- return key.strip()
869
- except Exception as exc:
870
- logging.warning("keyring read failed for %s: %s", provider, exc)
871
- users = load_users()
872
- user = users.get(email) or {}
873
- api_keys = user.get("api_keys") or {}
874
- key = api_keys.get(provider)
875
- if isinstance(key, str) and key.strip() and ALLOW_PLAINTEXT_API_KEYS:
876
- return key.strip()
877
- return None
878
-
879
- def set_user_api_key(email: str, provider: str, key: str) -> None:
880
- keyring_key = f"{email}:{provider}"
881
- if keyring is not None:
882
- try:
883
- keyring.set_password("LatticeAI", keyring_key, key)
884
- users = load_users()
885
- user = users.get(email)
886
- if user and "api_keys" in user:
887
- user["api_keys"].pop(provider, None)
888
- if not user["api_keys"]:
889
- user.pop("api_keys", None)
890
- save_users(users)
891
- return
892
- except Exception as exc:
893
- logging.warning("keyring write failed for %s: %s", provider, exc)
894
- if not ALLOW_PLAINTEXT_API_KEYS:
895
- raise HTTPException(
896
- status_code=500,
897
- detail="OS keyring에 API 키를 저장하지 못했습니다. keyring 설정을 확인하거나 LATTICEAI_ALLOW_PLAINTEXT_API_KEYS=true를 명시적으로 설정하세요.",
898
- )
899
-
900
- if not ALLOW_PLAINTEXT_API_KEYS:
901
- raise HTTPException(
902
- status_code=500,
903
- detail="keyring 패키지를 사용할 수 없어 API 키를 안전하게 저장할 수 없습니다.",
904
- )
606
+ _user_key_runtime = build_user_key_runtime(
607
+ load_users=load_users,
608
+ save_users=save_users,
609
+ ensure_user_identity=ensure_user_identity,
610
+ keyring=keyring,
611
+ allow_plaintext_api_keys=ALLOW_PLAINTEXT_API_KEYS,
612
+ logging=logging,
613
+ http_exception=HTTPException,
614
+ )
615
+ get_history_user = _user_key_runtime["get_history_user"]
616
+ get_user_api_key = _user_key_runtime["get_user_api_key"]
617
+ set_user_api_key = _user_key_runtime["set_user_api_key"]
905
618
 
906
- users = load_users()
907
- user = users.get(email)
908
- if not user:
909
- user = {
910
- "password_hash": "",
911
- "salt": "",
912
- "name": email,
913
- "nickname": email,
914
- "role": "user",
915
- "disabled": False,
916
- }
917
- ensure_user_identity(email, user)
918
- api_keys = user.get("api_keys") or {}
919
- api_keys[provider] = key
920
- user["api_keys"] = api_keys
921
- users[email] = user
922
- save_users(users)
619
+ # Chat service owns persistence and trace behavior after its user-key
620
+ # dependencies are available.
621
+ CHAT_SERVICE = ChatService(
622
+ store=WORKSPACE_OS,
623
+ get_history=get_history,
624
+ save_to_history=save_to_history,
625
+ get_history_user=get_history_user,
626
+ )
923
627
 
924
628
  # ── Sensitivity analysis — delegated to latticeai.core.audit ──────────────────
925
629
  def classify_sensitive_message(item: Dict, index: int) -> Dict:
@@ -977,8 +681,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
977
681
  local_server_processes=LOCAL_SERVER_PROCESSES,
978
682
  logger=logging,
979
683
  )
980
- autoload_default_model = _lifespan_runtime["autoload_default_model"]
981
- unload_idle_models_loop = _lifespan_runtime["unload_idle_models_loop"]
982
684
  _spawn = _lifespan_runtime["_spawn"]
983
685
  lifespan = _lifespan_runtime["lifespan"]
984
686
 
@@ -993,14 +695,11 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
993
695
  static_dir=STATIC_DIR,
994
696
  )
995
697
  app = _web_runtime["app"]
996
- CORS_ALLOWED_ORIGINS = _web_runtime["CORS_ALLOWED_ORIGINS"]
997
698
  ensure_agent_root()
998
699
 
999
700
  OPEN_REGISTRATION = CONFIG.open_registration
1000
- INVITE_CODE = CONFIG.invite_code
1001
- INVITE_GATE_ENABLED = CONFIG.invite_gate_enabled
1002
- configure_model_runtime_from_context(
1003
- configure_model_runtime=configure_model_runtime,
701
+ _model_runtime_service = configure_model_runtime_from_context(
702
+ build_model_runtime=build_model_runtime,
1004
703
  router=router,
1005
704
  APP_MODE=APP_MODE,
1006
705
  DEFAULT_HOST=DEFAULT_HOST,
@@ -1030,6 +729,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1030
729
  static_dir=STATIC_DIR,
1031
730
  invite_gate_enabled=INVITE_GATE_ENABLED,
1032
731
  invite_code=INVITE_CODE,
732
+ invite_cookie_secret=INVITE_COOKIE_SECRET,
733
+ secure_cookies=SECURE_COOKIES,
1033
734
  app_mode=APP_MODE,
1034
735
  model_router=router,
1035
736
  require_user=require_user,
@@ -1037,6 +738,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1037
738
  STATIC_ROUTES = _static_routes_bundle["STATIC_ROUTES"]
1038
739
  ui_file_response = _static_routes_bundle["ui_file_response"]
1039
740
  local_sysinfo = _static_routes_bundle["local_sysinfo"]
741
+ invite_authorized = _static_routes_bundle["invite_authorized"]
1040
742
 
1041
743
  # ── Auth & Admin routers (latticeai.api) ─────────────────────────────────────
1042
744
  _foundation_router_bundle = build_auth_admin_security_router_bundle(
@@ -1059,6 +761,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1059
761
  open_registration=OPEN_REGISTRATION,
1060
762
  session_ttl=_SESSION_TTL,
1061
763
  require_auth=REQUIRE_AUTH,
764
+ secure_cookies=SECURE_COOKIES,
765
+ invite_authorized=invite_authorized,
1062
766
  ensure_identity=ensure_user_identity,
1063
767
  create_admin_router=create_admin_router,
1064
768
  require_admin=require_admin,
@@ -1159,6 +863,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1159
863
  include_image_missing_replies: bool = True,
1160
864
  user_email: Optional[str] = None,
1161
865
  conversation_id: Optional[str] = None,
866
+ workspace_id: Optional[str] = None,
1162
867
  ) -> str:
1163
868
  return build_recent_chat_context(
1164
869
  get_history=get_history,
@@ -1166,6 +871,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1166
871
  include_image_missing_replies=include_image_missing_replies,
1167
872
  user_email=user_email,
1168
873
  conversation_id=conversation_id,
874
+ workspace_id=workspace_id,
1169
875
  )
1170
876
 
1171
877
  CHAT_AGENT_RUNTIME = build_chat_agent_runtime_from_context(
@@ -1182,7 +888,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1182
888
 
1183
889
  # ── Typed dependency context (latticeai.services.app_context) ────────────────
1184
890
  # One context object replaces the historical 25-30-kwarg router wiring.
1185
- context = build_app_context(
891
+ context = AppContext(
1186
892
  config=CONFIG,
1187
893
  data_dir=DATA_DIR,
1188
894
  static_dir=STATIC_DIR,
@@ -1264,6 +970,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1264
970
  agent_registry=AGENT_REGISTRY,
1265
971
  data_dir=DATA_DIR,
1266
972
  append_audit_event=append_audit_event,
973
+ memory_service=MEMORY_SERVICE,
1267
974
  tz_name=getattr(CONFIG, "timezone", None),
1268
975
  )
1269
976
  _llm_generate_sync = _platform_automation_runtime["_llm_generate_sync"]
@@ -1318,28 +1025,30 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1318
1025
  # ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
1319
1026
  # /health, /mode, /runtime_features, /engines(GET) now live in the health router.
1320
1027
  # Heavier engine mutation endpoints remain below in server_app.
1321
- MODEL_SERVICE = register_model_runtime_routers(
1028
+ _model_runtime = register_model_runtime_routers(
1322
1029
  app=app,
1323
1030
  create_health_router=create_health_router,
1324
1031
  create_models_router=create_models_router,
1325
1032
  register_health_and_model_routers=register_health_and_model_routers,
1326
1033
  model_router=router,
1327
- runtime_features=runtime_features,
1034
+ runtime_service=_model_runtime_service,
1035
+ runtime_features=_model_runtime_service.runtime_features,
1328
1036
  is_public_mode=IS_PUBLIC_MODE,
1329
- engine_status=engine_status,
1037
+ engine_status=_model_runtime_service.engine_status,
1330
1038
  get_current_user=get_current_user,
1331
1039
  require_auth=REQUIRE_AUTH,
1332
1040
  app_version=APP_VERSION,
1333
1041
  app_mode=APP_MODE,
1334
1042
  require_user=require_user,
1043
+ require_admin=require_admin,
1335
1044
  load_users=load_users,
1336
1045
  get_user_role=get_user_role,
1337
- install_engine=install_engine,
1338
- verify_cloud_models=verify_cloud_models,
1046
+ install_engine=_model_runtime_service.install_engine,
1047
+ verify_cloud_models=_model_runtime_service.verify_cloud_models,
1339
1048
  normalize_local_model_request=normalize_local_model_request,
1340
1049
  download_hf_model=download_hf_model,
1341
- prepare_and_load_model=prepare_and_load_model,
1342
- prepare_and_load_model_stream=prepare_and_load_model_stream,
1050
+ prepare_and_load_model=_model_runtime_service.prepare_and_load_model,
1051
+ prepare_and_load_model_stream=_model_runtime_service.prepare_and_load_model_stream,
1343
1052
  sse_event=sse_event,
1344
1053
  ensure_ollama_server=ensure_ollama_server,
1345
1054
  local_binary=local_binary,
@@ -1366,7 +1075,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1366
1075
 
1367
1076
  def _allowed_workspaces_for(user):
1368
1077
  # No-auth local mode is single-user: no scoping. With auth, scope
1369
- # reads to the caller's memberships (legacy-global rows stay visible).
1078
+ # reads to the caller's memberships (legacy-global rows require an
1079
+ # explicit maintenance-only compatibility opt-in).
1370
1080
  if not REQUIRE_AUTH or not user:
1371
1081
  return None
1372
1082
  return PLATFORM.allowed_scopes(user)
@@ -1395,6 +1105,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1395
1105
  install_mcp=install_mcp,
1396
1106
  mcp_public_item=mcp_public_item,
1397
1107
  hooks=HOOKS_REGISTRY,
1108
+ workspace_service=WORKSPACE_SERVICE,
1398
1109
  chat_context=context,
1399
1110
  search_service=SEARCH_SERVICE,
1400
1111
  allowed_workspaces_for=_allowed_workspaces_for,
@@ -1418,10 +1129,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1418
1129
  from latticeai.api.review_queue import create_review_queue_router
1419
1130
  run_review_item = build_review_run_now_runner(PLATFORM, HTTPException)
1420
1131
 
1421
- BRAIN_NETWORK = register_tail_runtime_routers(
1422
- app=app,
1132
+ register_review_and_brain_tail_routers(
1133
+ app,
1423
1134
  create_review_queue_router=create_review_queue_router,
1424
- register_review_and_brain_tail_routers=register_review_and_brain_tail_routers,
1425
1135
  review_queue=REVIEW_QUEUE,
1426
1136
  require_user=require_user,
1427
1137
  gate_read=PLATFORM.gate_read,
@@ -1430,6 +1140,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1430
1140
  append_audit_event=append_audit_event,
1431
1141
  create_browser_router=create_browser_router,
1432
1142
  ingestion_pipeline=INGESTION_PIPELINE,
1143
+ workspace_service=WORKSPACE_SERVICE,
1433
1144
  create_portability_router=create_portability_router,
1434
1145
  kg_portability=KG_PORTABILITY,
1435
1146
  require_admin=require_admin,
@@ -1450,9 +1161,78 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1450
1161
  uvicorn.run(app, host=DEFAULT_HOST, port=DEFAULT_PORT, log_level="info")
1451
1162
 
1452
1163
  # ── Constructed-namespace export (consumed by AppRuntime) ────────────────
1453
- # Every local singletons, helper functions, request models becomes an
1454
- # attribute of the runtime so the legacy ``server_app`` surface survives.
1455
- return dict(locals())
1164
+ # The five stages are typed and the compatibility surface is enumerated;
1165
+ # assembly locals never escape the composition root.
1166
+ router_bundle = build_router_bundle(app, context)
1167
+ runtime_bundle = RuntimeBundle(
1168
+ app=app,
1169
+ CONFIG=CONFIG,
1170
+ KNOWLEDGE_GRAPH=KNOWLEDGE_GRAPH,
1171
+ INGESTION_PIPELINE=INGESTION_PIPELINE,
1172
+ AGENT_RUNTIME=AGENT_RUNTIME,
1173
+ HOOKS_REGISTRY=HOOKS_REGISTRY,
1174
+ REVIEW_QUEUE=REVIEW_QUEUE,
1175
+ AGENT_REGISTRY=AGENT_REGISTRY,
1176
+ model_router=router,
1177
+ build_runtime=build_runtime,
1178
+ get_shared_runtime=get_shared_runtime,
1179
+ create_app=create_app,
1180
+ config_runtime=_config_runtime,
1181
+ security_runtime=_security_runtime,
1182
+ brain_runtime=_brain_runtime,
1183
+ model_runtime=_model_runtime,
1184
+ router_bundle=router_bundle,
1185
+ )
1186
+ return build_runtime_namespace(
1187
+ runtime_bundle=runtime_bundle,
1188
+ legacy_exports={
1189
+ "ENGINE_MODEL_CATALOG": ENGINE_MODEL_CATALOG,
1190
+ "TOOL_GOVERNANCE": TOOL_GOVERNANCE,
1191
+ "enforce_rate_limit": enforce_rate_limit,
1192
+ "filter_lower_family_versions": filter_lower_family_versions,
1193
+ "hash_password": hash_password,
1194
+ "normalize_local_model_request": normalize_local_model_request,
1195
+ "verify_password": verify_password,
1196
+ "_LOCAL_WRITE_BLOCKED_PREFIXES": _LOCAL_WRITE_BLOCKED_PREFIXES,
1197
+ "_RATE_LIMIT_ENABLED": _RATE_LIMIT_ENABLED,
1198
+ "_SESSION_TTL": _SESSION_TTL,
1199
+ "_TOOL_CATALOG_BRIEF": _TOOL_CATALOG_BRIEF,
1200
+ "_TOOL_GOVERNANCE_DEFAULT": _TOOL_GOVERNANCE_DEFAULT,
1201
+ "_agent_risk": _agent_risk,
1202
+ "_allowed_workspaces_for": _allowed_workspaces_for,
1203
+ "_build_admin_audit_report": _build_admin_audit_report,
1204
+ "_build_sensitivity_report": _build_sensitivity_report,
1205
+ "_bytes_match_extension": _bytes_match_extension,
1206
+ "_check_ip_rate_limit": _check_ip_rate_limit,
1207
+ "_check_rate_limit": _check_rate_limit,
1208
+ "_check_tool_role": _check_tool_role,
1209
+ "_classify_sensitive_message": _classify_sensitive_message,
1210
+ "_client_ip": _client_ip,
1211
+ "_create_security_router": _create_security_router,
1212
+ "_embedding_info": _embedding_info,
1213
+ "_fetch_skills_marketplace": _fetch_skills_marketplace,
1214
+ "_get_audit_log": _get_audit_log,
1215
+ "_get_sso_discovery": _get_sso_discovery,
1216
+ "_graph_stats_safe": _graph_stats_safe,
1217
+ "_host_is_loopback": _host_is_loopback,
1218
+ "_list_compat_profiles": _list_compat_profiles,
1219
+ "_llm_generate_sync": _llm_generate_sync,
1220
+ "_product_hardening_status": _product_hardening_status,
1221
+ "_recent_chat_context": _recent_chat_context,
1222
+ "_redact_secret_text": _redact_secret_text,
1223
+ "_require_graph": _require_graph,
1224
+ "_scoped_hybrid_search": _scoped_hybrid_search,
1225
+ "_security_audit_events_safe": _security_audit_events_safe,
1226
+ "_security_list_uploaded_files": _security_list_uploaded_files,
1227
+ "_spawn": _spawn,
1228
+ "_tool_response": _tool_response,
1229
+ "_user_id_for_email": _user_id_for_email,
1230
+ "_workspace_graph": _workspace_graph,
1231
+ "_workspace_models_payload": _workspace_models_payload,
1232
+ "_workspace_scope_from_request": _workspace_scope_from_request,
1233
+ "_workspace_settings_payload": _workspace_settings_payload,
1234
+ },
1235
+ )
1456
1236
 
1457
1237
 
1458
1238
  @dataclass(frozen=True)