Coverage for open_webui/routers/tools.py: 44%
318 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 05:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 05:07 +0000
1from __future__ import annotations
3import logging
4import re
5import time
6from pathlib import Path
7from typing import Optional
9import aiohttp
10from fastapi import APIRouter, Depends, HTTPException, Request, status
11from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, CACHE_DIR
12from open_webui.constants import ERROR_MESSAGES
13from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, ENABLE_PLUGINS
14from open_webui.events import EVENTS, publish_event
15from open_webui.internal.db import get_async_session
16from open_webui.models.access_grants import AccessGrants
17from open_webui.models.config import Config
18from open_webui.models.groups import Groups
19from open_webui.models.oauth_sessions import OAuthSessions
20from open_webui.models.tools import (
21 ToolAccessResponse,
22 ToolForm,
23 ToolModel,
24 ToolResponse,
25 Tools,
26 ToolUserResponse,
27)
28from open_webui.utils.access_control import (
29 filter_allowed_access_grants,
30 has_connection_access,
31 has_permission,
32)
33from open_webui.utils.auth import get_admin_user, get_verified_user
34from open_webui.utils.plugin import (
35 get_tool_contents_cache,
36 get_tools_cache,
37 get_tool_module_from_cache,
38 load_tool_module_by_id,
39 replace_imports,
40 resolve_valves_schema_options,
41)
42from open_webui.utils.tools import get_tool_servers, get_tool_specs
43from pydantic import BaseModel, HttpUrl
44from sqlalchemy.ext.asyncio import AsyncSession
46log = logging.getLogger(__name__)
49router = APIRouter()
52async def get_tool_module(request, tool_id, load_from_db=True):
53 """
54 Get the tool module by its ID.
55 """
56 tool_module, _ = await get_tool_module_from_cache(request, tool_id, load_from_db)
57 return tool_module
60############################
61# GetTools
62# The danger is not in having tools, but in reaching
63# for the wrong one. Let the choice here be deliberate.
64############################
67@router.get('/', response_model=list[ToolUserResponse])
68async def get_tools(
69 request: Request,
70 query: Optional[str] = None,
71 user=Depends(get_verified_user),
72 db: AsyncSession = Depends(get_async_session),
73):
74 tools = []
75 bypass_access_control = user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL
76 user_group_ids = (
77 set() if bypass_access_control else {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)}
78 )
80 # Local Tools
81 if ENABLE_PLUGINS: 81 ↛ 105line 81 didn't jump to line 105 because the condition on line 81 was always true
82 tools_cache = get_tools_cache(request)
83 for tool in await Tools.get_tools( 83 ↛ 89line 83 didn't jump to line 89 because the loop on line 83 never started
84 defer_content=True,
85 db=db,
86 user_id=None if bypass_access_control else user.id,
87 user_group_ids=user_group_ids,
88 ):
89 tool_module = tools_cache.get(tool.id)
90 has_user_valves = (
91 hasattr(tool_module, 'UserValves')
92 if tool_module
93 else (tool.meta.has_user_valves if tool.meta else False)
94 )
95 tools.append(
96 ToolUserResponse(
97 **{
98 **tool.model_dump(),
99 'has_user_valves': has_user_valves,
100 }
101 )
102 )
104 # OpenAPI Tool Servers
105 server_connections = {}
106 for server in await get_tool_servers(request): 106 ↛ 107line 106 didn't jump to line 107 because the loop on line 106 never started
107 server_idx = server.get('idx', 0)
108 connections = await Config.get('tool_server.connections', [])
109 if server_idx >= len(connections):
110 log.warning(
111 f'Tool server index {server_idx} out of range '
112 f'(have {len(connections)} connections), skipping server {server.get("id")}'
113 )
114 continue
115 connection = connections[server_idx]
116 server_id = f'server:{server.get("id")}'
117 server_connections[server_id] = connection
119 tools.append(
120 ToolUserResponse(
121 **{
122 'id': server_id,
123 'user_id': server_id,
124 'name': server.get('openapi', {}).get('info', {}).get('title', 'Tool Server'),
125 'meta': {
126 'description': server.get('openapi', {}).get('info', {}).get('description', ''),
127 },
128 'updated_at': int(time.time()),
129 'created_at': int(time.time()),
130 }
131 )
132 )
134 # MCP Tool Servers
135 for server in await Config.get('tool_server.connections', []):
136 if server.get('type', 'openapi') == 'mcp' and (server.get('config') or {}).get('enable'): 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true
137 info = server.get('info') or {}
138 server_id = info.get('id')
139 auth_type = server.get('auth_type', 'none')
141 session_token = None
142 if auth_type in ('oauth_2.1', 'oauth_2.1_static') and server_id:
143 splits = server_id.split(':')
144 server_id = splits[-1] if len(splits) > 1 else server_id
146 session_token = await request.app.state.oauth_client_manager.get_oauth_token(
147 user.id, f'mcp:{server_id}'
148 )
150 tool_id = f'server:mcp:{info.get("id")}'
151 server_connections[tool_id] = server
153 tools.append(
154 ToolUserResponse(
155 **{
156 'id': tool_id,
157 'user_id': tool_id,
158 'name': info.get('name', 'MCP Tool Server'),
159 'meta': {
160 'description': info.get('description', ''),
161 },
162 'updated_at': int(time.time()),
163 'created_at': int(time.time()),
164 **(
165 {
166 'authenticated': session_token is not None,
167 }
168 if auth_type in ('oauth_2.1', 'oauth_2.1_static')
169 else {}
170 ),
171 }
172 )
173 )
175 if not bypass_access_control: 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true
176 tools = [
177 tool
178 for tool in tools
179 if not str(tool.id).startswith('server:')
180 or await has_connection_access(
181 user,
182 server_connections[str(tool.id)],
183 user_group_ids,
184 )
185 ]
187 if query:
188 q = query.casefold()
189 tools = [tool for tool in tools if q in (tool.name or '').casefold()]
191 return tools
194############################
195# GetToolList
196############################
199@router.get('/list', response_model=list[ToolAccessResponse])
200async def get_tool_list(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)):
201 if not ENABLE_PLUGINS: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true
202 return []
204 bypass_access_control = user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL
205 user_group_ids = (
206 set() if bypass_access_control else {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)}
207 )
208 tools = await Tools.get_tools(
209 defer_content=True,
210 db=db,
211 user_id=None if bypass_access_control else user.id,
212 user_group_ids=user_group_ids,
213 )
215 result = []
216 for tool in tools: 216 ↛ 217line 216 didn't jump to line 217 because the loop on line 216 never started
217 has_write = (
218 bypass_access_control
219 or user.id == tool.user_id
220 or any(
221 g.permission == 'write'
222 and (
223 (g.principal_type == 'user' and (g.principal_id == user.id or g.principal_id == '*'))
224 or (g.principal_type == 'group' and g.principal_id in user_group_ids)
225 )
226 for g in tool.access_grants
227 )
228 )
229 result.append(
230 ToolAccessResponse(
231 **tool.model_dump(),
232 write_access=has_write,
233 )
234 )
235 return result
238############################
239# LoadFunctionFromLink
240############################
243class LoadUrlForm(BaseModel):
244 url: HttpUrl
247def github_url_to_raw_url(url: str) -> str:
248 # Handle 'tree' (folder) URLs (add main.py at the end)
249 m1 = re.match(r'https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)', url)
250 if m1: 250 ↛ 251line 250 didn't jump to line 251 because the condition on line 250 was never true
251 org, repo, branch, path = m1.groups()
252 return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip("/")}/main.py'
254 # Handle 'blob' (file) URLs
255 m2 = re.match(r'https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)', url)
256 if m2: 256 ↛ 257line 256 didn't jump to line 257 because the condition on line 256 was never true
257 org, repo, branch, path = m2.groups()
258 return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}'
260 # No match; return as-is
261 return url
264@router.post('/load/url', response_model=dict | None)
265async def load_tool_from_url(request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)):
266 # NOTE: This is NOT a SSRF vulnerability:
267 # This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use,
268 # and does NOT accept untrusted user input. Access is enforced by authentication.
270 url = str(form_data.url)
271 if not url: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 raise HTTPException(status_code=400, detail='Please enter a valid URL')
274 url = github_url_to_raw_url(url)
275 url_parts = url.rstrip('/').split('/')
277 file_name = url_parts[-1]
278 tool_name = (
279 file_name[:-3]
280 if (file_name.endswith('.py') and (not file_name.startswith(('main.py', 'index.py', '__init__.py'))))
281 else url_parts[-2]
282 if len(url_parts) > 1
283 else 'function'
284 )
286 try:
287 async with aiohttp.ClientSession(
288 trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
289 ) as session:
290 async with session.get(
291 url, headers={'Content-Type': 'application/json'}, ssl=AIOHTTP_CLIENT_SESSION_SSL
292 ) as resp:
293 if resp.status != 200: 293 ↛ 294line 293 didn't jump to line 294 because the condition on line 293 was never true
294 raise HTTPException(status_code=resp.status, detail='Failed to fetch the tool')
295 data = await resp.text()
296 if not data: 296 ↛ 298line 296 didn't jump to line 298
297 raise HTTPException(status_code=400, detail='No data received from the URL')
298 return {
299 'name': tool_name,
300 'content': data,
301 }
302 except HTTPException:
303 raise
304 except Exception as e:
305 raise HTTPException(
306 status_code=500,
307 detail=ERROR_MESSAGES.DEFAULT(e, 'Error fetching tool'),
308 )
311############################
312# ExportTools
313############################
316@router.get('/export', response_model=list[ToolModel])
317async def export_tools(
318 request: Request,
319 user=Depends(get_verified_user),
320 db: AsyncSession = Depends(get_async_session),
321):
322 if user.role != 'admin' and not await has_permission( 322 ↛ 328line 322 didn't jump to line 328 because the condition on line 322 was never true
323 user.id,
324 'workspace.tools_export',
325 await Config.get('user.permissions'),
326 db=db,
327 ):
328 raise HTTPException(
329 status_code=status.HTTP_401_UNAUTHORIZED,
330 detail=ERROR_MESSAGES.UNAUTHORIZED,
331 )
333 bypass_access_control = user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL
334 return await Tools.get_tools(
335 db=db,
336 user_id=None if bypass_access_control else user.id,
337 permission='write',
338 )
341############################
342# CreateNewTools
343############################
346@router.post('/create', response_model=ToolResponse | None)
347async def create_new_tools(
348 request: Request,
349 form_data: ToolForm,
350 user=Depends(get_verified_user),
351 db: AsyncSession = Depends(get_async_session),
352):
353 """Create a new tool from user-supplied Python source code."""
354 if user.role != 'admin' and not ( 354 ↛ 363line 354 didn't jump to line 363 because the condition on line 354 was never true
355 await has_permission(user.id, 'workspace.tools', await Config.get('user.permissions'), db=db)
356 or await has_permission(
357 user.id,
358 'workspace.tools_import',
359 await Config.get('user.permissions'),
360 db=db,
361 )
362 ):
363 raise HTTPException(
364 status_code=status.HTTP_401_UNAUTHORIZED,
365 detail=ERROR_MESSAGES.UNAUTHORIZED,
366 )
368 if not form_data.id.isidentifier():
369 raise HTTPException(
370 status_code=status.HTTP_400_BAD_REQUEST,
371 detail='Only alphanumeric characters and underscores are allowed in the id',
372 )
374 form_data.id = form_data.id.lower()
376 tools = await Tools.get_tool_by_id(form_data.id, db=db)
377 if tools is None: 377 ↛ 424line 377 didn't jump to line 424 because the condition on line 377 was always true
378 try:
379 form_data.access_grants = await filter_allowed_access_grants(
380 await Config.get('user.permissions'),
381 user.id,
382 user.role,
383 form_data.access_grants,
384 'sharing.public_tools',
385 )
387 form_data.content = replace_imports(form_data.content)
388 tool_module, frontmatter = await load_tool_module_by_id(form_data.id, content=form_data.content)
389 form_data.meta.manifest = frontmatter
390 form_data.meta.has_user_valves = hasattr(tool_module, 'UserValves')
392 TOOLS = get_tools_cache(request)
393 TOOLS[form_data.id] = tool_module
395 specs = get_tool_specs(TOOLS[form_data.id])
396 tools = await Tools.insert_new_tool(user.id, form_data, specs, db=db)
398 tool_cache_dir = CACHE_DIR / 'tools' / form_data.id
399 tool_cache_dir.mkdir(parents=True, exist_ok=True)
401 if tools:
402 await publish_event(
403 request,
404 EVENTS.TOOL_CREATED,
405 actor=user,
406 subject_id=tools.id,
407 data={'name': tools.name},
408 )
409 return tools
410 else:
411 raise HTTPException(
412 status_code=status.HTTP_400_BAD_REQUEST,
413 detail=ERROR_MESSAGES.DEFAULT('Error creating tools'),
414 )
415 except HTTPException:
416 raise
417 except Exception as e:
418 log.exception(f'Failed to load the tool by id {form_data.id}: {e}')
419 raise HTTPException(
420 status_code=status.HTTP_400_BAD_REQUEST,
421 detail=ERROR_MESSAGES.DEFAULT(e, 'Error creating tool'),
422 )
423 else:
424 raise HTTPException(
425 status_code=status.HTTP_400_BAD_REQUEST,
426 detail=ERROR_MESSAGES.ID_TAKEN,
427 )
430############################
431# GetToolsById
432############################
435@router.get('/id/{id}', response_model=ToolAccessResponse | None)
436async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)):
437 tools = await Tools.get_tool_by_id(id, db=db)
439 if tools: 439 ↛ 440line 439 didn't jump to line 440 because the condition on line 439 was never true
440 if (
441 user.role == 'admin'
442 or tools.user_id == user.id
443 or await AccessGrants.has_access(
444 user_id=user.id,
445 resource_type='tool',
446 resource_id=tools.id,
447 permission='read',
448 db=db,
449 )
450 ):
451 write_access = (
452 (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
453 or user.id == tools.user_id
454 or await AccessGrants.has_access(
455 user_id=user.id,
456 resource_type='tool',
457 resource_id=tools.id,
458 permission='write',
459 db=db,
460 )
461 )
462 data = tools.model_dump()
463 if not write_access:
464 # extra='allow' re-admits content from model_dump; source is writer-only
465 data.pop('content', None)
466 return ToolAccessResponse(**data, write_access=write_access)
467 else:
468 raise HTTPException(
469 status_code=status.HTTP_401_UNAUTHORIZED,
470 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
471 )
472 else:
473 raise HTTPException(
474 status_code=status.HTTP_404_NOT_FOUND,
475 detail=ERROR_MESSAGES.NOT_FOUND,
476 )
479############################
480# UpdateToolsById
481############################
484@router.post('/id/{id}/update', response_model=ToolModel | None)
485async def update_tools_by_id(
486 request: Request,
487 id: str,
488 form_data: ToolForm,
489 user=Depends(get_verified_user),
490 db: AsyncSession = Depends(get_async_session),
491):
492 """Update an existing tool's source code and metadata."""
493 tools = await Tools.get_tool_by_id(id, db=db)
494 if not tools: 494 ↛ 501line 494 didn't jump to line 501 because the condition on line 494 was always true
495 raise HTTPException(
496 status_code=status.HTTP_401_UNAUTHORIZED,
497 detail=ERROR_MESSAGES.NOT_FOUND,
498 )
500 # Is the user the original creator, in a group with write access, or an admin
501 if (
502 tools.user_id != user.id
503 and not await AccessGrants.has_access(
504 user_id=user.id,
505 resource_type='tool',
506 resource_id=tools.id,
507 permission='write',
508 db=db,
509 )
510 and user.role != 'admin'
511 ):
512 raise HTTPException(
513 status_code=status.HTTP_401_UNAUTHORIZED,
514 detail=ERROR_MESSAGES.UNAUTHORIZED,
515 )
517 # Content edits trigger exec on load — gate them behind workspace.tools (matches /create).
518 if form_data.content != tools.content:
519 if user.role != 'admin' and not (
520 await has_permission(user.id, 'workspace.tools', await Config.get('user.permissions'), db=db)
521 or await has_permission(user.id, 'workspace.tools_import', await Config.get('user.permissions'), db=db)
522 ):
523 raise HTTPException(
524 status_code=status.HTTP_401_UNAUTHORIZED,
525 detail=ERROR_MESSAGES.UNAUTHORIZED,
526 )
528 try:
529 form_data.content = replace_imports(form_data.content)
530 tool_module, frontmatter = await load_tool_module_by_id(id, content=form_data.content)
531 form_data.meta.manifest = frontmatter
532 form_data.meta.has_user_valves = hasattr(tool_module, 'UserValves')
534 TOOLS = get_tools_cache(request)
535 TOOLS[id] = tool_module
537 specs = get_tool_specs(TOOLS[id])
539 form_data.access_grants = await filter_allowed_access_grants(
540 await Config.get('user.permissions'),
541 user.id,
542 user.role,
543 form_data.access_grants,
544 'sharing.public_tools',
545 )
547 updated = {
548 **form_data.model_dump(exclude={'id'}),
549 'specs': specs,
550 }
552 log.debug(updated)
553 tools = await Tools.update_tool_by_id(id, updated, db=db)
555 if tools:
556 await publish_event(
557 request,
558 EVENTS.TOOL_UPDATED,
559 actor=user,
560 subject_id=tools.id,
561 data={'name': tools.name},
562 )
563 return tools
564 else:
565 raise HTTPException(
566 status_code=status.HTTP_400_BAD_REQUEST,
567 detail=ERROR_MESSAGES.DEFAULT('Error updating tools'),
568 )
570 except HTTPException:
571 raise
572 except Exception as e:
573 raise HTTPException(
574 status_code=status.HTTP_400_BAD_REQUEST,
575 detail=ERROR_MESSAGES.DEFAULT(e, 'Error updating tool'),
576 )
579############################
580# UpdateToolAccessById
581############################
584class ToolAccessGrantsForm(BaseModel):
585 access_grants: list[dict]
588@router.post('/id/{id}/access/update', response_model=ToolModel | None)
589async def update_tool_access_by_id(
590 request: Request,
591 id: str,
592 form_data: ToolAccessGrantsForm,
593 user=Depends(get_verified_user),
594 db: AsyncSession = Depends(get_async_session),
595):
596 tools = await Tools.get_tool_by_id(id, db=db)
597 if not tools: 597 ↛ 603line 597 didn't jump to line 603 because the condition on line 597 was always true
598 raise HTTPException(
599 status_code=status.HTTP_404_NOT_FOUND,
600 detail=ERROR_MESSAGES.NOT_FOUND,
601 )
603 if (
604 tools.user_id != user.id
605 and not await AccessGrants.has_access(
606 user_id=user.id,
607 resource_type='tool',
608 resource_id=tools.id,
609 permission='write',
610 db=db,
611 )
612 and user.role != 'admin'
613 ):
614 raise HTTPException(
615 status_code=status.HTTP_401_UNAUTHORIZED,
616 detail=ERROR_MESSAGES.UNAUTHORIZED,
617 )
619 form_data.access_grants = await filter_allowed_access_grants(
620 await Config.get('user.permissions'),
621 user.id,
622 user.role,
623 form_data.access_grants,
624 'sharing.public_tools',
625 )
627 await AccessGrants.set_access_grants('tool', id, form_data.access_grants, db=db)
629 tools = await Tools.get_tool_by_id(id, db=db)
630 await publish_event(
631 request,
632 EVENTS.TOOL_ACCESS_UPDATED,
633 actor=user,
634 subject_id=id,
635 data={'name': tools.name if tools else None},
636 )
637 return tools
640############################
641# DeleteToolsById
642############################
645@router.delete('/id/{id}/delete', response_model=bool)
646async def delete_tools_by_id(
647 request: Request,
648 id: str,
649 user=Depends(get_verified_user),
650 db: AsyncSession = Depends(get_async_session),
651):
652 tools = await Tools.get_tool_by_id(id, db=db)
653 if not tools: 653 ↛ 659line 653 didn't jump to line 659 because the condition on line 653 was always true
654 raise HTTPException(
655 status_code=status.HTTP_401_UNAUTHORIZED,
656 detail=ERROR_MESSAGES.NOT_FOUND,
657 )
659 if (
660 tools.user_id != user.id
661 and not await AccessGrants.has_access(
662 user_id=user.id,
663 resource_type='tool',
664 resource_id=tools.id,
665 permission='write',
666 db=db,
667 )
668 and user.role != 'admin'
669 ):
670 raise HTTPException(
671 status_code=status.HTTP_401_UNAUTHORIZED,
672 detail=ERROR_MESSAGES.UNAUTHORIZED,
673 )
675 result = await Tools.delete_tool_by_id(id, db=db)
676 if result:
677 TOOLS = get_tools_cache(request)
678 TOOLS.pop(id, None)
679 TOOL_CONTENTS = get_tool_contents_cache(request)
680 TOOL_CONTENTS.pop(id, None)
681 await publish_event(
682 request,
683 EVENTS.TOOL_DELETED,
684 actor=user,
685 subject_id=id,
686 data={'name': tools.name},
687 )
689 return result
692############################
693# GetToolValves
694############################
697@router.get('/id/{id}/valves', response_model=dict | None)
698async def get_tools_valves_by_id(
699 id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)
700):
701 tools = await Tools.get_tool_by_id(id, db=db)
702 if not tools: 702 ↛ 708line 702 didn't jump to line 708 because the condition on line 702 was always true
703 raise HTTPException(
704 status_code=status.HTTP_404_NOT_FOUND,
705 detail=ERROR_MESSAGES.NOT_FOUND,
706 )
708 if (
709 tools.user_id != user.id
710 and not await AccessGrants.has_access(
711 user_id=user.id,
712 resource_type='tool',
713 resource_id=tools.id,
714 permission='write',
715 db=db,
716 )
717 and user.role != 'admin'
718 ):
719 raise HTTPException(
720 status_code=status.HTTP_401_UNAUTHORIZED,
721 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
722 )
724 try:
725 valves = await Tools.get_tool_valves_by_id(id, db=db)
726 return valves
727 except Exception as e:
728 raise HTTPException(
729 status_code=status.HTTP_400_BAD_REQUEST,
730 detail=ERROR_MESSAGES.DEFAULT(e, 'Error getting tool valves'),
731 )
734############################
735# GetToolValvesSpec
736############################
739@router.get('/id/{id}/valves/spec', response_model=dict | None)
740async def get_tools_valves_spec_by_id(
741 request: Request,
742 id: str,
743 user=Depends(get_verified_user),
744 db: AsyncSession = Depends(get_async_session),
745):
746 tools = await Tools.get_tool_by_id(id, db=db)
747 if not tools: 747 ↛ 753line 747 didn't jump to line 753 because the condition on line 747 was always true
748 raise HTTPException(
749 status_code=status.HTTP_404_NOT_FOUND,
750 detail=ERROR_MESSAGES.NOT_FOUND,
751 )
753 if (
754 tools.user_id != user.id
755 and not await AccessGrants.has_access(
756 user_id=user.id,
757 resource_type='tool',
758 resource_id=tools.id,
759 permission='write',
760 db=db,
761 )
762 and user.role != 'admin'
763 ):
764 raise HTTPException(
765 status_code=status.HTTP_401_UNAUTHORIZED,
766 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
767 )
769 tools_module, _ = await get_tool_module_from_cache(request, id)
771 if hasattr(tools_module, 'Valves'):
772 Valves = tools_module.Valves
773 schema = Valves.schema()
774 # Resolve dynamic options for select dropdowns
775 schema = resolve_valves_schema_options(Valves, schema, user)
776 return schema
777 return None
780############################
781# UpdateToolValves
782############################
785@router.post('/id/{id}/valves/update', response_model=dict | None)
786async def update_tools_valves_by_id(
787 request: Request,
788 id: str,
789 form_data: dict,
790 user=Depends(get_verified_user),
791 db: AsyncSession = Depends(get_async_session),
792):
793 tools = await Tools.get_tool_by_id(id, db=db)
794 if not tools: 794 ↛ 800line 794 didn't jump to line 800 because the condition on line 794 was always true
795 raise HTTPException(
796 status_code=status.HTTP_401_UNAUTHORIZED,
797 detail=ERROR_MESSAGES.NOT_FOUND,
798 )
800 if (
801 tools.user_id != user.id
802 and not await AccessGrants.has_access(
803 user_id=user.id,
804 resource_type='tool',
805 resource_id=tools.id,
806 permission='write',
807 db=db,
808 )
809 and user.role != 'admin'
810 ):
811 raise HTTPException(
812 status_code=status.HTTP_400_BAD_REQUEST,
813 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
814 )
816 tools_module, _ = await get_tool_module_from_cache(request, id)
818 if not hasattr(tools_module, 'Valves'):
819 raise HTTPException(
820 status_code=status.HTTP_401_UNAUTHORIZED,
821 detail=ERROR_MESSAGES.NOT_FOUND,
822 )
823 Valves = tools_module.Valves
825 try:
826 form_data = {k: v for k, v in form_data.items() if v is not None}
827 valves = Valves(**form_data)
828 valves_dict = valves.model_dump(exclude_unset=True)
829 await Tools.update_tool_valves_by_id(id, valves_dict, db=db)
830 await publish_event(
831 request,
832 EVENTS.TOOL_VALVES_UPDATED,
833 actor=user,
834 subject_id=id,
835 )
836 return valves_dict
837 except Exception as e:
838 log.exception(f'Failed to update tool valves by id {id}: {e}')
839 raise HTTPException(
840 status_code=status.HTTP_400_BAD_REQUEST,
841 detail=ERROR_MESSAGES.DEFAULT(e, 'Error updating tool valves'),
842 )
845############################
846# ToolUserValves
847############################
850@router.get('/id/{id}/valves/user', response_model=dict | None)
851async def get_tools_user_valves_by_id(
852 id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)
853):
854 tools = await Tools.get_tool_by_id(id, db=db)
855 if not tools: 855 ↛ 861line 855 didn't jump to line 861 because the condition on line 855 was always true
856 raise HTTPException(
857 status_code=status.HTTP_404_NOT_FOUND,
858 detail=ERROR_MESSAGES.NOT_FOUND,
859 )
861 if (
862 tools.user_id != user.id
863 and not await AccessGrants.has_access(
864 user_id=user.id,
865 resource_type='tool',
866 resource_id=tools.id,
867 permission='read',
868 db=db,
869 )
870 and user.role != 'admin'
871 ):
872 raise HTTPException(
873 status_code=status.HTTP_401_UNAUTHORIZED,
874 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
875 )
877 try:
878 user_valves = await Tools.get_user_valves_by_id_and_user_id(id, user.id, db=db)
879 return user_valves
880 except Exception as e:
881 raise HTTPException(
882 status_code=status.HTTP_400_BAD_REQUEST,
883 detail=ERROR_MESSAGES.DEFAULT(e, 'Error getting tool user valves'),
884 )
887@router.get('/id/{id}/valves/user/spec', response_model=dict | None)
888async def get_tools_user_valves_spec_by_id(
889 request: Request,
890 id: str,
891 user=Depends(get_verified_user),
892 db: AsyncSession = Depends(get_async_session),
893):
894 tools = await Tools.get_tool_by_id(id, db=db)
895 if not tools: 895 ↛ 901line 895 didn't jump to line 901 because the condition on line 895 was always true
896 raise HTTPException(
897 status_code=status.HTTP_404_NOT_FOUND,
898 detail=ERROR_MESSAGES.NOT_FOUND,
899 )
901 if (
902 tools.user_id != user.id
903 and not await AccessGrants.has_access(
904 user_id=user.id,
905 resource_type='tool',
906 resource_id=tools.id,
907 permission='read',
908 db=db,
909 )
910 and user.role != 'admin'
911 ):
912 raise HTTPException(
913 status_code=status.HTTP_401_UNAUTHORIZED,
914 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
915 )
917 tools_module, _ = await get_tool_module_from_cache(request, id)
919 if hasattr(tools_module, 'UserValves'):
920 UserValves = tools_module.UserValves
921 schema = UserValves.schema()
922 # Resolve dynamic options for select dropdowns
923 schema = resolve_valves_schema_options(UserValves, schema, user)
924 return schema
925 return None
928@router.post('/id/{id}/valves/user/update', response_model=dict | None)
929async def update_tools_user_valves_by_id(
930 request: Request,
931 id: str,
932 form_data: dict,
933 user=Depends(get_verified_user),
934 db: AsyncSession = Depends(get_async_session),
935):
936 tools = await Tools.get_tool_by_id(id, db=db)
937 if not tools: 937 ↛ 943line 937 didn't jump to line 943 because the condition on line 937 was always true
938 raise HTTPException(
939 status_code=status.HTTP_404_NOT_FOUND,
940 detail=ERROR_MESSAGES.NOT_FOUND,
941 )
943 if (
944 tools.user_id != user.id
945 and not await AccessGrants.has_access(
946 user_id=user.id,
947 resource_type='tool',
948 resource_id=tools.id,
949 permission='read',
950 db=db,
951 )
952 and user.role != 'admin'
953 ):
954 raise HTTPException(
955 status_code=status.HTTP_401_UNAUTHORIZED,
956 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
957 )
959 tools_module, _ = await get_tool_module_from_cache(request, id)
961 if hasattr(tools_module, 'UserValves'):
962 UserValves = tools_module.UserValves
964 try:
965 form_data = {k: v for k, v in form_data.items() if v is not None}
966 user_valves = UserValves(**form_data)
967 user_valves_dict = user_valves.model_dump(exclude_unset=True)
968 await Tools.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db)
969 await publish_event(
970 request,
971 EVENTS.TOOL_VALVES_UPDATED,
972 actor=user,
973 subject_id=id,
974 data={'scope': 'user'},
975 )
976 return user_valves_dict
977 except Exception as e:
978 log.exception(f'Failed to update user valves by id {id}: {e}')
979 raise HTTPException(
980 status_code=status.HTTP_400_BAD_REQUEST,
981 detail=ERROR_MESSAGES.DEFAULT(e, 'Error updating tool user valves'),
982 )
983 else:
984 raise HTTPException(
985 status_code=status.HTTP_401_UNAUTHORIZED,
986 detail=ERROR_MESSAGES.NOT_FOUND,
987 )