ltcai 1.3.0 → 1.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 +27 -19
- package/docs/CHANGELOG.md +55 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +786 -0
- package/latticeai/api/computer_use.py +294 -0
- package/latticeai/api/deps.py +15 -0
- package/latticeai/api/garden.py +34 -0
- package/latticeai/api/local_files.py +125 -0
- package/latticeai/api/permissions.py +331 -0
- package/latticeai/api/setup.py +158 -0
- package/latticeai/api/static_routes.py +166 -0
- package/latticeai/api/tools.py +579 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +223 -4301
- package/latticeai/services/app_context.py +27 -0
- package/latticeai/services/model_runtime.py +1973 -0
- package/latticeai/services/tool_dispatch.py +135 -0
- package/latticeai/services/upload_service.py +99 -0
- package/package.json +3 -3
- package/skills/SKILL_TEMPLATE.md +1 -1
- package/skills/code_review/SKILL.md +1 -1
- package/skills/data_analysis/SKILL.md +1 -1
- package/skills/file_edit/SKILL.md +1 -1
- package/skills/summarize_document/SKILL.md +1 -1
- package/skills/web_search/SKILL.md +1 -1
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"""Direct tool, upload, MCP, and knowledge utility routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import io
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, File, HTTPException, Request, UploadFile
|
|
13
|
+
from fastapi.responses import FileResponse
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from latticeai.api.computer_use import create_computer_use_router
|
|
17
|
+
from latticeai.api.local_files import create_local_files_router
|
|
18
|
+
from latticeai.api.mcp import create_mcp_router
|
|
19
|
+
from latticeai.api.permissions import create_permissions_router
|
|
20
|
+
from latticeai.services.upload_service import process_uploaded_document
|
|
21
|
+
from latticeai.services.tool_dispatch import (
|
|
22
|
+
TOOL_GOVERNANCE,
|
|
23
|
+
TOOL_GOVERNANCE_DEFAULT as _TOOL_GOVERNANCE_DEFAULT,
|
|
24
|
+
check_tool_role as _check_tool_role,
|
|
25
|
+
get_tool_permission,
|
|
26
|
+
list_tool_permissions,
|
|
27
|
+
)
|
|
28
|
+
from p_reinforce import BRAIN_DIR
|
|
29
|
+
from tools import (
|
|
30
|
+
AGENT_ROOT,
|
|
31
|
+
ToolError,
|
|
32
|
+
build_project,
|
|
33
|
+
create_docx,
|
|
34
|
+
create_pdf,
|
|
35
|
+
create_pptx,
|
|
36
|
+
create_xlsx,
|
|
37
|
+
read_document,
|
|
38
|
+
deploy_project,
|
|
39
|
+
edit_file,
|
|
40
|
+
git_diff,
|
|
41
|
+
git_log,
|
|
42
|
+
git_show,
|
|
43
|
+
git_status,
|
|
44
|
+
grep,
|
|
45
|
+
inspect_html,
|
|
46
|
+
knowledge_save,
|
|
47
|
+
knowledge_search,
|
|
48
|
+
knowledge_tree,
|
|
49
|
+
list_dir,
|
|
50
|
+
network_status,
|
|
51
|
+
obsidian_save,
|
|
52
|
+
obsidian_search,
|
|
53
|
+
obsidian_tree,
|
|
54
|
+
preview_url,
|
|
55
|
+
read_file,
|
|
56
|
+
run_command,
|
|
57
|
+
search_files,
|
|
58
|
+
todo_read,
|
|
59
|
+
todo_write,
|
|
60
|
+
workspace_tree,
|
|
61
|
+
write_file,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ToolPathRequest(BaseModel):
|
|
66
|
+
path: str = "."
|
|
67
|
+
approval_token: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ToolWriteFileRequest(BaseModel):
|
|
71
|
+
path: str
|
|
72
|
+
content: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ToolRunCommandRequest(BaseModel):
|
|
76
|
+
command: str
|
|
77
|
+
cwd: Optional[str] = "."
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ToolScriptRequest(BaseModel):
|
|
81
|
+
cwd: Optional[str] = "."
|
|
82
|
+
script: str = "build"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ToolSearchFilesRequest(BaseModel):
|
|
86
|
+
query: str
|
|
87
|
+
path: str = "."
|
|
88
|
+
max_results: int = 20
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ToolReadFileRequest(BaseModel):
|
|
92
|
+
path: str
|
|
93
|
+
offset: int = 0
|
|
94
|
+
limit: int = 0
|
|
95
|
+
line_numbers: bool = True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ToolEditFileRequest(BaseModel):
|
|
99
|
+
path: str
|
|
100
|
+
old_string: str
|
|
101
|
+
new_string: str
|
|
102
|
+
replace_all: bool = False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ToolGrepRequest(BaseModel):
|
|
106
|
+
pattern: str
|
|
107
|
+
path: str = "."
|
|
108
|
+
glob: Optional[str] = None
|
|
109
|
+
max_results: int = 50
|
|
110
|
+
case_insensitive: bool = False
|
|
111
|
+
context_lines: int = 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ToolTodoWriteRequest(BaseModel):
|
|
115
|
+
todos: List[Dict] = []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ToolWorkspaceTreeRequest(BaseModel):
|
|
119
|
+
path: str = "."
|
|
120
|
+
max_depth: int = 3
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ToolClearHistoryRequest(BaseModel):
|
|
124
|
+
keep_last: int = 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ToolKnowledgeSaveRequest(BaseModel):
|
|
128
|
+
content: str
|
|
129
|
+
folder: str = "00_Raw"
|
|
130
|
+
title: Optional[str] = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ToolKnowledgeSearchRequest(BaseModel):
|
|
134
|
+
query: str
|
|
135
|
+
max_results: int = 5
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ToolDocxRequest(BaseModel):
|
|
139
|
+
title: str = ""
|
|
140
|
+
body: str = ""
|
|
141
|
+
filename: str = "document.docx"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ToolXlsxRequest(BaseModel):
|
|
145
|
+
rows: List[List] = []
|
|
146
|
+
filename: str = "spreadsheet.xlsx"
|
|
147
|
+
sheet_name: str = "Sheet1"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ToolPptxRequest(BaseModel):
|
|
151
|
+
title: str = ""
|
|
152
|
+
slides: List[Dict] = []
|
|
153
|
+
filename: str = "presentation.pptx"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ToolPdfRequest(BaseModel):
|
|
157
|
+
title: str = ""
|
|
158
|
+
body: str = ""
|
|
159
|
+
filename: str = "document.pdf"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ToolGitDiffRequest(BaseModel):
|
|
163
|
+
path: Optional[str] = None
|
|
164
|
+
cwd: Optional[str] = "."
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ToolGitLogRequest(BaseModel):
|
|
168
|
+
max_count: int = 5
|
|
169
|
+
cwd: Optional[str] = "."
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ToolGitShowRequest(BaseModel):
|
|
173
|
+
revision: str = "HEAD"
|
|
174
|
+
cwd: Optional[str] = "."
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def create_tools_router(
|
|
178
|
+
*,
|
|
179
|
+
config,
|
|
180
|
+
data_dir: Path,
|
|
181
|
+
static_dir: Path,
|
|
182
|
+
model_router,
|
|
183
|
+
require_user,
|
|
184
|
+
require_admin,
|
|
185
|
+
get_current_user,
|
|
186
|
+
clear_history,
|
|
187
|
+
append_audit_event,
|
|
188
|
+
enforce_rate_limit,
|
|
189
|
+
bytes_match_extension,
|
|
190
|
+
classify_sensitive_message,
|
|
191
|
+
save_to_history,
|
|
192
|
+
enable_graph: bool,
|
|
193
|
+
knowledge_graph,
|
|
194
|
+
require_graph,
|
|
195
|
+
local_kg_watcher,
|
|
196
|
+
load_mcp_installs,
|
|
197
|
+
recommend_mcps,
|
|
198
|
+
install_mcp,
|
|
199
|
+
mcp_public_item,
|
|
200
|
+
) -> APIRouter:
|
|
201
|
+
api_router = APIRouter()
|
|
202
|
+
CONFIG = config
|
|
203
|
+
DATA_DIR = data_dir
|
|
204
|
+
STATIC_DIR = static_dir
|
|
205
|
+
router = model_router
|
|
206
|
+
ENABLE_GRAPH = enable_graph
|
|
207
|
+
KNOWLEDGE_GRAPH = knowledge_graph
|
|
208
|
+
LOCAL_KG_WATCHER = local_kg_watcher
|
|
209
|
+
_require_graph = require_graph
|
|
210
|
+
_bytes_match_extension = bytes_match_extension
|
|
211
|
+
permissions_router, permission_gateway = create_permissions_router(
|
|
212
|
+
config=CONFIG,
|
|
213
|
+
data_dir=DATA_DIR,
|
|
214
|
+
require_user=require_user,
|
|
215
|
+
require_admin=require_admin,
|
|
216
|
+
get_current_user=get_current_user,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# ── Direct Tool API ───────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
def _tool_response(fn, *args):
|
|
222
|
+
try:
|
|
223
|
+
return {"status": "ok", "workspace": str(AGENT_ROOT), "result": fn(*args)}
|
|
224
|
+
except ToolError as exc:
|
|
225
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@api_router.post("/tools/list_dir")
|
|
229
|
+
async def tools_list_dir(req: ToolPathRequest, request: Request):
|
|
230
|
+
require_user(request)
|
|
231
|
+
return _tool_response(list_dir, req.path)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@api_router.post("/tools/workspace_tree")
|
|
235
|
+
async def tools_workspace_tree(req: ToolWorkspaceTreeRequest, request: Request):
|
|
236
|
+
require_user(request)
|
|
237
|
+
return _tool_response(workspace_tree, req.path, req.max_depth)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@api_router.post("/tools/read_file")
|
|
241
|
+
async def tools_read_file(req: ToolReadFileRequest, request: Request):
|
|
242
|
+
require_user(request)
|
|
243
|
+
try:
|
|
244
|
+
return {"status": "ok", "workspace": str(AGENT_ROOT),
|
|
245
|
+
"result": read_file(req.path, offset=req.offset, limit=req.limit, line_numbers=req.line_numbers)}
|
|
246
|
+
except ToolError as exc:
|
|
247
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@api_router.post("/tools/write_file")
|
|
251
|
+
async def tools_write_file(req: ToolWriteFileRequest, request: Request):
|
|
252
|
+
require_user(request)
|
|
253
|
+
return _tool_response(write_file, req.path, req.content)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@api_router.post("/tools/edit_file")
|
|
257
|
+
async def tools_edit_file(req: ToolEditFileRequest, request: Request):
|
|
258
|
+
require_user(request)
|
|
259
|
+
try:
|
|
260
|
+
return {"status": "ok", "workspace": str(AGENT_ROOT),
|
|
261
|
+
"result": edit_file(req.path, req.old_string, req.new_string, replace_all=req.replace_all)}
|
|
262
|
+
except ToolError as exc:
|
|
263
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@api_router.post("/tools/search_files")
|
|
267
|
+
async def tools_search_files(req: ToolSearchFilesRequest, request: Request):
|
|
268
|
+
require_user(request)
|
|
269
|
+
return _tool_response(search_files, req.query, req.path, req.max_results)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@api_router.post("/tools/grep")
|
|
273
|
+
async def tools_grep(req: ToolGrepRequest, request: Request):
|
|
274
|
+
require_user(request)
|
|
275
|
+
try:
|
|
276
|
+
return {"status": "ok", "workspace": str(AGENT_ROOT),
|
|
277
|
+
"result": grep(
|
|
278
|
+
req.pattern,
|
|
279
|
+
path=req.path,
|
|
280
|
+
glob=req.glob,
|
|
281
|
+
max_results=req.max_results,
|
|
282
|
+
case_insensitive=req.case_insensitive,
|
|
283
|
+
context_lines=req.context_lines,
|
|
284
|
+
)}
|
|
285
|
+
except ToolError as exc:
|
|
286
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@api_router.post("/tools/todo_read")
|
|
290
|
+
async def tools_todo_read(request: Request):
|
|
291
|
+
require_user(request)
|
|
292
|
+
return _tool_response(todo_read)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@api_router.post("/tools/todo_write")
|
|
296
|
+
async def tools_todo_write(req: ToolTodoWriteRequest, request: Request):
|
|
297
|
+
require_user(request)
|
|
298
|
+
return _tool_response(todo_write, req.todos)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@api_router.post("/tools/clear_history")
|
|
302
|
+
async def tools_clear_history(req: ToolClearHistoryRequest, request: Request):
|
|
303
|
+
current_user = require_user(request)
|
|
304
|
+
result = clear_history(req.keep_last)
|
|
305
|
+
append_audit_event(
|
|
306
|
+
"history_delete",
|
|
307
|
+
user_email=current_user,
|
|
308
|
+
source="tools",
|
|
309
|
+
keep_last=req.keep_last,
|
|
310
|
+
removed=result.get("removed", 0),
|
|
311
|
+
kept=result.get("kept", 0),
|
|
312
|
+
)
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@api_router.post("/tools/inspect_html")
|
|
317
|
+
async def tools_inspect_html(req: ToolPathRequest, request: Request):
|
|
318
|
+
require_user(request)
|
|
319
|
+
return _tool_response(inspect_html, req.path)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@api_router.post("/tools/preview_url")
|
|
323
|
+
async def tools_preview_url(req: ToolPathRequest, request: Request):
|
|
324
|
+
require_user(request)
|
|
325
|
+
return _tool_response(preview_url, req.path)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@api_router.post("/tools/create_docx")
|
|
329
|
+
async def tools_create_docx(req: ToolDocxRequest, request: Request):
|
|
330
|
+
require_user(request)
|
|
331
|
+
return _tool_response(create_docx, req.title, req.body, req.filename)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@api_router.post("/tools/create_xlsx")
|
|
335
|
+
async def tools_create_xlsx(req: ToolXlsxRequest, request: Request):
|
|
336
|
+
require_user(request)
|
|
337
|
+
return _tool_response(create_xlsx, req.rows, req.filename, req.sheet_name)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@api_router.post("/tools/create_pptx")
|
|
341
|
+
async def tools_create_pptx(req: ToolPptxRequest, request: Request):
|
|
342
|
+
require_user(request)
|
|
343
|
+
return _tool_response(create_pptx, req.title, req.slides, req.filename)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@api_router.post("/tools/create_pdf")
|
|
347
|
+
async def tools_create_pdf(req: ToolPdfRequest, request: Request):
|
|
348
|
+
require_user(request)
|
|
349
|
+
return _tool_response(create_pdf, req.title, req.body, req.filename)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@api_router.post("/tools/read_document")
|
|
353
|
+
async def tools_read_document(req: ToolPathRequest, request: Request):
|
|
354
|
+
current_user = require_user(request)
|
|
355
|
+
if Path(req.path).expanduser().is_absolute():
|
|
356
|
+
permission_gateway.require_local_approval(
|
|
357
|
+
token=req.approval_token,
|
|
358
|
+
path=req.path,
|
|
359
|
+
action="read",
|
|
360
|
+
user_email=current_user,
|
|
361
|
+
)
|
|
362
|
+
return _tool_response(read_document, req.path)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@api_router.get("/tools/pdf_pages")
|
|
366
|
+
async def tools_pdf_pages(path: str, request: Request, approval_token: Optional[str] = None):
|
|
367
|
+
"""Render PDF pages as base64 PNG images using pypdfium2 (Apache-2.0)."""
|
|
368
|
+
current_user = require_user(request)
|
|
369
|
+
permission_gateway.require_local_approval(
|
|
370
|
+
token=approval_token,
|
|
371
|
+
path=path,
|
|
372
|
+
action="read",
|
|
373
|
+
user_email=current_user,
|
|
374
|
+
)
|
|
375
|
+
target = Path(path).expanduser().resolve()
|
|
376
|
+
if not target.exists() or not target.is_file():
|
|
377
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
378
|
+
import pypdfium2 as pdfium
|
|
379
|
+
doc = None
|
|
380
|
+
try:
|
|
381
|
+
doc = pdfium.PdfDocument(str(target))
|
|
382
|
+
total = len(doc)
|
|
383
|
+
pages = []
|
|
384
|
+
for i in range(min(total, 20)): # 최대 20페이지
|
|
385
|
+
page = doc[i]
|
|
386
|
+
bitmap = page.render(scale=1.5)
|
|
387
|
+
pil_image = bitmap.to_pil()
|
|
388
|
+
buf = io.BytesIO()
|
|
389
|
+
pil_image.save(buf, format="PNG")
|
|
390
|
+
b64 = base64.b64encode(buf.getvalue()).decode()
|
|
391
|
+
pages.append({"page": i + 1, "b64": b64})
|
|
392
|
+
return {"total": total, "pages": pages}
|
|
393
|
+
except Exception as e:
|
|
394
|
+
raise HTTPException(status_code=500, detail=f"PDF 렌더링 실패: {e}")
|
|
395
|
+
finally:
|
|
396
|
+
if doc is not None:
|
|
397
|
+
try:
|
|
398
|
+
doc.close()
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logging.warning("pypdfium2 doc close failed: %s", e)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@api_router.get("/tools/download")
|
|
404
|
+
async def tools_download(path: str, request: Request):
|
|
405
|
+
"""Serve a generated file from agent workspace for download."""
|
|
406
|
+
require_user(request)
|
|
407
|
+
from urllib.parse import unquote
|
|
408
|
+
rel = unquote(path).lstrip("/")
|
|
409
|
+
target = (AGENT_ROOT / rel).resolve()
|
|
410
|
+
if AGENT_ROOT not in target.parents and target != AGENT_ROOT:
|
|
411
|
+
raise HTTPException(status_code=403, detail="경로가 작업 공간 밖입니다.")
|
|
412
|
+
if not target.exists() or not target.is_file():
|
|
413
|
+
raise HTTPException(status_code=404, detail="파일이 없습니다.")
|
|
414
|
+
return FileResponse(
|
|
415
|
+
path=target,
|
|
416
|
+
filename=target.name,
|
|
417
|
+
media_type="application/octet-stream",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@api_router.post("/upload/document")
|
|
422
|
+
async def upload_document(request: Request, file: UploadFile = File(...)):
|
|
423
|
+
current_user = require_user(request)
|
|
424
|
+
return await process_uploaded_document(
|
|
425
|
+
request=request,
|
|
426
|
+
file=file,
|
|
427
|
+
current_user=current_user,
|
|
428
|
+
enable_graph=ENABLE_GRAPH,
|
|
429
|
+
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
430
|
+
bytes_match_extension=_bytes_match_extension,
|
|
431
|
+
classify_sensitive_message=classify_sensitive_message,
|
|
432
|
+
append_audit_event=append_audit_event,
|
|
433
|
+
enforce_rate_limit=enforce_rate_limit,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
api_router.include_router(permissions_router)
|
|
438
|
+
api_router.include_router(create_local_files_router(
|
|
439
|
+
require_user=require_user,
|
|
440
|
+
tool_response=_tool_response,
|
|
441
|
+
permission_gateway=permission_gateway,
|
|
442
|
+
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
443
|
+
require_graph=_require_graph,
|
|
444
|
+
static_dir=STATIC_DIR,
|
|
445
|
+
local_kg_watcher=LOCAL_KG_WATCHER,
|
|
446
|
+
))
|
|
447
|
+
api_router.include_router(create_computer_use_router(
|
|
448
|
+
model_router=router,
|
|
449
|
+
require_user=require_user,
|
|
450
|
+
tool_response=_tool_response,
|
|
451
|
+
save_to_history=save_to_history,
|
|
452
|
+
))
|
|
453
|
+
|
|
454
|
+
@api_router.post("/tools/knowledge_save")
|
|
455
|
+
async def tools_knowledge_save(req: ToolKnowledgeSaveRequest, request: Request):
|
|
456
|
+
require_user(request)
|
|
457
|
+
return _tool_response(knowledge_save, req.content, req.folder, req.title)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@api_router.post("/tools/knowledge_search")
|
|
461
|
+
async def tools_knowledge_search(req: ToolKnowledgeSearchRequest, request: Request):
|
|
462
|
+
require_user(request)
|
|
463
|
+
return _tool_response(knowledge_search, req.query, req.max_results)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@api_router.get("/tools/knowledge_tree")
|
|
467
|
+
async def tools_knowledge_tree(request: Request):
|
|
468
|
+
require_user(request)
|
|
469
|
+
return _tool_response(knowledge_tree)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@api_router.post("/tools/obsidian_save")
|
|
473
|
+
async def tools_obsidian_save(req: ToolKnowledgeSaveRequest, request: Request):
|
|
474
|
+
require_user(request)
|
|
475
|
+
return _tool_response(obsidian_save, req.content, req.folder, req.title)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@api_router.post("/tools/obsidian_search")
|
|
479
|
+
async def tools_obsidian_search(req: ToolKnowledgeSearchRequest, request: Request):
|
|
480
|
+
require_user(request)
|
|
481
|
+
return _tool_response(obsidian_search, req.query, req.max_results)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@api_router.get("/tools/obsidian_tree")
|
|
485
|
+
async def tools_obsidian_tree(request: Request):
|
|
486
|
+
require_user(request)
|
|
487
|
+
return _tool_response(obsidian_tree)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@api_router.get("/obsidian/status")
|
|
491
|
+
async def obsidian_status(request: Request):
|
|
492
|
+
require_user(request)
|
|
493
|
+
return {
|
|
494
|
+
"status": "ok",
|
|
495
|
+
"vault_root": str(BRAIN_DIR),
|
|
496
|
+
"folders": [path.name for path in BRAIN_DIR.iterdir() if path.is_dir()] if BRAIN_DIR.exists() else [],
|
|
497
|
+
"ocr_engine": shutil.which("tesseract") or None,
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@api_router.get("/tools/git_status")
|
|
502
|
+
async def tools_git_status(request: Request):
|
|
503
|
+
require_user(request)
|
|
504
|
+
return _tool_response(git_status)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@api_router.post("/tools/git_diff")
|
|
508
|
+
async def tools_git_diff(req: ToolGitDiffRequest, request: Request):
|
|
509
|
+
require_user(request)
|
|
510
|
+
return _tool_response(git_diff, req.path, req.cwd)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@api_router.post("/tools/git_log")
|
|
514
|
+
async def tools_git_log(req: ToolGitLogRequest, request: Request):
|
|
515
|
+
require_user(request)
|
|
516
|
+
return _tool_response(git_log, req.max_count, req.cwd)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@api_router.post("/tools/git_show")
|
|
520
|
+
async def tools_git_show(req: ToolGitShowRequest, request: Request):
|
|
521
|
+
require_user(request)
|
|
522
|
+
return _tool_response(git_show, req.revision, req.cwd)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@api_router.post("/tools/run_command")
|
|
526
|
+
async def tools_run_command(req: ToolRunCommandRequest, request: Request):
|
|
527
|
+
require_admin(request)
|
|
528
|
+
return _tool_response(run_command, req.command, req.cwd)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@api_router.get("/tools/network_status")
|
|
532
|
+
async def tools_network_status(request: Request):
|
|
533
|
+
require_user(request)
|
|
534
|
+
return _tool_response(network_status)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@api_router.post("/tools/build_project")
|
|
538
|
+
async def tools_build_project(req: ToolScriptRequest, request: Request):
|
|
539
|
+
require_admin(request)
|
|
540
|
+
return _tool_response(build_project, req.cwd, req.script)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@api_router.post("/tools/deploy_project")
|
|
544
|
+
async def tools_deploy_project(req: ToolScriptRequest, request: Request):
|
|
545
|
+
require_admin(request)
|
|
546
|
+
return _tool_response(deploy_project, req.cwd, req.script)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@api_router.get("/tools/permissions")
|
|
550
|
+
async def tools_permissions(request: Request):
|
|
551
|
+
"""Compact tool permission view (tool / risk / requires_approval / network).
|
|
552
|
+
|
|
553
|
+
A simpler authorization-layer summary derived from TOOL_GOVERNANCE.
|
|
554
|
+
Use /mcp/tools for the full 7-dimensional governance object.
|
|
555
|
+
"""
|
|
556
|
+
require_user(request)
|
|
557
|
+
return {"status": "ok", "permissions": list_tool_permissions()}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ── MCP / skills / plugins router (latticeai.api.mcp, v1.3.0) ────────────────
|
|
561
|
+
api_router.include_router(create_mcp_router(
|
|
562
|
+
require_user=require_user,
|
|
563
|
+
require_admin=require_admin,
|
|
564
|
+
append_audit_event=append_audit_event,
|
|
565
|
+
load_mcp_installs=load_mcp_installs,
|
|
566
|
+
recommend_mcps=recommend_mcps,
|
|
567
|
+
install_mcp=install_mcp,
|
|
568
|
+
mcp_public_item=mcp_public_item,
|
|
569
|
+
get_tool_permission=get_tool_permission,
|
|
570
|
+
tool_governance=TOOL_GOVERNANCE,
|
|
571
|
+
tool_governance_default=_TOOL_GOVERNANCE_DEFAULT,
|
|
572
|
+
check_tool_role=_check_tool_role,
|
|
573
|
+
tool_response=_tool_response,
|
|
574
|
+
require_graph=_require_graph,
|
|
575
|
+
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
576
|
+
data_dir=DATA_DIR,
|
|
577
|
+
))
|
|
578
|
+
|
|
579
|
+
return api_router
|
|
@@ -18,7 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
WORKSPACE_OS_VERSION = "1.
|
|
21
|
+
WORKSPACE_OS_VERSION = "1.4.0"
|
|
22
22
|
|
|
23
23
|
# Workspace types separate single-user Personal workspaces from shared
|
|
24
24
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|