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