ltcai 6.0.0 → 6.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 +38 -35
- package/docs/CHANGELOG.md +36 -0
- package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +1 -1
- package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +1 -1
- package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +1 -1
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +1 -1
- package/frontend/src/App.tsx +5 -0
- package/frontend/src/components/ProductFlow.tsx +7 -0
- package/frontend/src/features/review/ReviewCard.tsx +15 -10
- package/frontend/src/i18n.ts +10 -0
- package/frontend/src/styles.css +20 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +52 -33
- package/latticeai/app_factory.py +29 -2
- package/latticeai/cli/__init__.py +1 -0
- package/latticeai/cli/runtime.py +37 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/app_context.py +1 -0
- package/latticeai/services/tool_dispatch.py +82 -25
- package/ltcai_cli.py +5 -31
- package/package.json +1 -1
- 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 +5 -5
- package/static/app/assets/{index-xRn29gI8.css → index-B744yblP.css} +1 -1
- package/static/app/assets/{index-D2zafMYb.js → index-DYaUKNfl.js} +2 -2
- package/static/app/assets/index-DYaUKNfl.js.map +1 -0
- package/static/app/index.html +2 -2
- package/telegram_bot.py +3 -9
- package/static/app/assets/index-D2zafMYb.js.map +0 -1
|
@@ -6,8 +6,9 @@ tool-response shaping are owned outside ``server_app``.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
from dataclasses import dataclass, field
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Callable, Dict, Optional
|
|
11
|
+
from typing import Any, Callable, Dict, Mapping, Optional
|
|
11
12
|
|
|
12
13
|
from fastapi import HTTPException
|
|
13
14
|
|
|
@@ -30,9 +31,6 @@ def _default_get_user_role(_email, _users=None) -> str:
|
|
|
30
31
|
return "user"
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
_load_users: Callable[[], Dict[str, Any]] = _default_load_users
|
|
34
|
-
_get_user_role: Callable[..., str] = _default_get_user_role
|
|
35
|
-
|
|
36
34
|
FILE_CREATE_ACTIONS = set(DEFAULT_TOOL_REGISTRY.file_create_actions)
|
|
37
35
|
TOOL_GOVERNANCE: Dict[str, ToolPolicy] = dict(DEFAULT_TOOL_REGISTRY.governance)
|
|
38
36
|
TOOL_GOVERNANCE_DEFAULT: ToolPolicy = DEFAULT_TOOL_REGISTRY.default_policy
|
|
@@ -41,41 +39,100 @@ LOCAL_WRITE_BLOCKED_PREFIXES = DEFAULT_TOOL_REGISTRY.local_write_blocked_prefixe
|
|
|
41
39
|
RISK_LEVEL_MAP = DEFAULT_TOOL_REGISTRY.risk_level_map
|
|
42
40
|
|
|
43
41
|
|
|
42
|
+
@dataclass
|
|
43
|
+
class ToolDispatchService:
|
|
44
|
+
"""Runtime-facing tool policy and authorization boundary.
|
|
45
|
+
|
|
46
|
+
``ToolRegistry`` owns the catalog and governance facts; this service owns
|
|
47
|
+
request/runtime authorization callbacks that must be configured during app
|
|
48
|
+
construction. Keeping those callbacks on an instance gives future runtime
|
|
49
|
+
assembly code an injectable seam while the module-level functions below
|
|
50
|
+
preserve the historical ``server_app`` surface.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
registry: Any = field(default_factory=lambda: DEFAULT_TOOL_REGISTRY)
|
|
54
|
+
load_users: Callable[[], Dict[str, Any]] = field(default=_default_load_users)
|
|
55
|
+
get_user_role: Callable[..., str] = field(default=_default_get_user_role)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def file_create_actions(self) -> frozenset[str]:
|
|
59
|
+
return self.registry.file_create_actions
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def tool_governance(self) -> Mapping[str, ToolPolicy]:
|
|
63
|
+
return self.registry.governance
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def risk_level_map(self) -> Mapping[str, str]:
|
|
67
|
+
return self.registry.risk_level_map
|
|
68
|
+
|
|
69
|
+
def configure(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
load_users: Callable[[], Dict[str, Any]],
|
|
73
|
+
get_user_role: Callable[..., str],
|
|
74
|
+
) -> None:
|
|
75
|
+
self.load_users = load_users
|
|
76
|
+
self.get_user_role = get_user_role
|
|
77
|
+
|
|
78
|
+
def policy_for(self, action_name: str, args: dict) -> ToolPolicy:
|
|
79
|
+
return self.registry.policy_for(action_name, args)
|
|
80
|
+
|
|
81
|
+
def risk_level(self, action_name: str, args: dict) -> str:
|
|
82
|
+
return self.registry.risk_level(action_name, args)
|
|
83
|
+
|
|
84
|
+
def risk_level_for_policy(self, policy: ToolPolicy) -> str:
|
|
85
|
+
return self.risk_level_map.get(policy["risk"], "medium")
|
|
86
|
+
|
|
87
|
+
def permission(self, name: str, args: Optional[dict] = None) -> ToolPermission:
|
|
88
|
+
return self.registry.permission(name, args or {})
|
|
89
|
+
|
|
90
|
+
def permissions(self) -> list[ToolPermission]:
|
|
91
|
+
return self.registry.permissions()
|
|
92
|
+
|
|
93
|
+
def check_role(self, tool_name: str, current_user: str) -> None:
|
|
94
|
+
if tool_name not in self.registry.admin_only_tools:
|
|
95
|
+
return
|
|
96
|
+
users = self.load_users()
|
|
97
|
+
if self.get_user_role(current_user, users) != "admin":
|
|
98
|
+
raise HTTPException(
|
|
99
|
+
status_code=403,
|
|
100
|
+
detail=f"'{tool_name}' 툴은 관리자 전용입니다.",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
DEFAULT_TOOL_DISPATCH_SERVICE = ToolDispatchService()
|
|
105
|
+
|
|
106
|
+
|
|
44
107
|
def configure_tool_dispatch(
|
|
45
108
|
*,
|
|
46
109
|
load_users: Callable[[], Dict[str, Any]],
|
|
47
110
|
get_user_role: Callable[..., str],
|
|
48
111
|
) -> None:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
112
|
+
DEFAULT_TOOL_DISPATCH_SERVICE.configure(
|
|
113
|
+
load_users=load_users,
|
|
114
|
+
get_user_role=get_user_role,
|
|
115
|
+
)
|
|
52
116
|
|
|
53
117
|
|
|
54
118
|
def agent_policy(action_name: str, args: dict) -> ToolPolicy:
|
|
55
|
-
return
|
|
119
|
+
return DEFAULT_TOOL_DISPATCH_SERVICE.policy_for(action_name, args)
|
|
56
120
|
|
|
57
121
|
|
|
58
122
|
def agent_risk(action_name: str, args: dict) -> str:
|
|
59
|
-
return
|
|
123
|
+
return DEFAULT_TOOL_DISPATCH_SERVICE.risk_level(action_name, args)
|
|
60
124
|
|
|
61
125
|
|
|
62
126
|
def get_tool_permission(name: str, args: Optional[dict] = None) -> ToolPermission:
|
|
63
|
-
return
|
|
127
|
+
return DEFAULT_TOOL_DISPATCH_SERVICE.permission(name, args or {})
|
|
64
128
|
|
|
65
129
|
|
|
66
130
|
def list_tool_permissions() -> list:
|
|
67
|
-
return
|
|
131
|
+
return DEFAULT_TOOL_DISPATCH_SERVICE.permissions()
|
|
68
132
|
|
|
69
133
|
|
|
70
134
|
def check_tool_role(tool_name: str, current_user: str) -> None:
|
|
71
|
-
|
|
72
|
-
return
|
|
73
|
-
users = _load_users()
|
|
74
|
-
if _get_user_role(current_user, users) != "admin":
|
|
75
|
-
raise HTTPException(
|
|
76
|
-
status_code=403,
|
|
77
|
-
detail=f"'{tool_name}' 툴은 관리자 전용입니다.",
|
|
78
|
-
)
|
|
135
|
+
DEFAULT_TOOL_DISPATCH_SERVICE.check_role(tool_name, current_user)
|
|
79
136
|
|
|
80
137
|
|
|
81
138
|
def collect_created_files(transcript: list) -> list:
|
|
@@ -113,17 +170,18 @@ def build_agent_runtime(
|
|
|
113
170
|
audit: Callable[..., None],
|
|
114
171
|
hooks: Any = None,
|
|
115
172
|
brain_memory: Any = None,
|
|
173
|
+
dispatch_service: ToolDispatchService = DEFAULT_TOOL_DISPATCH_SERVICE,
|
|
116
174
|
) -> AgentRuntime:
|
|
117
175
|
ensure_agent_root()
|
|
118
176
|
deps = AgentDeps(
|
|
119
177
|
generate_as=model_router.generate_as,
|
|
120
178
|
generate=model_router.generate,
|
|
121
179
|
execute_tool=execute_tool,
|
|
122
|
-
policy_for=
|
|
123
|
-
risk_level=
|
|
124
|
-
check_role=
|
|
125
|
-
tool_governance=
|
|
126
|
-
file_create_actions=
|
|
180
|
+
policy_for=dispatch_service.policy_for,
|
|
181
|
+
risk_level=dispatch_service.risk_level_for_policy,
|
|
182
|
+
check_role=dispatch_service.check_role,
|
|
183
|
+
tool_governance=dispatch_service.tool_governance,
|
|
184
|
+
file_create_actions=dispatch_service.file_create_actions,
|
|
127
185
|
recent_chat_context=recent_chat_context,
|
|
128
186
|
clear_history=clear_history,
|
|
129
187
|
knowledge_save=knowledge_save,
|
|
@@ -144,4 +202,3 @@ def tool_response(fn, *args):
|
|
|
144
202
|
return {"status": "ok", "workspace": str(AGENT_ROOT), "result": fn(*args)}
|
|
145
203
|
except ToolError as exc:
|
|
146
204
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
147
|
-
|
package/ltcai_cli.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
-
import importlib.util
|
|
7
6
|
import os
|
|
8
7
|
import platform
|
|
9
8
|
import re
|
|
@@ -17,36 +16,11 @@ import time
|
|
|
17
16
|
import urllib.request
|
|
18
17
|
from pathlib import Path
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
line = raw_line.strip()
|
|
26
|
-
if not line or line.startswith("#") or "=" not in line:
|
|
27
|
-
continue
|
|
28
|
-
key, value = line.split("=", 1)
|
|
29
|
-
key = key.strip()
|
|
30
|
-
value = value.strip().strip('"').strip("'")
|
|
31
|
-
if key and key not in os.environ:
|
|
32
|
-
os.environ[key] = value
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _apply_extra_path() -> None:
|
|
36
|
-
extra = os.getenv("LATTICEAI_EXTRA_PATH", "")
|
|
37
|
-
if not extra:
|
|
38
|
-
return
|
|
39
|
-
current = [p for p in os.environ.get("PATH", "").split(os.pathsep) if p]
|
|
40
|
-
for item in reversed([p for p in extra.split(os.pathsep) if p]):
|
|
41
|
-
expanded = str(Path(item).expanduser())
|
|
42
|
-
if Path(expanded).exists() and expanded not in current:
|
|
43
|
-
current.insert(0, expanded)
|
|
44
|
-
os.environ["PATH"] = os.pathsep.join(current)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _has_module(name: str) -> bool:
|
|
48
|
-
return importlib.util.find_spec(name) is not None
|
|
49
|
-
|
|
19
|
+
from latticeai.cli.runtime import (
|
|
20
|
+
_apply_extra_path,
|
|
21
|
+
_has_module,
|
|
22
|
+
_load_env_file,
|
|
23
|
+
)
|
|
50
24
|
|
|
51
25
|
def _local_ips() -> list[str]:
|
|
52
26
|
ips: list[str] = []
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "6.
|
|
2
|
+
"version": "6.1.0",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
5
|
"app": "/static/app/index.html"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"index.html": "/static/app/assets/index-
|
|
10
|
-
"assets/index-
|
|
9
|
+
"index.html": "/static/app/assets/index-DYaUKNfl.js",
|
|
10
|
+
"assets/index-B744yblP.css": "/static/app/assets/index-B744yblP.css"
|
|
11
11
|
},
|
|
12
12
|
"vite": {
|
|
13
13
|
"../node_modules/@tauri-apps/api/core.js": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"isDynamicEntry": true
|
|
18
18
|
},
|
|
19
19
|
"index.html": {
|
|
20
|
-
"file": "assets/index-
|
|
20
|
+
"file": "assets/index-DYaUKNfl.js",
|
|
21
21
|
"name": "index",
|
|
22
22
|
"src": "index.html",
|
|
23
23
|
"isEntry": true,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"../node_modules/@tauri-apps/api/core.js"
|
|
26
26
|
],
|
|
27
27
|
"css": [
|
|
28
|
-
"assets/index-
|
|
28
|
+
"assets/index-B744yblP.css"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|