feat: Tools Atomic PR of #20243 (#20370)

* feat: Add read-only access support for Tools

- Backend: Add write_access field to ToolAccessResponse
- Backend: Update /tools/list to return tools with write_access
- Frontend: Display Read Only badge in Tools list
- Frontend: Disable inputs and save button when no write access
- Frontend: Add readOnly prop to CodeEditor component

* Update Tools.svelte

* fix: Return write_access from getToolById endpoint

fix: Return write_access from getToolById endpoint

- Use ToolAccessResponse instead of raw dict
- Remove inefficient getToolList call in edit page

* refactor: Rename write_access to disabled in ToolkitEditor

- Rename prop from write_access to disabled
- Invert logic where needed
- Update edit page to pass disabled instead of write_access

* rem

* Update +page.svelte

* fix

* Update ToolkitEditor.svelte

* Update CodeEditor.svelte

* Update ToolkitEditor.svelte
This commit is contained in:
Classic298
2026-01-06 00:00:48 +01:00
committed by GitHub
parent 5921a19519
commit c87031e9a6
3 changed files with 102 additions and 36 deletions

View File

@@ -97,6 +97,10 @@ class ToolUserResponse(ToolResponse):
model_config = ConfigDict(extra="allow")
class ToolAccessResponse(ToolUserResponse):
write_access: Optional[bool] = False
class ToolForm(BaseModel):
id: str
name: str

View File

@@ -17,6 +17,7 @@ from open_webui.models.tools import (
ToolModel,
ToolResponse,
ToolUserResponse,
ToolAccessResponse,
Tools,
)
from open_webui.utils.plugin import (
@@ -157,13 +158,24 @@ async def get_tools(request: Request, user=Depends(get_verified_user), db: Sessi
############################
@router.get("/list", response_model=list[ToolUserResponse])
@router.get("/list", response_model=list[ToolAccessResponse])
async def get_tool_list(user=Depends(get_verified_user), db: Session = Depends(get_session)):
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
tools = Tools.get_tools(db=db)
else:
tools = Tools.get_tools_by_user_id(user.id, "write", db=db)
return tools
tools = Tools.get_tools_by_user_id(user.id, "read", db=db)
return [
ToolAccessResponse(
**tool.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == tool.user_id
or has_access(user.id, "write", tool.access_control, db=db)
),
)
for tool in tools
]
############################
@@ -338,7 +350,7 @@ async def create_new_tools(
############################
@router.get("/id/{id}", response_model=Optional[ToolModel])
@router.get("/id/{id}", response_model=Optional[ToolAccessResponse])
async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
tools = Tools.get_tool_by_id(id, db=db)
@@ -348,7 +360,14 @@ async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: Session
or tools.user_id == user.id
or has_access(user.id, "read", tools.access_control, db=db)
):
return tools
return ToolAccessResponse(
**tools.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == tools.user_id
or has_access(user.id, "write", tools.access_control, db=db)
),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View File

@@ -37,6 +37,7 @@
import AddToolMenu from './Tools/AddToolMenu.svelte';
import ImportModal from '../ImportModal.svelte';
import ViewSelector from './common/ViewSelector.svelte';
import Badge from '$lib/components/common/Badge.svelte';
let shiftKey = false;
let loaded = false;
@@ -357,45 +358,86 @@
{#each filteredItems as tool}
<Tooltip content={tool?.meta?.description ?? tool?.id}>
<div
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
class=" flex space-x-4 text-left w-full px-3 py-2.5 transition rounded-2xl {tool.write_access ? 'cursor-pointer dark:hover:bg-gray-850/50 hover:bg-gray-50' : 'cursor-not-allowed opacity-60'}"
>
<a
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
>
<div class="flex items-center text-left">
<div class=" flex-1 self-center">
<Tooltip content={tool.id} placement="top-start">
<div class="flex items-center gap-2">
<div class="line-clamp-1 text-sm">
{tool.name}
</div>
{#if tool?.meta?.manifest?.version}
<div class=" text-gray-500 text-xs font-medium shrink-0">
v{tool?.meta?.manifest?.version ?? ''}
{#if tool.write_access}
<a
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
>
<div class="flex items-center text-left">
<div class=" flex-1 self-center">
<Tooltip content={tool.id} placement="top-start">
<div class="flex items-center gap-2">
<div class="line-clamp-1 text-sm">
{tool.name}
</div>
{/if}
{#if tool?.meta?.manifest?.version}
<div class=" text-gray-500 text-xs font-medium shrink-0">
v{tool?.meta?.manifest?.version ?? ''}
</div>
{/if}
</div>
</Tooltip>
<div class="px-0.5">
<div class="text-xs text-gray-500 shrink-0">
<Tooltip
content={tool?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
tool?.user?.name ?? tool?.user?.email ?? $i18n.t('Deleted User')
)
})}
</Tooltip>
</div>
</div>
</Tooltip>
<div class="px-0.5">
<div class="text-xs text-gray-500 shrink-0">
<Tooltip
content={tool?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
tool?.user?.name ?? tool?.user?.email ?? $i18n.t('Deleted User')
)
})}
</div>
</div>
</a>
{:else}
<div
class=" flex flex-1 space-x-3.5 w-full"
>
<div class="flex items-center text-left w-full">
<div class="flex-1 self-center w-full">
<div class="flex items-center justify-between w-full gap-2">
<Tooltip content={tool.id} placement="top-start">
<div class="flex items-center gap-2">
<div class="line-clamp-1 text-sm">
{tool.name}
</div>
{#if tool?.meta?.manifest?.version}
<div class=" text-gray-500 text-xs font-medium shrink-0">
v{tool?.meta?.manifest?.version ?? ''}
</div>
{/if}
</div>
</Tooltip>
<Badge type="muted" content={$i18n.t('Read Only')} />
</div>
<div class="px-0.5">
<div class="text-xs text-gray-500 shrink-0">
<Tooltip
content={tool?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
tool?.user?.name ?? tool?.user?.email ?? $i18n.t('Deleted User')
)
})}
</Tooltip>
</div>
</div>
</div>
</div>
</div>
</a>
{/if}
{#if tool.write_access}
<div class="flex flex-row gap-0.5 self-center">
{#if shiftKey}
<Tooltip content={$i18n.t('Delete')}>
@@ -484,6 +526,7 @@
</ToolMenu>
{/if}
</div>
{/if}
</div>
</Tooltip>
{/each}