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

1from __future__ import annotations 

2 

3import logging 

4import re 

5import time 

6from pathlib import Path 

7from typing import Optional 

8 

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 

45 

46log = logging.getLogger(__name__) 

47 

48 

49router = APIRouter() 

50 

51 

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 

58 

59 

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############################ 

65 

66 

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 ) 

79 

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 ) 

103 

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 

118 

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 ) 

133 

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') 

140 

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 

145 

146 session_token = await request.app.state.oauth_client_manager.get_oauth_token( 

147 user.id, f'mcp:{server_id}' 

148 ) 

149 

150 tool_id = f'server:mcp:{info.get("id")}' 

151 server_connections[tool_id] = server 

152 

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 ) 

174 

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 ] 

186 

187 if query: 

188 q = query.casefold() 

189 tools = [tool for tool in tools if q in (tool.name or '').casefold()] 

190 

191 return tools 

192 

193 

194############################ 

195# GetToolList 

196############################ 

197 

198 

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 [] 

203 

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 ) 

214 

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 

236 

237 

238############################ 

239# LoadFunctionFromLink 

240############################ 

241 

242 

243class LoadUrlForm(BaseModel): 

244 url: HttpUrl 

245 

246 

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' 

253 

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}' 

259 

260 # No match; return as-is 

261 return url 

262 

263 

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. 

269 

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') 

273 

274 url = github_url_to_raw_url(url) 

275 url_parts = url.rstrip('/').split('/') 

276 

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 ) 

285 

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 ) 

309 

310 

311############################ 

312# ExportTools 

313############################ 

314 

315 

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 ) 

332 

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 ) 

339 

340 

341############################ 

342# CreateNewTools 

343############################ 

344 

345 

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 ) 

367 

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 ) 

373 

374 form_data.id = form_data.id.lower() 

375 

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 ) 

386 

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') 

391 

392 TOOLS = get_tools_cache(request) 

393 TOOLS[form_data.id] = tool_module 

394 

395 specs = get_tool_specs(TOOLS[form_data.id]) 

396 tools = await Tools.insert_new_tool(user.id, form_data, specs, db=db) 

397 

398 tool_cache_dir = CACHE_DIR / 'tools' / form_data.id 

399 tool_cache_dir.mkdir(parents=True, exist_ok=True) 

400 

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 ) 

428 

429 

430############################ 

431# GetToolsById 

432############################ 

433 

434 

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) 

438 

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 ) 

477 

478 

479############################ 

480# UpdateToolsById 

481############################ 

482 

483 

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 ) 

499 

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 ) 

516 

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 ) 

527 

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') 

533 

534 TOOLS = get_tools_cache(request) 

535 TOOLS[id] = tool_module 

536 

537 specs = get_tool_specs(TOOLS[id]) 

538 

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 ) 

546 

547 updated = { 

548 **form_data.model_dump(exclude={'id'}), 

549 'specs': specs, 

550 } 

551 

552 log.debug(updated) 

553 tools = await Tools.update_tool_by_id(id, updated, db=db) 

554 

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 ) 

569 

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 ) 

577 

578 

579############################ 

580# UpdateToolAccessById 

581############################ 

582 

583 

584class ToolAccessGrantsForm(BaseModel): 

585 access_grants: list[dict] 

586 

587 

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 ) 

602 

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 ) 

618 

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 ) 

626 

627 await AccessGrants.set_access_grants('tool', id, form_data.access_grants, db=db) 

628 

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 

638 

639 

640############################ 

641# DeleteToolsById 

642############################ 

643 

644 

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 ) 

658 

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 ) 

674 

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 ) 

688 

689 return result 

690 

691 

692############################ 

693# GetToolValves 

694############################ 

695 

696 

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 ) 

707 

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 ) 

723 

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 ) 

732 

733 

734############################ 

735# GetToolValvesSpec 

736############################ 

737 

738 

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 ) 

752 

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 ) 

768 

769 tools_module, _ = await get_tool_module_from_cache(request, id) 

770 

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 

778 

779 

780############################ 

781# UpdateToolValves 

782############################ 

783 

784 

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 ) 

799 

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 ) 

815 

816 tools_module, _ = await get_tool_module_from_cache(request, id) 

817 

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 

824 

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 ) 

843 

844 

845############################ 

846# ToolUserValves 

847############################ 

848 

849 

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 ) 

860 

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 ) 

876 

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 ) 

885 

886 

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 ) 

900 

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 ) 

916 

917 tools_module, _ = await get_tool_module_from_cache(request, id) 

918 

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 

926 

927 

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 ) 

942 

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 ) 

958 

959 tools_module, _ = await get_tool_module_from_cache(request, id) 

960 

961 if hasattr(tools_module, 'UserValves'): 

962 UserValves = tools_module.UserValves 

963 

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 )