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.
- package/README.md +81 -58
- package/auto_setup.py +7 -904
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +221 -238
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
- package/docs/DEVELOPMENT.md +53 -14
- package/docs/LEGACY_COMPATIBILITY.md +4 -4
- package/docs/ONBOARDING.md +10 -2
- package/docs/OPERATIONS.md +8 -4
- package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
- package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
- package/docs/TRUST_MODEL.md +5 -2
- package/docs/WHY_LATTICE.md +15 -10
- package/docs/WORKFLOW_DESIGNER.md +22 -0
- package/docs/kg-schema.md +13 -2
- package/docs/mcp-tools.md +17 -6
- package/docs/privacy.md +19 -3
- package/docs/public-deploy.md +32 -3
- package/docs/security-model.md +46 -11
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +4 -14
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +293 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +1126 -0
- package/lattice_brain/graph/documents.py +44 -9
- package/lattice_brain/graph/ingest.py +47 -20
- package/lattice_brain/graph/provenance.py +13 -6
- package/lattice_brain/graph/retrieval.py +141 -610
- package/lattice_brain/graph/retrieval_docgen.py +243 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +69 -4
- package/lattice_brain/portability.py +29 -23
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +169 -7
- package/lattice_brain/runtime/hooks.py +30 -13
- package/lattice_brain/runtime/multi_agent.py +27 -8
- package/lattice_brain/runtime/statuses.py +10 -0
- package/lattice_brain/utils.py +46 -0
- package/lattice_brain/workflow.py +1 -5
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agent_registry.py +10 -8
- package/latticeai/api/agents.py +22 -3
- package/latticeai/api/auth.py +78 -16
- package/latticeai/api/browser.py +303 -34
- package/latticeai/api/chat.py +355 -952
- package/latticeai/api/chat_agent_http.py +356 -0
- package/latticeai/api/chat_contracts.py +54 -0
- package/latticeai/api/chat_documents.py +256 -0
- package/latticeai/api/chat_helpers.py +250 -0
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +302 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +211 -40
- package/latticeai/api/health.py +9 -3
- package/latticeai/api/hooks.py +17 -6
- package/latticeai/api/knowledge_graph.py +78 -14
- package/latticeai/api/local_files.py +7 -2
- package/latticeai/api/marketplace.py +11 -0
- package/latticeai/api/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +70 -29
- package/latticeai/api/plugins.py +21 -7
- package/latticeai/api/portability.py +5 -5
- package/latticeai/api/realtime.py +33 -5
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +123 -24
- package/latticeai/api/tools.py +97 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +185 -405
- package/latticeai/core/agent.py +19 -9
- package/latticeai/core/agent_registry.py +1 -5
- package/latticeai/core/config.py +23 -3
- package/latticeai/core/context_builder.py +21 -3
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +45 -0
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- package/latticeai/core/plugins.py +15 -0
- package/latticeai/core/policy.py +1 -1
- package/latticeai/core/realtime.py +38 -15
- package/latticeai/core/sessions.py +7 -2
- package/latticeai/core/timeutil.py +10 -0
- package/latticeai/core/tool_registry.py +90 -18
- package/latticeai/core/users.py +5 -14
- package/latticeai/core/workspace_graph_trace.py +31 -5
- package/latticeai/core/workspace_memory.py +3 -1
- package/latticeai/core/workspace_os.py +18 -7
- package/latticeai/core/workspace_os_utils.py +2 -21
- package/latticeai/core/workspace_permissions.py +2 -1
- package/latticeai/core/workspace_plugins.py +1 -1
- package/latticeai/core/workspace_runs.py +14 -5
- package/latticeai/core/workspace_skills.py +1 -1
- package/latticeai/core/workspace_snapshots.py +2 -1
- package/latticeai/core/workspace_timeline.py +2 -1
- package/latticeai/integrations/telegram_bot.py +96 -40
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +189 -173
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +22 -7
- package/latticeai/runtime/brain_runtime.py +19 -7
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/config_runtime.py +58 -26
- package/latticeai/runtime/context_runtime.py +16 -4
- package/latticeai/runtime/history_runtime.py +163 -0
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +163 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- package/latticeai/runtime/platform_runtime_wiring.py +4 -3
- package/latticeai/runtime/review_wiring.py +1 -1
- package/latticeai/runtime/router_registration.py +34 -1
- package/latticeai/runtime/security_runtime.py +110 -13
- package/latticeai/runtime/sso_config_runtime.py +128 -0
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/runtime/user_key_runtime.py +106 -0
- package/latticeai/server_app.py +5 -1
- package/latticeai/services/architecture_readiness.py +74 -5
- package/latticeai/services/brain_automation.py +3 -26
- package/latticeai/services/chat_service.py +205 -15
- package/latticeai/services/local_knowledge.py +423 -0
- package/latticeai/services/memory_service.py +268 -21
- package/latticeai/services/model_engines.py +48 -33
- package/latticeai/services/model_errors.py +17 -0
- package/latticeai/services/model_loading.py +28 -25
- package/latticeai/services/model_runtime.py +228 -162
- package/latticeai/services/p_reinforce.py +22 -3
- package/latticeai/services/platform_runtime.py +92 -24
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +76 -11
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +25 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/setup_detection.py +80 -0
- package/latticeai/services/tool_dispatch.py +28 -5
- package/latticeai/services/triggers.py +53 -4
- package/latticeai/services/upload_service.py +13 -2
- package/latticeai/setup/__init__.py +25 -0
- package/latticeai/setup/auto_setup.py +857 -0
- package/latticeai/setup/wizard.py +1264 -0
- package/latticeai/tools/__init__.py +278 -0
- package/{tools → latticeai/tools}/commands.py +67 -7
- package/{tools → latticeai/tools}/computer.py +1 -1
- package/{tools → latticeai/tools}/documents.py +1 -1
- package/{tools → latticeai/tools}/filesystem.py +3 -3
- package/latticeai/tools/knowledge.py +178 -0
- package/{tools → latticeai/tools}/local_files.py +7 -1
- package/{tools → latticeai/tools}/network.py +0 -1
- package/local_knowledge_api.py +4 -342
- package/package.json +22 -2
- package/scripts/brain_quality_eval.py +3 -1
- package/scripts/bump_version.py +3 -0
- package/scripts/capture_release_evidence.mjs +180 -0
- package/scripts/check_current_release_docs.mjs +142 -0
- package/scripts/check_i18n_literals.mjs +107 -31
- package/scripts/check_openapi_drift.mjs +56 -0
- package/scripts/export_openapi.py +67 -7
- package/scripts/i18n_literal_allowlist.json +22 -24
- package/scripts/run_integration_tests.mjs +72 -10
- package/scripts/validate_release_artifacts.py +49 -7
- package/scripts/wheel_smoke.py +5 -0
- package/setup_wizard.py +3 -1304
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/Act-Bzz0bUyW.js +1 -0
- package/static/app/assets/Brain-Dj2J20YA.js +321 -0
- package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
- package/static/app/assets/Library-B03FP1Yx.js +1 -0
- package/static/app/assets/System-80lHW0Ux.js +1 -0
- package/static/app/assets/index-BiMofBTM.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-Q1A96_7v.js +1 -0
- package/static/app/assets/textarea-D13RtnTo.js +1 -0
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -2
- package/static/sw.js +1 -1
- package/tools/__init__.py +21 -269
- package/docs/CODE_REVIEW_2026-07-06.md +0 -764
- package/latticeai/runtime/app_context_runtime.py +0 -13
- package/latticeai/runtime/sso_runtime.py +0 -52
- package/latticeai/runtime/tail_wiring.py +0 -21
- package/scripts/com.pts.claudecode.discord.plist +0 -31
- package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
- package/scripts/start-pts-claudecode-discord.sh +0 -51
- package/static/app/assets/Act-fZokUnC0.js +0 -1
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
- package/static/app/assets/Library-C9kyFkSt.js +0 -1
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
- package/static/app/assets/textarea-CD8UNKIy.js +0 -1
- package/tools/knowledge.py +0 -95
package/latticeai/app_factory.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
182
|
-
#
|
|
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
|
|
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
|
|
259
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1422
|
-
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
|
-
#
|
|
1454
|
-
#
|
|
1455
|
-
|
|
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)
|