ltcai 9.0.0 → 9.2.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 +88 -46
- package/auto_setup.py +7 -853
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +178 -2
- package/docs/COMMUNITY_AND_PLUGINS.md +4 -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/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 +1 -6
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/graph/_kg_fsutil.py +1 -5
- package/lattice_brain/graph/discovery_index.py +174 -20
- 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 +140 -39
- package/lattice_brain/graph/retrieval_docgen.py +37 -4
- package/lattice_brain/ingestion.py +4 -7
- package/lattice_brain/portability.py +28 -14
- package/lattice_brain/runtime/agent_runtime.py +5 -9
- 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 +20 -2
- 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 +320 -850
- 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 +28 -1
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +316 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +62 -9
- 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/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +67 -26
- 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 +96 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +110 -52
- package/latticeai/core/agent.py +50 -13
- package/latticeai/core/agent_prompts.py +5 -0
- 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/file_generation.py +451 -0
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +9 -1
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/marketplace.py +1 -1
- 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 +1 -5
- 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 +0 -5
- 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 +94 -43
- package/latticeai/models/router.py +130 -36
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/automation_runtime.py +13 -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/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +62 -72
- 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/stages.py +27 -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 +55 -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 +83 -23
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +12 -0
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +4 -9
- package/latticeai/services/search_service.py +203 -28
- 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 +1 -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 -1260
- 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-C3dBrWE-.js +1 -0
- package/static/app/assets/Brain-DBYgdcjt.js +321 -0
- package/static/app/assets/Capture-Cf3hqRtN.js +1 -0
- package/static/app/assets/Library-CFfkNn3s.js +1 -0
- package/static/app/assets/System-BOurbT-v.js +1 -0
- package/static/app/assets/index-A3M9sElj.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-DcUUmhdC.js +1 -0
- package/static/app/assets/textarea-BklR6zN4.js +1 -0
- package/static/app/index.html +2 -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/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-21lIXx2E.js +0 -1
- package/static/app/assets/Brain-BqUd5UJJ.js +0 -321
- package/static/app/assets/Capture-BA7Z2Q1u.js +0 -1
- package/static/app/assets/Library-bFMtyni3.js +0 -1
- package/static/app/assets/System-K6krGCqn.js +0 -1
- package/static/app/assets/index-C4R3ws30.js +0 -17
- package/static/app/assets/index-ChSeOB02.css +0 -2
- package/static/app/assets/primitives-sQU3it5I.js +0 -1
- package/static/app/assets/textarea-DK3Fd_lR.js +0 -1
- package/tools/knowledge.py +0 -95
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import secrets
|
|
5
8
|
import subprocess
|
|
9
|
+
import time
|
|
6
10
|
from dataclasses import dataclass
|
|
7
11
|
from pathlib import Path
|
|
8
12
|
from typing import Callable, Optional
|
|
@@ -26,6 +30,66 @@ PRODUCTION_CSP = (
|
|
|
26
30
|
"frame-ancestors 'none'"
|
|
27
31
|
)
|
|
28
32
|
|
|
33
|
+
INVITE_COOKIE_NAME = "lattice_invite"
|
|
34
|
+
INVITE_COOKIE_TTL_SECONDS = 60 * 60 * 24 * 7
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _sign_invite_cookie(secret: str, *, now: Optional[int] = None) -> str:
|
|
38
|
+
"""Create an expiring, nonce-bearing invite-gate capability cookie."""
|
|
39
|
+
|
|
40
|
+
issued_at = int(time.time() if now is None else now)
|
|
41
|
+
expires_at = issued_at + INVITE_COOKIE_TTL_SECONDS
|
|
42
|
+
nonce = secrets.token_urlsafe(24)
|
|
43
|
+
payload = f"{expires_at}.{nonce}"
|
|
44
|
+
signature = hmac.new(
|
|
45
|
+
str(secret).encode("utf-8"),
|
|
46
|
+
payload.encode("utf-8"),
|
|
47
|
+
hashlib.sha256,
|
|
48
|
+
).hexdigest()
|
|
49
|
+
return f"v1.{payload}.{signature}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _verify_invite_cookie(
|
|
53
|
+
value: Optional[str],
|
|
54
|
+
secret: str,
|
|
55
|
+
*,
|
|
56
|
+
now: Optional[int] = None,
|
|
57
|
+
) -> bool:
|
|
58
|
+
"""Verify version, expiry and HMAC without trusting client claims."""
|
|
59
|
+
|
|
60
|
+
if not value or not secret:
|
|
61
|
+
return False
|
|
62
|
+
try:
|
|
63
|
+
version, raw_expiry, nonce, supplied_signature = value.split(".", 3)
|
|
64
|
+
expires_at = int(raw_expiry)
|
|
65
|
+
except (TypeError, ValueError):
|
|
66
|
+
return False
|
|
67
|
+
if version != "v1" or not nonce or expires_at <= int(time.time() if now is None else now):
|
|
68
|
+
return False
|
|
69
|
+
payload = f"{expires_at}.{nonce}"
|
|
70
|
+
expected_signature = hmac.new(
|
|
71
|
+
str(secret).encode("utf-8"),
|
|
72
|
+
payload.encode("utf-8"),
|
|
73
|
+
hashlib.sha256,
|
|
74
|
+
).hexdigest()
|
|
75
|
+
return hmac.compare_digest(supplied_signature, expected_signature)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _invite_denied_response() -> HTMLResponse:
|
|
79
|
+
return HTMLResponse(
|
|
80
|
+
content="""
|
|
81
|
+
<body style="background:#0f1115; color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; font-family:sans-serif;">
|
|
82
|
+
<div style="background:#16191f; padding:40px; border-radius:24px; border:1px solid rgba(255,255,255,0.1); text-align:center; box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
|
|
83
|
+
<div style="font-size:48px; margin-bottom:20px;">🔒</div>
|
|
84
|
+
<h1 style="color:#378ADD; margin:0; font-size:24px;">Invitation Required</h1>
|
|
85
|
+
<p style="color:#94a3b8; margin:20px 0; line-height:1.6;">이 서비스는 비공개로 운영되고 있습니다.<br>선생님께 받은 <b>초대용 전용 링크</b>를 통해 접속해 주세요.</p>
|
|
86
|
+
<div style="margin-top:30px; padding-top:20px; border-top:1px solid rgba(255,255,255,0.05); font-size:11px; color:rgba(255,255,255,0.2); letter-spacing:1px;">LATTICE AI</div>
|
|
87
|
+
</div>
|
|
88
|
+
</body>
|
|
89
|
+
""",
|
|
90
|
+
status_code=403,
|
|
91
|
+
)
|
|
92
|
+
|
|
29
93
|
|
|
30
94
|
def ui_file_response(path: Path) -> FileResponse:
|
|
31
95
|
response = FileResponse(path)
|
|
@@ -40,6 +104,7 @@ class StaticRoutesBundle:
|
|
|
40
104
|
router: APIRouter
|
|
41
105
|
ui_file_response: Callable[[Path], FileResponse]
|
|
42
106
|
local_sysinfo: Callable[[Request], object]
|
|
107
|
+
invite_authorized: Callable[[Request], bool]
|
|
43
108
|
|
|
44
109
|
|
|
45
110
|
def create_static_routes_router(
|
|
@@ -50,46 +115,69 @@ def create_static_routes_router(
|
|
|
50
115
|
app_mode: str,
|
|
51
116
|
model_router,
|
|
52
117
|
require_user,
|
|
118
|
+
invite_cookie_secret: str = "",
|
|
119
|
+
secure_cookies: bool = False,
|
|
53
120
|
) -> StaticRoutesBundle:
|
|
54
121
|
api_router = APIRouter()
|
|
55
122
|
STATIC_DIR = static_dir
|
|
56
123
|
INVITE_GATE_ENABLED = invite_gate_enabled
|
|
57
124
|
INVITE_CODE = invite_code
|
|
125
|
+
INVITE_COOKIE_SECRET = invite_cookie_secret or secrets.token_urlsafe(48)
|
|
126
|
+
SECURE_COOKIES = bool(secure_cookies)
|
|
58
127
|
APP_MODE = app_mode
|
|
59
128
|
router = model_router
|
|
60
129
|
|
|
130
|
+
def invite_authorized(request: Request) -> bool:
|
|
131
|
+
"""Verify the request's signed invite claim at every gated boundary."""
|
|
132
|
+
|
|
133
|
+
if not INVITE_GATE_ENABLED:
|
|
134
|
+
return True
|
|
135
|
+
return _verify_invite_cookie(
|
|
136
|
+
request.cookies.get(INVITE_COOKIE_NAME),
|
|
137
|
+
INVITE_COOKIE_SECRET,
|
|
138
|
+
)
|
|
139
|
+
|
|
61
140
|
@api_router.get("/")
|
|
62
|
-
async def root(
|
|
141
|
+
async def root(
|
|
142
|
+
request: Request,
|
|
143
|
+
code: Optional[str] = None,
|
|
144
|
+
invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
|
|
145
|
+
):
|
|
63
146
|
"""로그인/회원가입 페이지. 초대 게이트 활성화 시 코드 검증 후 진입."""
|
|
64
147
|
if not INVITE_GATE_ENABLED:
|
|
65
148
|
return app_redirect("account", request)
|
|
66
|
-
|
|
67
|
-
# 1.
|
|
68
|
-
if
|
|
69
|
-
return app_redirect("account"
|
|
70
|
-
|
|
149
|
+
|
|
150
|
+
# 1. 유효한 서버 서명 쿠키가 있는 경우
|
|
151
|
+
if invite_authorized(request):
|
|
152
|
+
return app_redirect("account")
|
|
153
|
+
|
|
71
154
|
# 2. 초대 코드가 일치하는 경우 (최초 진입)
|
|
72
|
-
if code
|
|
73
|
-
|
|
74
|
-
response
|
|
155
|
+
if INVITE_CODE and code and secrets.compare_digest(code, INVITE_CODE):
|
|
156
|
+
# Do not retain the invitation code in the redirect URL/history.
|
|
157
|
+
response = app_redirect("account")
|
|
158
|
+
response.set_cookie(
|
|
159
|
+
key=INVITE_COOKIE_NAME,
|
|
160
|
+
value=_sign_invite_cookie(INVITE_COOKIE_SECRET),
|
|
161
|
+
httponly=True,
|
|
162
|
+
secure=SECURE_COOKIES,
|
|
163
|
+
samesite="lax",
|
|
164
|
+
max_age=INVITE_COOKIE_TTL_SECONDS,
|
|
165
|
+
path="/",
|
|
166
|
+
)
|
|
75
167
|
return response
|
|
76
|
-
|
|
168
|
+
|
|
77
169
|
# 3. 인증 실패 시 차단 화면
|
|
78
|
-
return
|
|
79
|
-
<body style="background:#0f1115; color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; font-family:sans-serif;">
|
|
80
|
-
<div style="background:#16191f; padding:40px; border-radius:24px; border:1px solid rgba(255,255,255,0.1); text-align:center; box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
|
|
81
|
-
<div style="font-size:48px; margin-bottom:20px;">🔒</div>
|
|
82
|
-
<h1 style="color:#378ADD; margin:0; font-size:24px;">Invitation Required</h1>
|
|
83
|
-
<p style="color:#94a3b8; margin:20px 0; line-height:1.6;">이 서비스는 비공개로 운영되고 있습니다.<br>선생님께 받은 <b>초대용 전용 링크</b>를 통해 접속해 주세요.</p>
|
|
84
|
-
<div style="margin-top:30px; padding-top:20px; border-top:1px solid rgba(255,255,255,0.05); font-size:11px; color:rgba(255,255,255,0.2); letter-spacing:1px;">LATTICE AI</div>
|
|
85
|
-
</div>
|
|
86
|
-
</body>
|
|
87
|
-
""", status_code=403)
|
|
170
|
+
return _invite_denied_response()
|
|
88
171
|
|
|
89
172
|
|
|
90
173
|
@api_router.get("/account")
|
|
91
|
-
async def account_page(
|
|
174
|
+
async def account_page(
|
|
175
|
+
request: Request,
|
|
176
|
+
invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
|
|
177
|
+
):
|
|
92
178
|
"""Direct login/register page route used by logout and manual navigation."""
|
|
179
|
+
if INVITE_GATE_ENABLED and not invite_authorized(request):
|
|
180
|
+
return _invite_denied_response()
|
|
93
181
|
return app_redirect("account")
|
|
94
182
|
|
|
95
183
|
|
|
@@ -127,8 +215,13 @@ def create_static_routes_router(
|
|
|
127
215
|
|
|
128
216
|
|
|
129
217
|
@api_router.get("/app")
|
|
130
|
-
async def app_shell(
|
|
218
|
+
async def app_shell(
|
|
219
|
+
request: Request,
|
|
220
|
+
invite_cookie: Optional[str] = Cookie(None, alias=INVITE_COOKIE_NAME),
|
|
221
|
+
):
|
|
131
222
|
"""React desktop single-page workspace shell."""
|
|
223
|
+
if INVITE_GATE_ENABLED and not invite_authorized(request):
|
|
224
|
+
return _invite_denied_response()
|
|
132
225
|
page = STATIC_DIR / "app" / "index.html"
|
|
133
226
|
if not page.exists():
|
|
134
227
|
raise HTTPException(status_code=404, detail="React shell not found.")
|
|
@@ -143,8 +236,9 @@ def create_static_routes_router(
|
|
|
143
236
|
# (latticeai.api.workspace), included below after its dependencies are defined.
|
|
144
237
|
|
|
145
238
|
@api_router.get("/status")
|
|
146
|
-
async def status():
|
|
239
|
+
async def status(request: Request):
|
|
147
240
|
"""서버 상태 및 현재 로드된 모델 정보를 반환합니다."""
|
|
241
|
+
require_user(request)
|
|
148
242
|
return {
|
|
149
243
|
"message": "🧠 Lattice AI MLX Server is running!",
|
|
150
244
|
"status": "online",
|
|
@@ -193,4 +287,9 @@ def create_static_routes_router(
|
|
|
193
287
|
result["error"] = str(e)
|
|
194
288
|
return result
|
|
195
289
|
|
|
196
|
-
return StaticRoutesBundle(
|
|
290
|
+
return StaticRoutesBundle(
|
|
291
|
+
api_router,
|
|
292
|
+
ui_file_response,
|
|
293
|
+
local_sysinfo,
|
|
294
|
+
invite_authorized,
|
|
295
|
+
)
|
package/latticeai/api/tools.py
CHANGED
|
@@ -31,8 +31,7 @@ from latticeai.services.tool_dispatch import (
|
|
|
31
31
|
tool_registry_manifest,
|
|
32
32
|
)
|
|
33
33
|
from latticeai.services.router_context import ToolRouterContext
|
|
34
|
-
from latticeai.
|
|
35
|
-
from tools import (
|
|
34
|
+
from latticeai.tools import (
|
|
36
35
|
AGENT_ROOT,
|
|
37
36
|
ToolError,
|
|
38
37
|
build_project,
|
|
@@ -51,6 +50,7 @@ from tools import (
|
|
|
51
50
|
inspect_html,
|
|
52
51
|
knowledge_save,
|
|
53
52
|
knowledge_search,
|
|
53
|
+
knowledge_scope_root,
|
|
54
54
|
knowledge_tree,
|
|
55
55
|
list_dir,
|
|
56
56
|
network_status,
|
|
@@ -207,6 +207,7 @@ def create_tools_router(
|
|
|
207
207
|
mcp_public_item=None,
|
|
208
208
|
hooks=None,
|
|
209
209
|
allowed_workspaces_for=None,
|
|
210
|
+
workspace_service=None,
|
|
210
211
|
) -> APIRouter:
|
|
211
212
|
if tool_context is not None:
|
|
212
213
|
config = tool_context.config
|
|
@@ -233,6 +234,7 @@ def create_tools_router(
|
|
|
233
234
|
mcp_public_item = tool_context.mcp_public_item
|
|
234
235
|
hooks = tool_context.hooks
|
|
235
236
|
allowed_workspaces_for = tool_context.allowed_workspaces_for
|
|
237
|
+
workspace_service = tool_context.workspace_service
|
|
236
238
|
|
|
237
239
|
api_router = APIRouter()
|
|
238
240
|
HOOKS = hooks
|
|
@@ -313,6 +315,37 @@ def create_tools_router(
|
|
|
313
315
|
if require_auth and user_email and allowed_workspaces_for is not None:
|
|
314
316
|
scope["allowed_workspaces"] = allowed_workspaces_for(user_email)
|
|
315
317
|
return scope
|
|
318
|
+
|
|
319
|
+
def _requested_workspace(request: Request) -> Optional[str]:
|
|
320
|
+
return (
|
|
321
|
+
request.headers.get("X-Workspace-Id")
|
|
322
|
+
or request.query_params.get("workspace_id")
|
|
323
|
+
or None
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _knowledge_scope(request: Request, current_user: str, *, write: bool) -> Dict[str, str]:
|
|
327
|
+
# Preserve the historical shared vault only for explicit single-user,
|
|
328
|
+
# no-auth local mode. Authenticated deployments always partition by
|
|
329
|
+
# both authorized workspace and account.
|
|
330
|
+
if not bool(getattr(CONFIG, "require_auth", False)):
|
|
331
|
+
return {}
|
|
332
|
+
requested = _requested_workspace(request)
|
|
333
|
+
try:
|
|
334
|
+
if workspace_service is not None:
|
|
335
|
+
resolver = (
|
|
336
|
+
workspace_service.resolve_write_scope
|
|
337
|
+
if write
|
|
338
|
+
else workspace_service.resolve_read_scope
|
|
339
|
+
)
|
|
340
|
+
workspace_id = resolver(requested, current_user)
|
|
341
|
+
else:
|
|
342
|
+
workspace_id = requested or "personal"
|
|
343
|
+
allowed = allowed_workspaces_for(current_user) if allowed_workspaces_for else None
|
|
344
|
+
if allowed is not None and workspace_id not in set(allowed):
|
|
345
|
+
raise PermissionError(f"workspace '{workspace_id}' is not readable")
|
|
346
|
+
except PermissionError as exc:
|
|
347
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
348
|
+
return {"workspace_id": str(workspace_id), "user_email": current_user}
|
|
316
349
|
|
|
317
350
|
|
|
318
351
|
@api_router.post("/tools/list_dir")
|
|
@@ -437,14 +470,23 @@ def create_tools_router(
|
|
|
437
470
|
@api_router.post("/tools/read_document")
|
|
438
471
|
async def tools_read_document(req: ToolPathRequest, request: Request):
|
|
439
472
|
current_user = require_user(request)
|
|
440
|
-
|
|
473
|
+
raw_path = Path(req.path).expanduser()
|
|
474
|
+
target = raw_path.resolve() if raw_path.is_absolute() else (AGENT_ROOT / raw_path).resolve()
|
|
475
|
+
inside_agent_workspace = target == AGENT_ROOT or AGENT_ROOT in target.parents
|
|
476
|
+
if not inside_agent_workspace:
|
|
441
477
|
permission_gateway.require_local_approval(
|
|
442
478
|
token=req.approval_token,
|
|
443
|
-
path=
|
|
479
|
+
path=str(target),
|
|
444
480
|
action="read",
|
|
445
481
|
user_email=current_user,
|
|
446
482
|
)
|
|
447
|
-
return _tool_response(
|
|
483
|
+
return _tool_response(
|
|
484
|
+
read_document,
|
|
485
|
+
str(target),
|
|
486
|
+
current_user=current_user,
|
|
487
|
+
source="workspace" if inside_agent_workspace else "approved_local",
|
|
488
|
+
trusted_admin=True,
|
|
489
|
+
)
|
|
448
490
|
|
|
449
491
|
|
|
450
492
|
@api_router.get("/tools/pdf_pages")
|
|
@@ -518,12 +560,14 @@ def create_tools_router(
|
|
|
518
560
|
append_audit_event=append_audit_event,
|
|
519
561
|
enforce_rate_limit=enforce_rate_limit,
|
|
520
562
|
hooks=HOOKS,
|
|
563
|
+
workspace_service=workspace_service,
|
|
521
564
|
)
|
|
522
565
|
|
|
523
566
|
|
|
524
567
|
api_router.include_router(permissions_router)
|
|
525
568
|
api_router.include_router(create_local_files_router(
|
|
526
569
|
require_user=require_user,
|
|
570
|
+
require_admin=require_admin,
|
|
527
571
|
tool_response=_tool_response,
|
|
528
572
|
permission_gateway=permission_gateway,
|
|
529
573
|
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
@@ -534,6 +578,7 @@ def create_tools_router(
|
|
|
534
578
|
hooks=HOOKS,
|
|
535
579
|
data_dir=DATA_DIR,
|
|
536
580
|
allowed_workspaces_for=allowed_workspaces_for,
|
|
581
|
+
workspace_service=workspace_service,
|
|
537
582
|
))
|
|
538
583
|
api_router.include_router(create_computer_use_router(
|
|
539
584
|
model_router=router,
|
|
@@ -542,51 +587,86 @@ def create_tools_router(
|
|
|
542
587
|
save_to_history=save_to_history,
|
|
543
588
|
hooks=HOOKS,
|
|
544
589
|
append_audit_event=append_audit_event,
|
|
590
|
+
workspace_service=workspace_service,
|
|
545
591
|
))
|
|
546
592
|
|
|
547
593
|
@api_router.post("/tools/knowledge_save")
|
|
548
594
|
async def tools_knowledge_save(req: ToolKnowledgeSaveRequest, request: Request):
|
|
549
595
|
current_user = require_user(request)
|
|
550
|
-
|
|
596
|
+
scope = _knowledge_scope(request, current_user, write=True)
|
|
597
|
+
return _tool_response(
|
|
598
|
+
knowledge_save,
|
|
599
|
+
req.content,
|
|
600
|
+
req.folder,
|
|
601
|
+
req.title,
|
|
602
|
+
current_user=current_user,
|
|
603
|
+
**scope,
|
|
604
|
+
)
|
|
551
605
|
|
|
552
606
|
|
|
553
607
|
@api_router.post("/tools/knowledge_search")
|
|
554
608
|
async def tools_knowledge_search(req: ToolKnowledgeSearchRequest, request: Request):
|
|
555
609
|
current_user = require_user(request)
|
|
556
|
-
|
|
610
|
+
scope = _knowledge_scope(request, current_user, write=False)
|
|
611
|
+
return _tool_response(
|
|
612
|
+
knowledge_search,
|
|
613
|
+
req.query,
|
|
614
|
+
req.max_results,
|
|
615
|
+
current_user=current_user,
|
|
616
|
+
**scope,
|
|
617
|
+
)
|
|
557
618
|
|
|
558
619
|
|
|
559
620
|
@api_router.get("/tools/knowledge_tree")
|
|
560
621
|
async def tools_knowledge_tree(request: Request):
|
|
561
622
|
current_user = require_user(request)
|
|
562
|
-
|
|
623
|
+
scope = _knowledge_scope(request, current_user, write=False)
|
|
624
|
+
return _tool_response(knowledge_tree, current_user=current_user, **scope)
|
|
563
625
|
|
|
564
626
|
|
|
565
627
|
@api_router.post("/tools/obsidian_save")
|
|
566
628
|
async def tools_obsidian_save(req: ToolKnowledgeSaveRequest, request: Request):
|
|
567
629
|
current_user = require_user(request)
|
|
568
|
-
|
|
630
|
+
scope = _knowledge_scope(request, current_user, write=True)
|
|
631
|
+
return _tool_response(
|
|
632
|
+
obsidian_save,
|
|
633
|
+
req.content,
|
|
634
|
+
req.folder,
|
|
635
|
+
req.title,
|
|
636
|
+
current_user=current_user,
|
|
637
|
+
**scope,
|
|
638
|
+
)
|
|
569
639
|
|
|
570
640
|
|
|
571
641
|
@api_router.post("/tools/obsidian_search")
|
|
572
642
|
async def tools_obsidian_search(req: ToolKnowledgeSearchRequest, request: Request):
|
|
573
643
|
current_user = require_user(request)
|
|
574
|
-
|
|
644
|
+
scope = _knowledge_scope(request, current_user, write=False)
|
|
645
|
+
return _tool_response(
|
|
646
|
+
obsidian_search,
|
|
647
|
+
req.query,
|
|
648
|
+
req.max_results,
|
|
649
|
+
current_user=current_user,
|
|
650
|
+
**scope,
|
|
651
|
+
)
|
|
575
652
|
|
|
576
653
|
|
|
577
654
|
@api_router.get("/tools/obsidian_tree")
|
|
578
655
|
async def tools_obsidian_tree(request: Request):
|
|
579
656
|
current_user = require_user(request)
|
|
580
|
-
|
|
657
|
+
scope = _knowledge_scope(request, current_user, write=False)
|
|
658
|
+
return _tool_response(obsidian_tree, current_user=current_user, **scope)
|
|
581
659
|
|
|
582
660
|
|
|
583
661
|
@api_router.get("/obsidian/status")
|
|
584
662
|
async def obsidian_status(request: Request):
|
|
585
|
-
require_user(request)
|
|
663
|
+
current_user = require_user(request)
|
|
664
|
+
scope = _knowledge_scope(request, current_user, write=False)
|
|
665
|
+
root = knowledge_scope_root(**scope)
|
|
586
666
|
return {
|
|
587
667
|
"status": "ok",
|
|
588
|
-
"vault_root": str(
|
|
589
|
-
"folders": [path.name for path in
|
|
668
|
+
"vault_root": str(root),
|
|
669
|
+
"folders": [path.name for path in root.iterdir() if path.is_dir()] if root.exists() else [],
|
|
590
670
|
"ocr_engine": shutil.which("tesseract") or None,
|
|
591
671
|
}
|
|
592
672
|
|
|
@@ -680,6 +760,8 @@ def create_tools_router(
|
|
|
680
760
|
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
681
761
|
ingestion_pipeline=ingestion_pipeline,
|
|
682
762
|
data_dir=DATA_DIR,
|
|
763
|
+
allowed_workspaces_for=allowed_workspaces_for,
|
|
764
|
+
workspace_service=workspace_service,
|
|
683
765
|
))
|
|
684
766
|
|
|
685
767
|
return api_router
|
|
@@ -13,6 +13,7 @@ actually drive plugins, skills, and multi-agent runs.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
from copy import deepcopy
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any, Callable, Dict, List, Optional
|
|
18
19
|
|
|
@@ -283,6 +284,42 @@ def create_workflow_designer_router(
|
|
|
283
284
|
store.list_workflows(workspace_id=scope).get("workflows"), recipe_id
|
|
284
285
|
)
|
|
285
286
|
if existing is not None:
|
|
287
|
+
existing_metadata = existing.get("metadata") or {}
|
|
288
|
+
if req.enabled and existing_metadata.get("automation_state") != "enabled":
|
|
289
|
+
enabled_nodes = deepcopy(existing.get("nodes") or definition["nodes"])
|
|
290
|
+
for node in enabled_nodes:
|
|
291
|
+
if node.get("type") != "trigger":
|
|
292
|
+
continue
|
|
293
|
+
node["config"] = {
|
|
294
|
+
**(node.get("config") or {}),
|
|
295
|
+
"enabled": True,
|
|
296
|
+
"review_queue": True,
|
|
297
|
+
"consent_required": True,
|
|
298
|
+
}
|
|
299
|
+
errors = validate_definition({
|
|
300
|
+
"name": existing.get("name") or definition["name"],
|
|
301
|
+
"nodes": enabled_nodes,
|
|
302
|
+
})
|
|
303
|
+
if errors:
|
|
304
|
+
raise HTTPException(status_code=400, detail={"validation_errors": errors})
|
|
305
|
+
enabled_metadata = {
|
|
306
|
+
**existing_metadata,
|
|
307
|
+
"automation_state": "enabled",
|
|
308
|
+
"requires_user_enable": False,
|
|
309
|
+
}
|
|
310
|
+
existing = store.update_workflow_definition(
|
|
311
|
+
str(existing.get("id") or existing.get("workflow_id") or ""),
|
|
312
|
+
name=existing.get("name") or definition["name"],
|
|
313
|
+
nodes=enabled_nodes,
|
|
314
|
+
metadata=enabled_metadata,
|
|
315
|
+
workspace_id=scope,
|
|
316
|
+
)
|
|
317
|
+
append_audit_event(
|
|
318
|
+
"brain_automation_recipe_enabled",
|
|
319
|
+
user_email=current_user,
|
|
320
|
+
workflow_id=existing.get("id"),
|
|
321
|
+
recipe_id=recipe_id,
|
|
322
|
+
)
|
|
286
323
|
return {
|
|
287
324
|
"workflow": existing,
|
|
288
325
|
"recipe": existing.get("metadata") or definition["metadata"],
|
|
@@ -290,7 +290,7 @@ def create_workspace_router(context: AppContext) -> APIRouter:
|
|
|
290
290
|
# not_recommended) for this machine, used by the onboarding model step.
|
|
291
291
|
catalog = None
|
|
292
292
|
try:
|
|
293
|
-
from auto_setup import probe as auto_setup_probe
|
|
293
|
+
from latticeai.setup.auto_setup import probe as auto_setup_probe
|
|
294
294
|
from latticeai.services.model_recommendation import recommend_catalog
|
|
295
295
|
profile = await asyncio.to_thread(lambda: auto_setup_probe().to_json())
|
|
296
296
|
catalog = recommend_catalog(profile, engine="local_mlx")
|
|
@@ -714,6 +714,7 @@ def create_workspace_router(context: AppContext) -> APIRouter:
|
|
|
714
714
|
req.action,
|
|
715
715
|
user_email=current_user or None,
|
|
716
716
|
source="vscode",
|
|
717
|
+
workspace_id=workflow.get("workspace_id"),
|
|
717
718
|
metadata={
|
|
718
719
|
"file_path": req.file_path,
|
|
719
720
|
"language": req.language,
|