diff --git a/backend/README.md b/backend/README.md index 937d391581..dc6122e2b0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -135,7 +135,8 @@ FastAPI application providing REST endpoints for frontend integration: | `GET /api/models` | List available LLM models | | `GET/PUT /api/mcp/config` | Manage MCP server configurations | | `POST /api/mcp/cache/reset` | Reset cached MCP tools so they reload on next use | -| `GET/PUT /api/skills` | List and manage skills | +| `GET /api/skills` | List skills visible to the caller | +| `PUT /api/skills/{skill_name}` | Enable or disable a skill (admin only) | | `POST /api/skills/install` | Install skill from `.skill` archive | | `GET /api/memory` | Retrieve memory data | | `POST /api/memory/reload` | Force memory reload | diff --git a/backend/README_zh.md b/backend/README_zh.md index 26bcb3791d..5e7d8281b1 100644 --- a/backend/README_zh.md +++ b/backend/README_zh.md @@ -121,7 +121,8 @@ FastAPI 应用程序,为前端集成提供 REST 接口: | `GET /api/models` | 列出可用的 LLM 模型 | | `GET/PUT /api/mcp/config` | 管理 MCP 服务器配置 | | `POST /api/mcp/cache/reset` | 重置缓存的 MCP 工具,使其在下次使用时重新加载 | -| `GET/PUT /api/skills` | 列出并管理技能 | +| `GET /api/skills` | 列出当前调用者可见的技能 | +| `PUT /api/skills/{skill_name}` | 启用或禁用指定技能(仅管理员) | | `POST /api/skills/install` | 从 `.skill` 归档文件安装技能 | | `GET /api/memory` | 获取记忆数据 | | `POST /api/memory/reload` | 强制重新加载记忆 | diff --git a/backend/app/gateway/routers/skills.py b/backend/app/gateway/routers/skills.py index 9f0559c18d..934a936c63 100644 --- a/backend/app/gateway/routers/skills.py +++ b/backend/app/gateway/routers/skills.py @@ -834,13 +834,14 @@ def _write_extensions_skill_state( "/skills/{skill_name}", response_model=SkillResponse, summary="Update Skill", - description="Update a skill's enabled status by modifying the extensions_config.json file.", + description=("Update a skill's enabled status (admin only). Public skills persist to the shared extensions_config.json; custom/legacy skills persist to per-user storage when available, otherwise to the shared configuration."), ) async def update_skill(skill_name: str, body: SkillUpdateRequest, request: Request, config: AppConfig = Depends(get_config)) -> SkillResponse: - # Enabling/disabling a skill writes the shared extensions_config.json and - # refreshes the system prompt for every tenant, so it is a global mutation - # (there is no per-user skill state). Guard it as admin-only like the other - # global config writes, matching the MCP router. + # Keep skill toggles admin-only, including user-scoped custom/legacy skills. + # Public state uses shared extensions_config.json; custom/legacy state uses + # per-user storage when available, falling back to shared config otherwise. + # Public toggles clear all users' caches; custom/legacy refresh only the caller, + # even in the shared-config fallback (other users' cached state may be stale). await require_admin_user(request, detail=_ADMIN_REQUIRED_DETAIL) try: skill_name = skill_name.replace("\r\n", "").replace("\n", "") @@ -889,8 +890,9 @@ async def update_skill(skill_name: str, body: SkillUpdateRequest, request: Reque # PUBLIC skill enabled state lives in the global extensions_config.json # and affects every user, so the prompt cache for ALL users must be - # invalidated. CUSTOM/LEGACY skill state is per-user so only that - # user's cache needs to be dropped. The state write and its cache + # invalidated. CUSTOM/LEGACY drops only the caller's cache, including the + # non-user-scoped fallback above, where other users' entries may stay stale. + # The state write and its cache # invalidation settle as one drained unit: a cancelled caller must # not leave the prompt cache serving the previous enablement. if skill.category == SkillCategory.PUBLIC: