ltcai 8.9.0 → 9.0.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 +28 -39
- package/auto_setup.py +11 -62
- package/docs/CHANGELOG.md +76 -237
- package/docs/COMMUNITY_AND_PLUGINS.md +2 -2
- package/docs/DEVELOPMENT.md +8 -8
- package/docs/LEGACY_COMPATIBILITY.md +1 -1
- package/docs/ONBOARDING.md +2 -2
- package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +2 -2
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +4 -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 +297 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +972 -0
- package/lattice_brain/graph/retrieval.py +0 -570
- package/lattice_brain/graph/retrieval_docgen.py +210 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +68 -0
- package/lattice_brain/portability.py +1 -9
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +166 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/utils.py +28 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +340 -407
- package/latticeai/api/chat_helpers.py +227 -0
- package/latticeai/api/computer_use.py +149 -31
- package/latticeai/api/marketplace.py +11 -0
- package/latticeai/api/permissions.py +3 -3
- package/latticeai/api/tools.py +1 -0
- package/latticeai/app_factory.py +82 -360
- package/latticeai/core/io_utils.py +37 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- 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/users.py +4 -9
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/core/workspace_os_utils.py +3 -17
- package/latticeai/integrations/telegram_bot.py +7 -2
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +58 -136
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +9 -0
- package/latticeai/runtime/history_runtime.py +163 -0
- package/latticeai/runtime/namespace_runtime.py +173 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- package/latticeai/runtime/sso_config_runtime.py +128 -0
- package/latticeai/runtime/user_key_runtime.py +106 -0
- package/latticeai/services/architecture_readiness.py +2 -2
- package/latticeai/services/memory_service.py +213 -0
- package/latticeai/services/platform_runtime.py +9 -1
- package/latticeai/services/product_readiness.py +11 -11
- package/latticeai/services/review_queue.py +64 -11
- package/latticeai/services/run_executor.py +21 -0
- package/latticeai/services/setup_detection.py +80 -0
- package/latticeai/services/tool_dispatch.py +1 -1
- package/package.json +1 -1
- package/setup_wizard.py +11 -55
- 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-fZokUnC0.js → Act-21lIXx2E.js} +1 -1
- package/static/app/assets/Brain-BqUd5UJJ.js +321 -0
- package/static/app/assets/{Capture-D5KV3Cu7.js → Capture-BA7Z2Q1u.js} +1 -1
- package/static/app/assets/{Library-C9kyFkSt.js → Library-bFMtyni3.js} +1 -1
- package/static/app/assets/System-K6krGCqn.js +1 -0
- package/static/app/assets/index-C4R3ws30.js +17 -0
- package/static/app/assets/index-ChSeOB02.css +2 -0
- package/static/app/assets/primitives-sQU3it5I.js +1 -0
- package/static/app/assets/{textarea-CD8UNKIy.js → textarea-DK3Fd_lR.js} +1 -1
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -2
- package/static/sw.js +1 -1
- package/tools/local_files.py +6 -0
- package/latticeai/runtime/sso_runtime.py +0 -52
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- 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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Runtime namespace filtering for legacy server_app compatibility.
|
|
2
|
+
|
|
3
|
+
The app factory still constructs a broad local namespace for historical
|
|
4
|
+
``server_app`` attribute access. This module keeps that surface deliberate:
|
|
5
|
+
public runtime objects and known legacy helpers stay visible, while internal
|
|
6
|
+
assembly scratch values do not leak into ``server_app.__getattr__``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, fields
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
from typing import Any, Dict, Mapping
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class RuntimeBundle:
|
|
18
|
+
"""Typed migration target for app-factory runtime dependencies."""
|
|
19
|
+
|
|
20
|
+
app: Any
|
|
21
|
+
CONFIG: Any
|
|
22
|
+
KNOWLEDGE_GRAPH: Any
|
|
23
|
+
INGESTION_PIPELINE: Any
|
|
24
|
+
AGENT_RUNTIME: Any
|
|
25
|
+
HOOKS_REGISTRY: Any
|
|
26
|
+
REVIEW_QUEUE: Any
|
|
27
|
+
AGENT_REGISTRY: Any
|
|
28
|
+
model_router: Any
|
|
29
|
+
build_runtime: Any
|
|
30
|
+
get_shared_runtime: Any
|
|
31
|
+
create_app: Any
|
|
32
|
+
|
|
33
|
+
def as_legacy_dict(self) -> Dict[str, Any]:
|
|
34
|
+
return {field.name: getattr(self, field.name) for field in fields(self)}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
INTERNAL_RUNTIME_NAMES = {
|
|
38
|
+
"config",
|
|
39
|
+
"logging",
|
|
40
|
+
"os",
|
|
41
|
+
"threading",
|
|
42
|
+
"Path",
|
|
43
|
+
"mx",
|
|
44
|
+
"uvicorn",
|
|
45
|
+
"HTTPException",
|
|
46
|
+
"Request",
|
|
47
|
+
"BaseModel",
|
|
48
|
+
"keyring",
|
|
49
|
+
"datetime",
|
|
50
|
+
"runtime_bundle",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
LEGACY_UNDERSCORE_EXPORTS = {
|
|
54
|
+
"_LOCAL_WRITE_BLOCKED_PREFIXES",
|
|
55
|
+
"_RATE_LIMIT_ENABLED",
|
|
56
|
+
"_SESSION_TTL",
|
|
57
|
+
"_TOOL_CATALOG_BRIEF",
|
|
58
|
+
"_TOOL_GOVERNANCE_DEFAULT",
|
|
59
|
+
"_agent_risk",
|
|
60
|
+
"_allowed_workspaces_for",
|
|
61
|
+
"_build_admin_audit_report",
|
|
62
|
+
"_build_sensitivity_report",
|
|
63
|
+
"_bytes_match_extension",
|
|
64
|
+
"_check_ip_rate_limit",
|
|
65
|
+
"_check_rate_limit",
|
|
66
|
+
"_check_tool_role",
|
|
67
|
+
"_classify_sensitive_message",
|
|
68
|
+
"_client_ip",
|
|
69
|
+
"_create_security_router",
|
|
70
|
+
"_embedding_info",
|
|
71
|
+
"_fetch_skills_marketplace",
|
|
72
|
+
"_get_audit_log",
|
|
73
|
+
"_get_sso_discovery",
|
|
74
|
+
"_graph_stats_safe",
|
|
75
|
+
"_host_is_loopback",
|
|
76
|
+
"_list_compat_profiles",
|
|
77
|
+
"_llm_generate_sync",
|
|
78
|
+
"_product_hardening_status",
|
|
79
|
+
"_recent_chat_context",
|
|
80
|
+
"_redact_secret_text",
|
|
81
|
+
"_require_graph",
|
|
82
|
+
"_scoped_hybrid_search",
|
|
83
|
+
"_security_audit_events_safe",
|
|
84
|
+
"_security_list_uploaded_files",
|
|
85
|
+
"_spawn",
|
|
86
|
+
"_tool_response",
|
|
87
|
+
"_user_id_for_email",
|
|
88
|
+
"_workspace_graph",
|
|
89
|
+
"_workspace_models_payload",
|
|
90
|
+
"_workspace_scope_from_request",
|
|
91
|
+
"_workspace_settings_payload",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
LEGACY_PUBLIC_EXPORTS = {
|
|
95
|
+
"ENGINE_MODEL_CATALOG",
|
|
96
|
+
"TOOL_GOVERNANCE",
|
|
97
|
+
"enforce_rate_limit",
|
|
98
|
+
"filter_lower_family_versions",
|
|
99
|
+
"hash_password",
|
|
100
|
+
"normalize_local_model_request",
|
|
101
|
+
"verify_password",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_internal_runtime_dict(name: str, value: Any) -> bool:
|
|
106
|
+
if not isinstance(value, dict):
|
|
107
|
+
return False
|
|
108
|
+
if name in {"_RUNTIME_BUNDLE"}:
|
|
109
|
+
return False
|
|
110
|
+
return (
|
|
111
|
+
name.endswith("_runtime")
|
|
112
|
+
or name.endswith("_router_bundle")
|
|
113
|
+
or name.endswith("_rt")
|
|
114
|
+
or name
|
|
115
|
+
in {
|
|
116
|
+
"_mcp_state",
|
|
117
|
+
"_garden_import",
|
|
118
|
+
"_foundation_router_bundle",
|
|
119
|
+
"_static_routes_bundle",
|
|
120
|
+
"_vpc_runtime",
|
|
121
|
+
"_sso_runtime",
|
|
122
|
+
"_security_runtime",
|
|
123
|
+
"_session_runtime",
|
|
124
|
+
"_config_runtime",
|
|
125
|
+
"_context_runtime",
|
|
126
|
+
"_brain_runtime",
|
|
127
|
+
"_hooks_runtime",
|
|
128
|
+
"_history_query_runtime",
|
|
129
|
+
"_persistence_runtime",
|
|
130
|
+
"_platform_automation_runtime",
|
|
131
|
+
"_user_key_runtime",
|
|
132
|
+
"_web_runtime",
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_runtime_namespace(
|
|
138
|
+
local_namespace: Mapping[str, Any],
|
|
139
|
+
*,
|
|
140
|
+
runtime_bundle: RuntimeBundle | Mapping[str, Any],
|
|
141
|
+
) -> Dict[str, Any]:
|
|
142
|
+
"""Return the compatibility namespace without assembly scratch values."""
|
|
143
|
+
legacy_bundle = (
|
|
144
|
+
runtime_bundle.as_legacy_dict()
|
|
145
|
+
if isinstance(runtime_bundle, RuntimeBundle)
|
|
146
|
+
else dict(runtime_bundle)
|
|
147
|
+
)
|
|
148
|
+
exported: Dict[str, Any] = dict(legacy_bundle)
|
|
149
|
+
allowed = set(legacy_bundle) | LEGACY_PUBLIC_EXPORTS | LEGACY_UNDERSCORE_EXPORTS
|
|
150
|
+
for name in allowed:
|
|
151
|
+
if name in exported:
|
|
152
|
+
continue
|
|
153
|
+
if name in INTERNAL_RUNTIME_NAMES:
|
|
154
|
+
continue
|
|
155
|
+
value = local_namespace.get(name)
|
|
156
|
+
if value is None:
|
|
157
|
+
continue
|
|
158
|
+
if isinstance(value, ModuleType):
|
|
159
|
+
continue
|
|
160
|
+
if _is_internal_runtime_dict(name, value):
|
|
161
|
+
continue
|
|
162
|
+
exported[name] = value
|
|
163
|
+
exported["RUNTIME_BUNDLE"] = runtime_bundle
|
|
164
|
+
exported["_RUNTIME_BUNDLE"] = legacy_bundle
|
|
165
|
+
return exported
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
__all__ = [
|
|
169
|
+
"INTERNAL_RUNTIME_NAMES",
|
|
170
|
+
"LEGACY_UNDERSCORE_EXPORTS",
|
|
171
|
+
"RuntimeBundle",
|
|
172
|
+
"build_runtime_namespace",
|
|
173
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""VPC network-profile config seam extracted from the legacy app factory.
|
|
2
|
+
|
|
3
|
+
``DEFAULT_VPC_CONFIG`` and the load/save helpers used to live inline in
|
|
4
|
+
``app_factory._build``. Behaviour and exported names are preserved exactly for
|
|
5
|
+
the legacy ``server_app`` compatibility namespace; the only change is that they
|
|
6
|
+
now live behind a builder so the factory stays a wiring path.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_vpc_runtime(*, vpc_file: Path, logging: Any) -> Dict[str, Any]:
|
|
19
|
+
"""Return ``DEFAULT_VPC_CONFIG`` and the VPC load/save helpers."""
|
|
20
|
+
|
|
21
|
+
default_vpc_config: Dict[str, Any] = {
|
|
22
|
+
"provider": "AWS",
|
|
23
|
+
"region": "ap-northeast-2",
|
|
24
|
+
"cidr_block": "10.42.0.0/16",
|
|
25
|
+
"private_subnets": ["10.42.10.0/24", "10.42.20.0/24"],
|
|
26
|
+
"endpoint": "ltcai-private.local",
|
|
27
|
+
"vpn_status": "standby",
|
|
28
|
+
"peering_status": "not_configured",
|
|
29
|
+
"notes": "로컬 MLX 브릿지를 프라이빗 서브넷 또는 VPN 뒤에서 운영할 때 쓰는 네트워크 프로필입니다.",
|
|
30
|
+
"updated_at": None,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def load_vpc_config() -> Dict:
|
|
34
|
+
if not os.path.exists(vpc_file):
|
|
35
|
+
return default_vpc_config.copy()
|
|
36
|
+
try:
|
|
37
|
+
with open(vpc_file, "r", encoding="utf-8") as f:
|
|
38
|
+
stored = json.load(f)
|
|
39
|
+
return {**default_vpc_config, **stored}
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logging.warning("load_vpc_config failed (using defaults): %s", e)
|
|
42
|
+
return default_vpc_config.copy()
|
|
43
|
+
|
|
44
|
+
def save_vpc_config(config: Dict) -> None:
|
|
45
|
+
config["updated_at"] = datetime.now().isoformat()
|
|
46
|
+
with open(vpc_file, "w", encoding="utf-8") as f:
|
|
47
|
+
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"DEFAULT_VPC_CONFIG": default_vpc_config,
|
|
51
|
+
"load_vpc_config": load_vpc_config,
|
|
52
|
+
"save_vpc_config": save_vpc_config,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["build_vpc_runtime"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""SSO / OIDC config + discovery seam extracted from the app factory.
|
|
2
|
+
|
|
3
|
+
Owns the whole SSO surface that used to be inline in ``app_factory._build``:
|
|
4
|
+
the env-default resolution, the ``sso_config.json`` load/save, the public
|
|
5
|
+
(secret-stripped) view, and the OIDC discovery document cache.
|
|
6
|
+
|
|
7
|
+
Unlike the previous split (config inline in the factory + a separate discovery
|
|
8
|
+
cache in ``sso_runtime``), the discovery cache now lives in the *same* closure
|
|
9
|
+
as ``save_sso_config``, so saving a new SSO config actually invalidates the
|
|
10
|
+
cached discovery document. The old inline ``save_sso_config`` reassigned a
|
|
11
|
+
factory-local cache variable that ``_get_sso_discovery`` never read, so the
|
|
12
|
+
invalidation was a silent no-op — this consolidation fixes that.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_sso_config_runtime(
|
|
23
|
+
*,
|
|
24
|
+
sso_file: Path,
|
|
25
|
+
discovery_url: str,
|
|
26
|
+
client_id: str,
|
|
27
|
+
client_secret: str,
|
|
28
|
+
redirect_uri: str,
|
|
29
|
+
provider_name: str,
|
|
30
|
+
logging: Any,
|
|
31
|
+
) -> Dict[str, Any]:
|
|
32
|
+
"""Return the SSO config helpers + discovery accessor as a name → value dict."""
|
|
33
|
+
|
|
34
|
+
# Single shared discovery cache: save_sso_config and _get_sso_discovery both
|
|
35
|
+
# close over it, so a config change is observed on the next discovery fetch.
|
|
36
|
+
_discovery_cache: Dict[str, Any] = {"data": None, "url": ""}
|
|
37
|
+
_sso_states: Dict[str, float] = {}
|
|
38
|
+
|
|
39
|
+
def _sso_env_defaults() -> Dict[str, object]:
|
|
40
|
+
return {
|
|
41
|
+
"enabled": bool(discovery_url and client_id and client_secret),
|
|
42
|
+
"provider_name": provider_name,
|
|
43
|
+
"discovery_url": discovery_url,
|
|
44
|
+
"client_id": client_id,
|
|
45
|
+
"client_secret": client_secret,
|
|
46
|
+
"redirect_uri": redirect_uri,
|
|
47
|
+
"scopes": "openid email profile",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def load_sso_config() -> Dict[str, object]:
|
|
51
|
+
config = _sso_env_defaults()
|
|
52
|
+
if sso_file.exists():
|
|
53
|
+
try:
|
|
54
|
+
data = json.loads(sso_file.read_text(encoding="utf-8"))
|
|
55
|
+
if isinstance(data, dict):
|
|
56
|
+
config.update({k: v for k, v in data.items() if v is not None})
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logging.warning("load_sso_config failed (using env/defaults): %s", e)
|
|
59
|
+
config["provider_name"] = str(config.get("provider_name") or "SSO")
|
|
60
|
+
config["discovery_url"] = str(config.get("discovery_url") or "")
|
|
61
|
+
config["client_id"] = str(config.get("client_id") or "")
|
|
62
|
+
config["client_secret"] = str(config.get("client_secret") or "")
|
|
63
|
+
config["redirect_uri"] = str(config.get("redirect_uri") or redirect_uri)
|
|
64
|
+
config["scopes"] = str(config.get("scopes") or "openid email profile")
|
|
65
|
+
config["enabled"] = bool(config.get("enabled")) and bool(
|
|
66
|
+
config["discovery_url"] and config["client_id"] and config["client_secret"]
|
|
67
|
+
)
|
|
68
|
+
return config
|
|
69
|
+
|
|
70
|
+
def get_sso_settings() -> Dict[str, object]:
|
|
71
|
+
return load_sso_config()
|
|
72
|
+
|
|
73
|
+
def public_sso_config(config: Optional[Dict[str, object]] = None) -> Dict[str, object]:
|
|
74
|
+
cfg = config or get_sso_settings()
|
|
75
|
+
return {
|
|
76
|
+
"enabled": bool(cfg.get("enabled")),
|
|
77
|
+
"provider_name": cfg.get("provider_name") or "",
|
|
78
|
+
"discovery_url": cfg.get("discovery_url") or "",
|
|
79
|
+
"client_id": cfg.get("client_id") or "",
|
|
80
|
+
"redirect_uri": cfg.get("redirect_uri") or redirect_uri,
|
|
81
|
+
"scopes": cfg.get("scopes") or "openid email profile",
|
|
82
|
+
"secret_configured": bool(cfg.get("client_secret")),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def save_sso_config(update: Dict[str, object]) -> Dict[str, object]:
|
|
86
|
+
current = load_sso_config()
|
|
87
|
+
if update.get("client_secret") == "":
|
|
88
|
+
update.pop("client_secret", None)
|
|
89
|
+
current.update({k: v for k, v in update.items() if v is not None})
|
|
90
|
+
current["enabled"] = bool(current.get("enabled")) and bool(
|
|
91
|
+
current.get("discovery_url") and current.get("client_id") and current.get("client_secret")
|
|
92
|
+
)
|
|
93
|
+
sso_file.write_text(json.dumps(current, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
94
|
+
_discovery_cache["data"] = None
|
|
95
|
+
_discovery_cache["url"] = ""
|
|
96
|
+
return current
|
|
97
|
+
|
|
98
|
+
async def _get_sso_discovery() -> Optional[Dict]:
|
|
99
|
+
settings = get_sso_settings()
|
|
100
|
+
url = settings.get("discovery_url", "")
|
|
101
|
+
if _discovery_cache["data"] and _discovery_cache["url"] == url:
|
|
102
|
+
return _discovery_cache["data"]
|
|
103
|
+
if not url:
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
import httpx as _httpx
|
|
107
|
+
async with _httpx.AsyncClient() as c:
|
|
108
|
+
r = await c.get(url, timeout=10)
|
|
109
|
+
r.raise_for_status()
|
|
110
|
+
_discovery_cache["data"] = r.json()
|
|
111
|
+
_discovery_cache["url"] = url
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logging.warning("SSO discovery failed: %s", e)
|
|
114
|
+
return None
|
|
115
|
+
return _discovery_cache["data"]
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"_sso_env_defaults": _sso_env_defaults,
|
|
119
|
+
"load_sso_config": load_sso_config,
|
|
120
|
+
"get_sso_settings": get_sso_settings,
|
|
121
|
+
"public_sso_config": public_sso_config,
|
|
122
|
+
"save_sso_config": save_sso_config,
|
|
123
|
+
"_get_sso_discovery": _get_sso_discovery,
|
|
124
|
+
"_sso_states": _sso_states,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = ["build_sso_config_runtime"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""User profile + provider API-key helpers extracted from the app factory.
|
|
2
|
+
|
|
3
|
+
The helpers close over the user store, keyring adapter, identity migration, and
|
|
4
|
+
plaintext-key policy. Returning callables preserves the legacy ``server_app``
|
|
5
|
+
namespace while keeping ``app_factory._build`` focused on wiring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_user_key_runtime(
|
|
14
|
+
*,
|
|
15
|
+
load_users: Any,
|
|
16
|
+
save_users: Any,
|
|
17
|
+
ensure_user_identity: Any,
|
|
18
|
+
keyring: Any,
|
|
19
|
+
allow_plaintext_api_keys: bool,
|
|
20
|
+
logging: Any,
|
|
21
|
+
http_exception: Any,
|
|
22
|
+
) -> Dict[str, Any]:
|
|
23
|
+
"""Return history-user and provider API-key helpers."""
|
|
24
|
+
|
|
25
|
+
def get_history_user(email: Optional[str], nickname: Optional[str] = None) -> Dict:
|
|
26
|
+
if not email:
|
|
27
|
+
return {"user_email": None, "user_nickname": nickname or None}
|
|
28
|
+
users = load_users()
|
|
29
|
+
user = users.get(email, {})
|
|
30
|
+
return {
|
|
31
|
+
"user_email": email,
|
|
32
|
+
"user_nickname": nickname or user.get("nickname") or user.get("name") or email,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def get_user_api_key(email: Optional[str], provider: str) -> Optional[str]:
|
|
36
|
+
if not email:
|
|
37
|
+
return None
|
|
38
|
+
keyring_key = f"{email}:{provider}"
|
|
39
|
+
if keyring is not None:
|
|
40
|
+
try:
|
|
41
|
+
key = keyring.get_password("LatticeAI", keyring_key)
|
|
42
|
+
if key:
|
|
43
|
+
return key.strip()
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
logging.warning("keyring read failed for %s: %s", provider, exc)
|
|
46
|
+
users = load_users()
|
|
47
|
+
user = users.get(email) or {}
|
|
48
|
+
api_keys = user.get("api_keys") or {}
|
|
49
|
+
key = api_keys.get(provider)
|
|
50
|
+
if isinstance(key, str) and key.strip() and allow_plaintext_api_keys:
|
|
51
|
+
return key.strip()
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def set_user_api_key(email: str, provider: str, key: str) -> None:
|
|
55
|
+
keyring_key = f"{email}:{provider}"
|
|
56
|
+
if keyring is not None:
|
|
57
|
+
try:
|
|
58
|
+
keyring.set_password("LatticeAI", keyring_key, key)
|
|
59
|
+
users = load_users()
|
|
60
|
+
user = users.get(email)
|
|
61
|
+
if user and "api_keys" in user:
|
|
62
|
+
user["api_keys"].pop(provider, None)
|
|
63
|
+
if not user["api_keys"]:
|
|
64
|
+
user.pop("api_keys", None)
|
|
65
|
+
save_users(users)
|
|
66
|
+
return
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
logging.warning("keyring write failed for %s: %s", provider, exc)
|
|
69
|
+
if not allow_plaintext_api_keys:
|
|
70
|
+
raise http_exception(
|
|
71
|
+
status_code=500,
|
|
72
|
+
detail="OS keyring에 API 키를 저장하지 못했습니다. keyring 설정을 확인하거나 LATTICEAI_ALLOW_PLAINTEXT_API_KEYS=true를 명시적으로 설정하세요.",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not allow_plaintext_api_keys:
|
|
76
|
+
raise http_exception(
|
|
77
|
+
status_code=500,
|
|
78
|
+
detail="keyring 패키지를 사용할 수 없어 API 키를 안전하게 저장할 수 없습니다.",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
users = load_users()
|
|
82
|
+
user = users.get(email)
|
|
83
|
+
if not user:
|
|
84
|
+
user = {
|
|
85
|
+
"password_hash": "",
|
|
86
|
+
"salt": "",
|
|
87
|
+
"name": email,
|
|
88
|
+
"nickname": email,
|
|
89
|
+
"role": "user",
|
|
90
|
+
"disabled": False,
|
|
91
|
+
}
|
|
92
|
+
ensure_user_identity(email, user)
|
|
93
|
+
api_keys = user.get("api_keys") or {}
|
|
94
|
+
api_keys[provider] = key
|
|
95
|
+
user["api_keys"] = api_keys
|
|
96
|
+
users[email] = user
|
|
97
|
+
save_users(users)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"get_history_user": get_history_user,
|
|
101
|
+
"get_user_api_key": get_user_api_key,
|
|
102
|
+
"set_user_api_key": set_user_api_key,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = ["build_user_key_runtime"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Machine-checkable architecture readiness gates for release work.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
9.0.0 keeps the major architecture priorities under an explicit release
|
|
4
4
|
contract while product maturity work reduces visible beta seams. AgentRuntime, ToolRegistry,
|
|
5
5
|
central Config, decomposed server runtime, and Knowledge Graph stabilization
|
|
6
6
|
must remain discoverable, ordered, and backed by tests before the release can be
|
|
@@ -17,7 +17,7 @@ from typing import Any, Dict, List
|
|
|
17
17
|
from latticeai.core.legacy_compatibility import legacy_shim_report
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
ARCHITECTURE_VERSION_TARGET = "
|
|
20
|
+
ARCHITECTURE_VERSION_TARGET = "9.0.0"
|
|
21
21
|
|
|
22
22
|
PREFERRED_REFACTORING_ORDER = [
|
|
23
23
|
"agent-runtime",
|