[Bug] 沙盒缓存残留导致内置 star 提供的 skill(如 pdf)被误判为 sandbox preset,WebUI 无法查看
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 75/100
Research direction
Start with SkillManager.is_sandbox_only_skill() in astrbot/core/skills/skill_manager.py and trace its use from SkillsService.resolve_local_skill_dir() in astrbot/dashboard/services/skills_service.py. Reproduce the sandbox-to-local switch with a cached pdf entry, then verify that plugin-provided skills such as pdf can be opened read-only without the sandbox preset error.
Written by the indexing model from the issue text.
Description
问题描述
SkillManager.is_sandbox_only_skill()(astrbot/core/skills/skill_manager.py:683)存在两处缺陷,叠加导致内置 star skill 无法在 WebUI 查看:
- 只检查本地
skills_root与data/sandbox_skills_cache.json,从不检查 plugin-provided skills。内置 starastrbot自带的skills/pdf、documents、spreadsheets、skill-creator属于 plugin skill(source_type="plugin",只读可查看); - 读取沙盒缓存时不区分当前
computer_use_runtime。该缓存是持久文件,仅在新沙盒会话发现 skills 时更新,切回 local 后不会清理。
因此只要曾经配置过沙盒(缓存中已包含同名条目),之后即使切回 local 运行时,SkillsService.resolve_local_skill_dir()(astrbot/dashboard/services/skills_service.py:123)会先命中该检查并抛出:
PermissionError: Sandbox preset skill cannot be opened from local skill files.
前端按 source_type="plugin" 预期该 skill 可只读查看,结果收到错误。
背景:此前上传过名为 pdf 的本地 skill,与后来版本新增的内置 astrbot star 的 skills/pdf 同名。list_skills 中本地 skill 优先、内置被跳过(重名遮蔽);删除本地同名 skill 后内置 pdf 浮现,随即触发本问题。
建议修复:is_sandbox_only_skill() 开头增加
if self._get_plugin_skill_dir(name) is not None:
return False
Workaround:删除 data/sandbox_skills_cache.json(纯缓存,local 运行时无用,下次沙盒会话会重新生成)。
如何复现?
- 配置
computer_use_runtime为 sandbox 并运行,使data/sandbox_skills_cache.json生成(内含 pdf 等内置 skill 条目) - 切回 local 运行时(缓存保留)
- 确保
data/skills/pdf不存在(全新安装即满足;或曾上传同名本地 skill 后删除) - WebUI → 扩展 → 技能,点击内置
pdfskill 查看文件 - 报错:
Sandbox preset skill cannot be opened from local skill files.
AstrBot 版本
v4.27.4
操作系统
Linux
部署方式
Docker
使用的消息平台适配器
无关
错误日志
[16:04:43.151] [Core] [ERRO] [v4.27.4] [api.skills:80]: Sandbox preset skill cannot be opened from local skill files.
Traceback (most recent call last):
File "/AstrBot/main.py", line 155, in <module>
asyncio.run(main_async(args.webui_dir))
│ │ │ │ └ None
│ │ │ └ Namespace(webui_dir=None, reset_password=False)
│ │ └ <function main_async at 0x7fb60d14ab60>
│ └ <function run at 0x7fb610611a80>
└ <module 'asyncio' from '/usr/local/lib/python3.12/asyncio/__init__.py'>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
│ │ └ <coroutine object main_async at 0x7fb60355a880>
│ └ <function Runner.run at 0x7fb61044af20>
└ <asyncio.runners.Runner object at 0x7fb602bb4f50>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
│ │ │ └ <Task pending name='Task-1' coro=<main_async() running at /AstrBot/main.py:127> wait_for=<_GatheringFuture pending cb=[Task.t...
│ │ └ <function BaseEventLoop.run_until_complete at 0x7fb610448b80>
│ └ <_UnixSelectorEventLoop running=True closed=False debug=False>
└ <asyncio.runners.Runner object at 0x7fb602bb4f50>
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 678, in run_until_complete
self.run_forever()
│ └ <function BaseEventLoop.run_forever at 0x7fb610448ae0>
└ <_UnixSelectorEventLoop running=True closed=False debug=False>
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 645, in run_forever
self._run_once()
│ └ <function BaseEventLoop._run_once at 0x7fb61044a8e0>
└ <_UnixSelectorEventLoop running=True closed=False debug=False>
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 1999, in _run_once
handle._run()
│ └ <function Handle._run at 0x7fb6105b0c20>
└ <Handle Task.task_wakeup(<Future finis...7fb5d63d4230>>)>
File "/usr/local/lib/python3.12/asyncio/events.py", line 88, in _run
self._context.run(self._callback, *self._args)
│ │ │ │ │ └ <member '_args' of 'Handle' objects>
│ │ │ │ └ <Handle Task.task_wakeup(<Future finis...7fb5d63d4230>>)>
│ │ │ └ <member '_callback' of 'Handle' objects>
│ │ └ <Handle Task.task_wakeup(<Future finis...7fb5d63d4230>>)>
│ └ <member '_context' of 'Handle' objects>
└ <Handle Task.task_wakeup(<Future finis...7fb5d63d4230>>)>
File "/usr/local/lib/python3.12/site-packages/anyio/_core/_tasks.py", line 275, in _run_coro
retval = await self._coro
│ └ <member '_coro' of 'TaskHandle' objects>
└ <TaskHandle pending name='starlette.middleware.base.BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.coro' coro=<corou...
File "/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py", line 144, in coro
await self.app(scope, receive_or_disconnect, send_no_error)
│ │ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.send_no_error at 0x7fb5ba468540>
│ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ └ <starlette.middleware.exceptions.ExceptionMiddleware object at 0x7fb5d6283b60>
└ <starlette.middleware.base.BaseHTTPMiddleware object at 0x7fb5d623e750>
File "/usr/local/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 63, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
│ │ │ │ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.send_no_error at 0x7fb5ba468540>
│ │ │ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ │ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ │ │ └ <starlette.requests.Request object at 0x7fb5b9d32c30>
│ │ └ <fastapi.middleware.asyncexitstack.AsyncExitStackMiddleware object at 0x7fb5d623f200>
│ └ <starlette.middleware.exceptions.ExceptionMiddleware object at 0x7fb5d6283b60>
└ <function wrap_app_handling_exceptions at 0x7fb607ff47c0>
File "/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
│ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
└ <fastapi.middleware.asyncexitstack.AsyncExitStackMiddleware object at 0x7fb5d623f200>
File "/usr/local/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
│ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ └ <fastapi.routing.APIRouter object at 0x7fb5d63a7ad0>
└ <fastapi.middleware.asyncexitstack.AsyncExitStackMiddleware object at 0x7fb5d623f200>
File "/usr/local/lib/python3.12/site-packages/starlette/routing.py", line 670, in __call__
await self.middleware_stack(scope, receive, send)
│ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ └ <bound method APIRouter.app of <fastapi.routing.APIRouter object at 0x7fb5d63a7ad0>>
└ <fastapi.routing.APIRouter object at 0x7fb5d63a7ad0>
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 2734, in app
await route.handle(scope, receive, send)
│ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ └ <function _IncludedRouter.handle at 0x7fb6037079c0>
└ _IncludedRouter(original_router=<fastapi.routing.APIRouter object at 0x7fb5d63d6750>, include_context=_RouterIncludeContext(i...
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 1780, in handle
await self.original_router.handle(scope, receive, send)
│ │ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ │ └ <function APIRouter.handle at 0x7fb60372da80>
│ └ <fastapi.routing.APIRouter object at 0x7fb5d63d6750>
└ _IncludedRouter(original_router=<fastapi.routing.APIRouter object at 0x7fb5d63d6750>, include_context=_RouterIncludeContext(i...
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 2789, in handle
await included_router._handle_selected(scope, receive, send)
│ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ └ <function _IncludedRouter._handle_selected at 0x7fb603707a60>
└ _IncludedRouter(original_router=<fastapi.routing.APIRouter object at 0x7fb5d63d6750>, include_context=_RouterIncludeContext(i...
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 1791, in _handle_selected
await route.handle(scope, receive, send)
│ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ └ <function _IncludedRouter.handle at 0x7fb6037079c0>
└ _IncludedRouter(original_router=<fastapi.routing.APIRouter object at 0x7fb602c43d40>, include_context=_RouterIncludeContext(i...
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 1780, in handle
await self.original_router.handle(scope, receive, send)
│ │ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ │ └ <function APIRouter.handle at 0x7fb60372da80>
│ └ <fastapi.routing.APIRouter object at 0x7fb602c43d40>
└ _IncludedRouter(original_router=<fastapi.routing.APIRouter object at 0x7fb602c43d40>, include_context=_RouterIncludeContext(i...
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 2789, in handle
await included_router._handle_selected(scope, receive, send)
│ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ └ <function _IncludedRouter._handle_selected at 0x7fb603707a60>
└ _IncludedRouter(original_router=<fastapi.routing.APIRouter object at 0x7fb602c43d40>, include_context=_RouterIncludeContext(i...
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 1800, in _handle_selected
await original_route.handle(scope, receive, send)
│ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ └ <function APIRoute.handle at 0x7fb603705a80>
└ APIRoute(path='/skills/files', name='list_skill_files_by_name', methods=['GET'])
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 1279, in handle
await app(scope, receive, send)
│ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
└ <function request_response.<locals>.app at 0x7fb5b9d04040>
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 158, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
│ │ │ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d04f40>
│ │ │ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ │ │ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
│ │ └ <starlette.requests.Request object at 0x7fb5b9d31b50>
│ └ <function request_response.<locals>.app.<locals>.app at 0x7fb5b9d04400>
└ <function wrap_app_handling_exceptions at 0x7fb607ff47c0>
File "/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
│ │ │ └ <function wrap_app_handling_exceptions.<locals>.wrapped_app.<locals>.sender at 0x7fb5b9d072e0>
│ │ └ <function BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.receive_or_disconnect at 0x7fb5ba46a520>
│ └ {'type': 'http', 'http_version': '1.1', 'asgi': {'spec_version': '2.1', 'version': '3.0'}, 'method': 'GET', 'scheme': 'http',...
└ <function request_response.<locals>.app.<locals>.app at 0x7fb5b9d04400>
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 144, in app
response = await f(request)
│ └ <starlette.requests.Request object at 0x7fb5b9d31b50>
└ <function get_request_handler.<locals>.app at 0x7fb5b9d049a0>
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 706, in app
raw_response = await run_endpoint_function(
└ <function run_endpoint_function at 0x7fb603704e00>
File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 352, in run_endpoint_function
return await dependant.call(**values)
│ │ └ {'_auth': AuthContext(username='rain1', scopes=['*'], api_key_id=None, via='jwt'), 'service': <astrbot.dashboard.services.ski...
│ └ <member 'call' of 'Dependant' objects>
└ Dependant(path_params=[], query_params=[ModelField(field_info=Query(PydanticUndefined), name='skill_name', mode='validation',...
File "/AstrBot/astrbot/dashboard/api/skills.py", line 181, in list_skill_files_by_name
return await _run(
└ <function _run at 0x7fb602c4c9a0>
> File "/AstrBot/astrbot/dashboard/api/skills.py", line 73, in _run
result = await run_maybe_async(operation)
│ └ <function list_skill_files_by_name.<locals>.<lambda> at 0x7fb5b9f63ba0>
└ <function run_maybe_async at 0x7fb60330ba60>
File "/AstrBot/astrbot/dashboard/async_utils.py", line 19, in run_maybe_async
result: Any = operation() if callable(operation) else operation
│ │ └ <function list_skill_files_by_name.<locals>.<lambda> at 0x7fb5b9f63ba0>
│ └ <function list_skill_files_by_name.<locals>.<lambda> at 0x7fb5b9f63ba0>
└ <function list_skill_files_by_name.<locals>.<lambda> at 0x7fb5b9f63ba0>
File "/AstrBot/astrbot/dashboard/api/skills.py", line 182, in <lambda>
lambda: service.list_skill_files(
│ └ <function SkillsService.list_skill_files at 0x7fb6030ea200>
└ <astrbot.dashboard.services.skills_service.SkillsService object at 0x7fb5d63d4230>
File "/AstrBot/astrbot/dashboard/services/skills_service.py", line 502, in list_skill_files
skill_dir = self.resolve_local_skill_dir(skill_name)
│ │ └ 'pdf'
│ └ <function SkillsService.resolve_local_skill_dir at 0x7fb6030e9a80>
└ <astrbot.dashboard.services.skills_service.SkillsService object at 0x7fb5d63d4230>
File "/AstrBot/astrbot/dashboard/services/skills_service.py", line 132, in resolve_local_skill_dir
raise PermissionError(
PermissionError: Sandbox preset skill cannot be opened from local skill files.
辅助信息
No response
检查清单
- Dominant language
- Python
- Stars
- 40.7k
- Forks
- 2.9k
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 112
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from AstrBotDevs/AstrBot
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
AstrBotDevs/AstrBot#10033 · 3 comments ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
AstrBotDevs/AstrBot#9999 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
AstrBotDevs/AstrBot#9945 · 2 comments ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
AstrBotDevs/AstrBot#9938 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
AstrBotDevs/AstrBot#9929 · 2 comments ·
All issues in AstrBotDevs/AstrBot
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100