ltcai 8.7.0 → 8.9.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.
Files changed (103) hide show
  1. package/README.md +38 -26
  2. package/auto_setup.py +73 -8
  3. package/docs/CHANGELOG.md +63 -0
  4. package/docs/CODE_REVIEW_2026-07-06.md +764 -0
  5. package/docs/COMMUNITY_AND_PLUGINS.md +4 -3
  6. package/docs/DEVELOPMENT.md +10 -10
  7. package/docs/LEGACY_COMPATIBILITY.md +33 -14
  8. package/docs/ONBOARDING.md +2 -2
  9. package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
  10. package/docs/TRUST_MODEL.md +5 -1
  11. package/docs/WHY_LATTICE.md +4 -3
  12. package/docs/architecture.md +4 -0
  13. package/docs/kg-schema.md +1 -1
  14. package/kg_schema.py +1 -1
  15. package/knowledge_graph.py +2 -2
  16. package/lattice_brain/__init__.py +1 -1
  17. package/lattice_brain/conversations.py +156 -21
  18. package/lattice_brain/core.py +1 -1
  19. package/lattice_brain/graph/_kg_common.py +12 -34
  20. package/lattice_brain/graph/json_utils.py +25 -0
  21. package/lattice_brain/graph/retrieval.py +66 -27
  22. package/lattice_brain/graph/runtime.py +16 -0
  23. package/lattice_brain/ingestion.py +35 -20
  24. package/lattice_brain/runtime/agent_runtime.py +37 -3
  25. package/lattice_brain/runtime/multi_agent.py +1 -1
  26. package/latticeai/__init__.py +1 -1
  27. package/latticeai/api/chat.py +31 -14
  28. package/latticeai/api/mcp.py +3 -2
  29. package/latticeai/api/models.py +4 -1
  30. package/latticeai/api/permissions.py +69 -30
  31. package/latticeai/api/setup.py +17 -2
  32. package/latticeai/api/tools.py +104 -62
  33. package/latticeai/app_factory.py +93 -10
  34. package/latticeai/core/agent.py +25 -7
  35. package/latticeai/core/legacy_compatibility.py +36 -25
  36. package/latticeai/core/marketplace.py +1 -1
  37. package/latticeai/core/sessions.py +11 -3
  38. package/latticeai/core/tool_registry.py +15 -4
  39. package/latticeai/core/workspace_os.py +1 -1
  40. package/latticeai/runtime/bootstrap.py +1 -1
  41. package/latticeai/runtime/persistence_runtime.py +1 -1
  42. package/latticeai/runtime/platform_services_runtime.py +1 -1
  43. package/latticeai/services/app_context.py +1 -0
  44. package/latticeai/services/architecture_readiness.py +2 -2
  45. package/latticeai/services/memory_service.py +38 -7
  46. package/latticeai/services/model_engines.py +79 -12
  47. package/latticeai/services/model_runtime.py +24 -4
  48. package/latticeai/services/process_audit.py +208 -0
  49. package/latticeai/services/product_readiness.py +11 -11
  50. package/latticeai/services/search_service.py +106 -30
  51. package/latticeai/services/tool_dispatch.py +66 -0
  52. package/latticeai/services/workspace_service.py +15 -0
  53. package/package.json +1 -1
  54. package/scripts/check_i18n_literals.mjs +20 -8
  55. package/scripts/i18n_literal_allowlist.json +34 -0
  56. package/scripts/lint_frontend.mjs +6 -2
  57. package/setup_wizard.py +185 -19
  58. package/src-tauri/Cargo.lock +1 -1
  59. package/src-tauri/Cargo.toml +1 -1
  60. package/src-tauri/tauri.conf.json +1 -1
  61. package/static/app/asset-manifest.json +11 -11
  62. package/static/app/assets/{Act-_U7mhXir.js → Act-fZokUnC0.js} +1 -1
  63. package/static/app/assets/{Brain-BxyTHZ21.js → Brain-DtyuWubr.js} +2 -2
  64. package/static/app/assets/{Capture-DyDKWNh9.js → Capture-D5KV3Cu7.js} +1 -1
  65. package/static/app/assets/{Library-DJ8KioFM.js → Library-C9kyFkSt.js} +1 -1
  66. package/static/app/assets/{System-C0FIb3OO.js → System-VbChmX7r.js} +1 -1
  67. package/static/app/assets/index-DCh5AoXt.css +2 -0
  68. package/static/app/assets/index-DPdcPoF0.js +17 -0
  69. package/static/app/assets/{primitives-BywkNS3f.js → primitives-DFeanEV6.js} +1 -1
  70. package/static/app/assets/{textarea-CQ61Rycp.js → textarea-CD8UNKIy.js} +1 -1
  71. package/static/app/index.html +2 -2
  72. package/static/sw.js +1 -1
  73. package/lattice_brain/_kg_common.py +0 -11
  74. package/lattice_brain/discovery.py +0 -11
  75. package/lattice_brain/documents.py +0 -11
  76. package/lattice_brain/identity.py +0 -11
  77. package/lattice_brain/ingest.py +0 -11
  78. package/lattice_brain/network.py +0 -11
  79. package/lattice_brain/projection.py +0 -11
  80. package/lattice_brain/provenance.py +0 -11
  81. package/lattice_brain/retrieval.py +0 -11
  82. package/lattice_brain/schema.py +0 -11
  83. package/lattice_brain/store.py +0 -11
  84. package/lattice_brain/write_master.py +0 -11
  85. package/latticeai/brain/__init__.py +0 -36
  86. package/latticeai/brain/_kg_common.py +0 -17
  87. package/latticeai/brain/context.py +0 -17
  88. package/latticeai/brain/conversations.py +0 -17
  89. package/latticeai/brain/discovery.py +0 -17
  90. package/latticeai/brain/documents.py +0 -17
  91. package/latticeai/brain/identity.py +0 -17
  92. package/latticeai/brain/ingest.py +0 -17
  93. package/latticeai/brain/memory.py +0 -17
  94. package/latticeai/brain/network.py +0 -17
  95. package/latticeai/brain/projection.py +0 -17
  96. package/latticeai/brain/provenance.py +0 -17
  97. package/latticeai/brain/retrieval.py +0 -17
  98. package/latticeai/brain/schema.py +0 -17
  99. package/latticeai/brain/store.py +0 -17
  100. package/latticeai/brain/write_master.py +0 -17
  101. package/latticeai/services/agent_runtime.py +0 -11
  102. package/static/app/assets/index-Bh7IIlyY.js +0 -16
  103. package/static/app/assets/index-_M5aCv21.css +0 -2
@@ -30,6 +30,7 @@ class PermissionGateway:
30
30
  self.get_current_user = get_current_user
31
31
  self.local_approval_ttl_seconds = 5 * 60
32
32
  self.local_approval_lock = threading.Lock()
33
+ self.perm_queue_lock = threading.Lock()
33
34
  self.local_approvals: Dict[str, Dict[str, object]] = {}
34
35
  self.discord_permission_webhook_url = config.discord_permission_webhook
35
36
  self.discord_bot_token = config.discord_bot_token
@@ -37,26 +38,46 @@ class PermissionGateway:
37
38
  self.permission_monitor_secret = config.permission_monitor_secret
38
39
  self.perm_queue_file = data_dir / "permission_queue.json"
39
40
 
41
+ @staticmethod
42
+ def token_hash(token: str) -> str:
43
+ return hashlib.sha256(str(token or "").encode("utf-8")).hexdigest()
44
+
45
+ @staticmethod
46
+ def token_hint(token: str) -> str:
47
+ value = str(token or "")
48
+ return value[:8] if value else ""
49
+
50
+ def _read_queue(self) -> Dict:
51
+ if not self.perm_queue_file.exists():
52
+ return {}
53
+ try:
54
+ return json.loads(self.perm_queue_file.read_text(encoding="utf-8"))
55
+ except Exception:
56
+ return {}
57
+
58
+ def _write_queue(self, queue: Dict) -> None:
59
+ self.perm_queue_file.parent.mkdir(parents=True, exist_ok=True)
60
+ tmp = self.perm_queue_file.with_suffix(self.perm_queue_file.suffix + ".tmp")
61
+ tmp.write_text(json.dumps(queue, ensure_ascii=False, indent=2), encoding="utf-8")
62
+ tmp.replace(self.perm_queue_file)
63
+
40
64
  def _perm_queue_write(self, token: str, record: Dict[str, object]) -> None:
41
65
  try:
42
- queue: Dict = {}
43
- if self.perm_queue_file.exists():
44
- try:
45
- queue = json.loads(self.perm_queue_file.read_text(encoding="utf-8"))
46
- except Exception:
47
- queue = {}
48
- queue[token] = {**record, "notified": False}
49
- self.perm_queue_file.write_text(json.dumps(queue, ensure_ascii=False, indent=2), encoding="utf-8")
66
+ key = self.token_hash(token)
67
+ with self.perm_queue_lock:
68
+ queue = self._read_queue()
69
+ queue[key] = {**record, "token_hint": self.token_hint(token), "notified": False}
70
+ self._write_queue(queue)
50
71
  except Exception as exc:
51
72
  logging.warning("perm_queue_write failed: %s", exc)
52
73
 
53
74
  def _perm_queue_remove(self, token: str) -> None:
54
75
  try:
55
- if not self.perm_queue_file.exists():
56
- return
57
- queue: Dict = json.loads(self.perm_queue_file.read_text(encoding="utf-8"))
58
- queue.pop(token, None)
59
- self.perm_queue_file.write_text(json.dumps(queue, ensure_ascii=False, indent=2), encoding="utf-8")
76
+ key = self.token_hash(token)
77
+ with self.perm_queue_lock:
78
+ queue = self._read_queue()
79
+ queue.pop(key, None)
80
+ self._write_queue(queue)
60
81
  except Exception as exc:
61
82
  logging.warning("perm_queue_remove failed: %s", exc)
62
83
 
@@ -156,9 +177,10 @@ class PermissionGateway:
156
177
  "approved": False,
157
178
  }
158
179
  if action == "write":
180
+ self.ensure_path_allowed(path, action="write")
159
181
  record["content_hash"] = self.content_fingerprint(content)
160
182
  with self.local_approval_lock:
161
- self.local_approvals[token] = record
183
+ self.local_approvals[self.token_hash(token)] = {**record, "token_hint": self.token_hint(token)}
162
184
  self._perm_queue_write(token, record)
163
185
  action_label = _PERMISSION_ACTION_LABELS.get(action, action)
164
186
  return {
@@ -190,12 +212,14 @@ class PermissionGateway:
190
212
  if not token:
191
213
  raise HTTPException(status_code=403, detail="파일 접근 승인 토큰이 필요합니다.")
192
214
  normalized = self.normalize_local_path_for_approval(path)
215
+ self.ensure_path_allowed(normalized, action=action)
193
216
  now = time.time()
217
+ key = self.token_hash(token)
194
218
  with self.local_approval_lock:
195
219
  expired = [key for key, value in self.local_approvals.items() if float(value.get("expires_at", 0)) < now]
196
220
  for key in expired:
197
221
  self.local_approvals.pop(key, None)
198
- record = self.local_approvals.get(token)
222
+ record = self.local_approvals.get(key)
199
223
  if not record:
200
224
  raise HTTPException(status_code=403, detail="파일 접근 승인이 만료되었거나 유효하지 않습니다.")
201
225
  if not record.get("approved"):
@@ -213,13 +237,25 @@ class PermissionGateway:
213
237
  if auth_header == f"Bearer {self.permission_monitor_secret}":
214
238
  return
215
239
  if token:
240
+ key = self.token_hash(token)
216
241
  current_user = self.get_current_user(request)
217
242
  with self.local_approval_lock:
218
- record = self.local_approvals.get(token)
243
+ record = self.local_approvals.get(key)
219
244
  if current_user and record and record.get("user_email") == current_user:
220
245
  return
221
246
  self.require_admin(request)
222
247
 
248
+ def ensure_path_allowed(self, path: str, *, action: str) -> None:
249
+ if action != "write":
250
+ return
251
+ normalized = self.normalize_local_path_for_approval(path)
252
+ from latticeai.services.tool_dispatch import LOCAL_WRITE_BLOCKED_PREFIXES
253
+
254
+ for prefix in LOCAL_WRITE_BLOCKED_PREFIXES:
255
+ normalized_prefix = str(Path(prefix).expanduser()).rstrip("/")
256
+ if normalized == normalized_prefix or normalized.startswith(normalized_prefix + "/"):
257
+ raise HTTPException(status_code=403, detail=f"쓰기 금지 경로입니다: {prefix}")
258
+
223
259
 
224
260
  def create_permissions_router(
225
261
  *,
@@ -243,11 +279,11 @@ def create_permissions_router(
243
279
  now = time.time()
244
280
  with gateway.local_approval_lock:
245
281
  result = {}
246
- for tok, rec in list(gateway.local_approvals.items()):
282
+ for token_hash, rec in list(gateway.local_approvals.items()):
247
283
  expires_at = float(rec.get("expires_at", 0))
248
284
  if expires_at < now:
249
285
  continue
250
- result[tok] = {
286
+ result[str(rec.get("token_hint") or token_hash[:8])] = {
251
287
  "path": rec.get("path"),
252
288
  "action": rec.get("action"),
253
289
  "action_label": _PERMISSION_ACTION_LABELS.get(str(rec.get("action", "")), str(rec.get("action", ""))),
@@ -260,25 +296,26 @@ def create_permissions_router(
260
296
  @router.post("/permissions/approve/{token}")
261
297
  async def permissions_approve(token: str, request: Request):
262
298
  gateway.check_permission_auth(request, token)
299
+ key = gateway.token_hash(token)
263
300
  with gateway.local_approval_lock:
264
- record = gateway.local_approvals.get(token)
301
+ record = gateway.local_approvals.get(key)
265
302
  if not record:
266
303
  raise HTTPException(status_code=404, detail="토큰이 없거나 만료되었습니다.")
267
304
  if float(record.get("expires_at", 0)) < time.time():
268
- gateway.local_approvals.pop(token, None)
305
+ gateway.local_approvals.pop(key, None)
269
306
  raise HTTPException(status_code=410, detail="토큰이 만료되었습니다.")
270
307
  record["approved"] = True
271
308
  gateway._perm_queue_remove(token)
272
309
  logging.info(
273
310
  "Permission approved: token=%s path=%s action=%s user=%s",
274
- token,
311
+ gateway.token_hint(token),
275
312
  record.get("path"),
276
313
  record.get("action"),
277
314
  record.get("user_email"),
278
315
  )
279
316
  return {
280
317
  "ok": True,
281
- "token": token,
318
+ "token_hint": gateway.token_hint(token),
282
319
  "path": record.get("path"),
283
320
  "action": record.get("action"),
284
321
  "user_email": record.get("user_email"),
@@ -287,14 +324,15 @@ def create_permissions_router(
287
324
  @router.post("/permissions/deny/{token}")
288
325
  async def permissions_deny(token: str, request: Request):
289
326
  gateway.check_permission_auth(request, token)
327
+ key = gateway.token_hash(token)
290
328
  with gateway.local_approval_lock:
291
- record = gateway.local_approvals.pop(token, None)
329
+ record = gateway.local_approvals.pop(key, None)
292
330
  gateway._perm_queue_remove(token)
293
331
  if not record:
294
332
  raise HTTPException(status_code=404, detail="토큰이 없거나 이미 처리되었습니다.")
295
333
  logging.info(
296
334
  "Permission denied: token=%s path=%s action=%s user=%s",
297
- token,
335
+ gateway.token_hint(token),
298
336
  record.get("path"),
299
337
  record.get("action"),
300
338
  record.get("user_email"),
@@ -302,7 +340,7 @@ def create_permissions_router(
302
340
  return {
303
341
  "ok": True,
304
342
  "denied": True,
305
- "token": token,
343
+ "token_hint": gateway.token_hint(token),
306
344
  "path": record.get("path"),
307
345
  "action": record.get("action"),
308
346
  }
@@ -311,17 +349,18 @@ def create_permissions_router(
311
349
  async def permissions_status(token: str, request: Request):
312
350
  require_user(request)
313
351
  now = time.time()
352
+ key = gateway.token_hash(token)
314
353
  with gateway.local_approval_lock:
315
- record = gateway.local_approvals.get(token)
354
+ record = gateway.local_approvals.get(key)
316
355
  if not record:
317
- return {"status": "denied_or_expired", "token": token}
356
+ return {"status": "denied_or_expired", "token_hint": gateway.token_hint(token)}
318
357
  if float(record.get("expires_at", 0)) < now:
319
- return {"status": "expired", "token": token}
358
+ return {"status": "expired", "token_hint": gateway.token_hint(token)}
320
359
  if record.get("approved"):
321
- return {"status": "approved", "token": token}
360
+ return {"status": "approved", "token_hint": gateway.token_hint(token)}
322
361
  return {
323
362
  "status": "pending",
324
- "token": token,
363
+ "token_hint": gateway.token_hint(token),
325
364
  "expires_in": round(float(record.get("expires_at", 0)) - now),
326
365
  }
327
366
 
@@ -8,6 +8,7 @@ from fastapi import APIRouter, HTTPException, Request
8
8
  from fastapi.responses import StreamingResponse
9
9
  from pydantic import BaseModel
10
10
 
11
+ from latticeai.services.process_audit import command_plan
11
12
  from auto_setup import (
12
13
  plan as auto_setup_plan,
13
14
  preset as auto_setup_preset,
@@ -21,6 +22,7 @@ from setup_wizard import get_recommendations, install_stream, open_url, scan_env
21
22
 
22
23
  class SetupInstallRequest(BaseModel):
23
24
  items: List[Dict]
25
+ confirmation_token: Optional[str] = None
24
26
 
25
27
 
26
28
  def create_setup_router(*, model_router, require_user) -> APIRouter:
@@ -83,11 +85,19 @@ def create_setup_router(*, model_router, require_user) -> APIRouter:
83
85
  command = ["lattice-ai", "models", "load", str(model_id)]
84
86
  else:
85
87
  command = ["huggingface-cli", "download", str(model_id), "--quiet"]
88
+ step_plan = command_plan(
89
+ command,
90
+ name=f"weights:{model_id}",
91
+ purpose="auto_setup_install",
92
+ metadata={"model_id": str(model_id)},
93
+ )
86
94
  zero_config["plan"]["steps"] = [{
87
95
  "name": f"weights:{model_id}",
88
96
  "why": "추론에 사용할 모델 가중치",
89
97
  "command": command,
90
98
  "requires_admin": False,
99
+ "command_plan": step_plan,
100
+ "confirmation_token": step_plan["confirmation_token"],
91
101
  }]
92
102
  if isinstance(zero_config.get("preset"), dict):
93
103
  zero_config["preset"].setdefault("model", {})["id"] = model_id
@@ -107,9 +117,14 @@ def create_setup_router(*, model_router, require_user) -> APIRouter:
107
117
  @api_router.post("/setup/install")
108
118
  async def setup_install(req: SetupInstallRequest, request: Request):
109
119
  """선택된 항목을 순서대로 설치 · 로드하는 SSE 스트림."""
110
- require_user(request)
120
+ user_email = require_user(request)
111
121
  async def _gen():
112
- async for chunk in install_stream(req.items, router):
122
+ async for chunk in install_stream(
123
+ req.items,
124
+ router,
125
+ confirmation_token=req.confirmation_token,
126
+ user_email=user_email,
127
+ ):
113
128
  yield chunk
114
129
  return StreamingResponse(_gen(), media_type="text/event-stream",
115
130
  headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
@@ -3,11 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import base64
6
+ import inspect
6
7
  import io
7
8
  import logging
8
9
  import shutil
9
10
  from pathlib import Path
10
- from typing import Dict, List, Optional
11
+ from typing import Any, Dict, List, Optional
11
12
 
12
13
  from fastapi import APIRouter, File, HTTPException, Request, UploadFile
13
14
  from fastapi.responses import FileResponse
@@ -24,6 +25,7 @@ from latticeai.services.tool_dispatch import (
24
25
  TOOL_GOVERNANCE_DEFAULT as _TOOL_GOVERNANCE_DEFAULT,
25
26
  check_tool_role as _check_tool_role,
26
27
  get_tool_permission,
28
+ enforce_tool_policy,
27
29
  list_tool_permissions,
28
30
  tool_registry_diagnostics,
29
31
  tool_registry_manifest,
@@ -253,7 +255,21 @@ def create_tools_router(
253
255
 
254
256
  # ── Direct Tool API ───────────────────────────────────────────────────────────
255
257
 
256
- def _tool_response(fn, *args, **kwargs):
258
+ def _policy_args(fn, *args, **kwargs) -> Dict:
259
+ try:
260
+ bound = inspect.signature(fn).bind_partial(*args, **kwargs)
261
+ return dict(bound.arguments)
262
+ except Exception:
263
+ return dict(kwargs)
264
+
265
+ def _tool_response(
266
+ fn,
267
+ *args,
268
+ current_user: Optional[str] = None,
269
+ source: str = "http",
270
+ trusted_admin: bool = False,
271
+ **kwargs,
272
+ ):
257
273
  # Shared tool lifecycle (same path as the agent + workflow tool calls):
258
274
  # pre_tool (may block) → execute → post_tool. Keyword args are forwarded
259
275
  # to the tool and surfaced in the hook payload so read_file / edit_file /
@@ -261,7 +277,16 @@ def create_tools_router(
261
277
  # tool instead of bypassing it.
262
278
  tool_name = getattr(fn, "__name__", "tool")
263
279
  try:
264
- result = dispatch_tool(HOOKS, tool_name, dict(kwargs), lambda: fn(*args, **kwargs), source="http")
280
+ policy_args = _policy_args(fn, *args, **kwargs)
281
+ if current_user is not None:
282
+ enforce_tool_policy(
283
+ tool_name,
284
+ policy_args,
285
+ current_user=current_user,
286
+ source=source,
287
+ trusted_admin=trusted_admin,
288
+ )
289
+ result = dispatch_tool(HOOKS, tool_name, dict(kwargs), lambda: fn(*args, **kwargs), source=source)
265
290
  except PermissionError as exc:
266
291
  raise HTTPException(status_code=403, detail=str(exc))
267
292
  except ToolError as exc:
@@ -277,47 +302,58 @@ def create_tools_router(
277
302
  raise HTTPException(status_code=403, detail=str(exc))
278
303
  except ToolError as exc:
279
304
  raise HTTPException(status_code=400, detail=str(exc))
305
+
306
+ def _history_scope(user_email: str) -> Dict[str, Any]:
307
+ require_auth = bool(getattr(CONFIG, "require_auth", False))
308
+ scope: Dict[str, Any] = {
309
+ "user_email": user_email if require_auth else None,
310
+ "allowed_workspaces": None,
311
+ "include_legacy_global": not require_auth,
312
+ }
313
+ if require_auth and user_email and allowed_workspaces_for is not None:
314
+ scope["allowed_workspaces"] = allowed_workspaces_for(user_email)
315
+ return scope
280
316
 
281
317
 
282
318
  @api_router.post("/tools/list_dir")
283
319
  async def tools_list_dir(req: ToolPathRequest, request: Request):
284
- require_user(request)
285
- return _tool_response(list_dir, req.path)
320
+ current_user = require_user(request)
321
+ return _tool_response(list_dir, req.path, current_user=current_user)
286
322
 
287
323
 
288
324
  @api_router.post("/tools/workspace_tree")
289
325
  async def tools_workspace_tree(req: ToolWorkspaceTreeRequest, request: Request):
290
- require_user(request)
291
- return _tool_response(workspace_tree, req.path, req.max_depth)
326
+ current_user = require_user(request)
327
+ return _tool_response(workspace_tree, req.path, req.max_depth, current_user=current_user)
292
328
 
293
329
 
294
330
  @api_router.post("/tools/read_file")
295
331
  async def tools_read_file(req: ToolReadFileRequest, request: Request):
296
- require_user(request)
297
- return _tool_response(read_file, req.path, offset=req.offset, limit=req.limit, line_numbers=req.line_numbers)
332
+ current_user = require_user(request)
333
+ return _tool_response(read_file, req.path, offset=req.offset, limit=req.limit, line_numbers=req.line_numbers, current_user=current_user)
298
334
 
299
335
 
300
336
  @api_router.post("/tools/write_file")
301
337
  async def tools_write_file(req: ToolWriteFileRequest, request: Request):
302
- require_user(request)
303
- return _tool_response(write_file, req.path, req.content)
338
+ current_user = require_user(request)
339
+ return _tool_response(write_file, req.path, req.content, current_user=current_user)
304
340
 
305
341
 
306
342
  @api_router.post("/tools/edit_file")
307
343
  async def tools_edit_file(req: ToolEditFileRequest, request: Request):
308
- require_user(request)
309
- return _tool_response(edit_file, req.path, req.old_string, req.new_string, replace_all=req.replace_all)
344
+ current_user = require_user(request)
345
+ return _tool_response(edit_file, req.path, req.old_string, req.new_string, replace_all=req.replace_all, current_user=current_user)
310
346
 
311
347
 
312
348
  @api_router.post("/tools/search_files")
313
349
  async def tools_search_files(req: ToolSearchFilesRequest, request: Request):
314
- require_user(request)
315
- return _tool_response(search_files, req.query, req.path, req.max_results)
350
+ current_user = require_user(request)
351
+ return _tool_response(search_files, req.query, req.path, req.max_results, current_user=current_user)
316
352
 
317
353
 
318
354
  @api_router.post("/tools/grep")
319
355
  async def tools_grep(req: ToolGrepRequest, request: Request):
320
- require_user(request)
356
+ current_user = require_user(request)
321
357
  return _tool_response(
322
358
  grep,
323
359
  req.pattern,
@@ -326,25 +362,31 @@ def create_tools_router(
326
362
  max_results=req.max_results,
327
363
  case_insensitive=req.case_insensitive,
328
364
  context_lines=req.context_lines,
365
+ current_user=current_user,
329
366
  )
330
367
 
331
368
 
332
369
  @api_router.post("/tools/todo_read")
333
370
  async def tools_todo_read(request: Request):
334
- require_user(request)
335
- return _tool_response(todo_read)
371
+ current_user = require_user(request)
372
+ return _tool_response(todo_read, current_user=current_user)
336
373
 
337
374
 
338
375
  @api_router.post("/tools/todo_write")
339
376
  async def tools_todo_write(req: ToolTodoWriteRequest, request: Request):
340
- require_user(request)
341
- return _tool_response(todo_write, req.todos)
377
+ current_user = require_user(request)
378
+ return _tool_response(todo_write, req.todos, current_user=current_user)
342
379
 
343
380
 
344
381
  @api_router.post("/tools/clear_history")
345
382
  async def tools_clear_history(req: ToolClearHistoryRequest, request: Request):
346
383
  current_user = require_user(request)
347
- result = _dispatch("clear_history", {"keep_last": req.keep_last}, lambda: clear_history(req.keep_last))
384
+ scope = _history_scope(current_user)
385
+ result = _dispatch(
386
+ "clear_history",
387
+ {"keep_last": req.keep_last, **scope},
388
+ lambda: clear_history(req.keep_last, **scope),
389
+ )
348
390
  append_audit_event(
349
391
  "history_delete",
350
392
  user_email=current_user,
@@ -358,38 +400,38 @@ def create_tools_router(
358
400
 
359
401
  @api_router.post("/tools/inspect_html")
360
402
  async def tools_inspect_html(req: ToolPathRequest, request: Request):
361
- require_user(request)
362
- return _tool_response(inspect_html, req.path)
403
+ current_user = require_user(request)
404
+ return _tool_response(inspect_html, req.path, current_user=current_user)
363
405
 
364
406
 
365
407
  @api_router.post("/tools/preview_url")
366
408
  async def tools_preview_url(req: ToolPathRequest, request: Request):
367
- require_user(request)
368
- return _tool_response(preview_url, req.path)
409
+ current_user = require_user(request)
410
+ return _tool_response(preview_url, req.path, current_user=current_user)
369
411
 
370
412
 
371
413
  @api_router.post("/tools/create_docx")
372
414
  async def tools_create_docx(req: ToolDocxRequest, request: Request):
373
- require_user(request)
374
- return _tool_response(create_docx, req.title, req.body, req.filename)
415
+ current_user = require_user(request)
416
+ return _tool_response(create_docx, req.title, req.body, req.filename, current_user=current_user)
375
417
 
376
418
 
377
419
  @api_router.post("/tools/create_xlsx")
378
420
  async def tools_create_xlsx(req: ToolXlsxRequest, request: Request):
379
- require_user(request)
380
- return _tool_response(create_xlsx, req.rows, req.filename, req.sheet_name)
421
+ current_user = require_user(request)
422
+ return _tool_response(create_xlsx, req.rows, req.filename, req.sheet_name, current_user=current_user)
381
423
 
382
424
 
383
425
  @api_router.post("/tools/create_pptx")
384
426
  async def tools_create_pptx(req: ToolPptxRequest, request: Request):
385
- require_user(request)
386
- return _tool_response(create_pptx, req.title, req.slides, req.filename)
427
+ current_user = require_user(request)
428
+ return _tool_response(create_pptx, req.title, req.slides, req.filename, current_user=current_user)
387
429
 
388
430
 
389
431
  @api_router.post("/tools/create_pdf")
390
432
  async def tools_create_pdf(req: ToolPdfRequest, request: Request):
391
- require_user(request)
392
- return _tool_response(create_pdf, req.title, req.body, req.filename)
433
+ current_user = require_user(request)
434
+ return _tool_response(create_pdf, req.title, req.body, req.filename, current_user=current_user)
393
435
 
394
436
 
395
437
  @api_router.post("/tools/read_document")
@@ -402,7 +444,7 @@ def create_tools_router(
402
444
  action="read",
403
445
  user_email=current_user,
404
446
  )
405
- return _tool_response(read_document, req.path)
447
+ return _tool_response(read_document, req.path, current_user=current_user)
406
448
 
407
449
 
408
450
  @api_router.get("/tools/pdf_pages")
@@ -503,38 +545,38 @@ def create_tools_router(
503
545
 
504
546
  @api_router.post("/tools/knowledge_save")
505
547
  async def tools_knowledge_save(req: ToolKnowledgeSaveRequest, request: Request):
506
- require_user(request)
507
- return _tool_response(knowledge_save, req.content, req.folder, req.title)
548
+ current_user = require_user(request)
549
+ return _tool_response(knowledge_save, req.content, req.folder, req.title, current_user=current_user)
508
550
 
509
551
 
510
552
  @api_router.post("/tools/knowledge_search")
511
553
  async def tools_knowledge_search(req: ToolKnowledgeSearchRequest, request: Request):
512
- require_user(request)
513
- return _tool_response(knowledge_search, req.query, req.max_results)
554
+ current_user = require_user(request)
555
+ return _tool_response(knowledge_search, req.query, req.max_results, current_user=current_user)
514
556
 
515
557
 
516
558
  @api_router.get("/tools/knowledge_tree")
517
559
  async def tools_knowledge_tree(request: Request):
518
- require_user(request)
519
- return _tool_response(knowledge_tree)
560
+ current_user = require_user(request)
561
+ return _tool_response(knowledge_tree, current_user=current_user)
520
562
 
521
563
 
522
564
  @api_router.post("/tools/obsidian_save")
523
565
  async def tools_obsidian_save(req: ToolKnowledgeSaveRequest, request: Request):
524
- require_user(request)
525
- return _tool_response(obsidian_save, req.content, req.folder, req.title)
566
+ current_user = require_user(request)
567
+ return _tool_response(obsidian_save, req.content, req.folder, req.title, current_user=current_user)
526
568
 
527
569
 
528
570
  @api_router.post("/tools/obsidian_search")
529
571
  async def tools_obsidian_search(req: ToolKnowledgeSearchRequest, request: Request):
530
- require_user(request)
531
- return _tool_response(obsidian_search, req.query, req.max_results)
572
+ current_user = require_user(request)
573
+ return _tool_response(obsidian_search, req.query, req.max_results, current_user=current_user)
532
574
 
533
575
 
534
576
  @api_router.get("/tools/obsidian_tree")
535
577
  async def tools_obsidian_tree(request: Request):
536
- require_user(request)
537
- return _tool_response(obsidian_tree)
578
+ current_user = require_user(request)
579
+ return _tool_response(obsidian_tree, current_user=current_user)
538
580
 
539
581
 
540
582
  @api_router.get("/obsidian/status")
@@ -550,50 +592,50 @@ def create_tools_router(
550
592
 
551
593
  @api_router.get("/tools/git_status")
552
594
  async def tools_git_status(request: Request):
553
- require_user(request)
554
- return _tool_response(git_status)
595
+ current_user = require_user(request)
596
+ return _tool_response(git_status, current_user=current_user)
555
597
 
556
598
 
557
599
  @api_router.post("/tools/git_diff")
558
600
  async def tools_git_diff(req: ToolGitDiffRequest, request: Request):
559
- require_user(request)
560
- return _tool_response(git_diff, req.path, req.cwd)
601
+ current_user = require_user(request)
602
+ return _tool_response(git_diff, req.path, req.cwd, current_user=current_user)
561
603
 
562
604
 
563
605
  @api_router.post("/tools/git_log")
564
606
  async def tools_git_log(req: ToolGitLogRequest, request: Request):
565
- require_user(request)
566
- return _tool_response(git_log, req.max_count, req.cwd)
607
+ current_user = require_user(request)
608
+ return _tool_response(git_log, req.max_count, req.cwd, current_user=current_user)
567
609
 
568
610
 
569
611
  @api_router.post("/tools/git_show")
570
612
  async def tools_git_show(req: ToolGitShowRequest, request: Request):
571
- require_user(request)
572
- return _tool_response(git_show, req.revision, req.cwd)
613
+ current_user = require_user(request)
614
+ return _tool_response(git_show, req.revision, req.cwd, current_user=current_user)
573
615
 
574
616
 
575
617
  @api_router.post("/tools/run_command")
576
618
  async def tools_run_command(req: ToolRunCommandRequest, request: Request):
577
- require_admin(request)
578
- return _tool_response(run_command, req.command, req.cwd)
619
+ current_user, _users = require_admin(request)
620
+ return _tool_response(run_command, req.command, req.cwd, current_user=current_user, trusted_admin=True)
579
621
 
580
622
 
581
623
  @api_router.get("/tools/network_status")
582
624
  async def tools_network_status(request: Request):
583
- require_user(request)
584
- return _tool_response(network_status)
625
+ current_user = require_user(request)
626
+ return _tool_response(network_status, current_user=current_user)
585
627
 
586
628
 
587
629
  @api_router.post("/tools/build_project")
588
630
  async def tools_build_project(req: ToolScriptRequest, request: Request):
589
- require_admin(request)
590
- return _tool_response(build_project, req.cwd, req.script)
631
+ current_user, _users = require_admin(request)
632
+ return _tool_response(build_project, req.cwd, req.script, current_user=current_user, trusted_admin=True)
591
633
 
592
634
 
593
635
  @api_router.post("/tools/deploy_project")
594
636
  async def tools_deploy_project(req: ToolScriptRequest, request: Request):
595
- require_admin(request)
596
- return _tool_response(deploy_project, req.cwd, req.script)
637
+ current_user, _users = require_admin(request)
638
+ return _tool_response(deploy_project, req.cwd, req.script, current_user=current_user, trusted_admin=True)
597
639
 
598
640
 
599
641
  @api_router.get("/tools/permissions")