Conversation
1. 抽取 get_local_tools_description_zh 到独立模块 tool_local_service.py,避免循环导入 2. ToolInfo 模型添加 params: List 字段 3. 移除用户不感知参数(exclude=True)的 description_zh 4. 修复 send_email_tool description 中的 typo
- Restore SSETransport and StreamableHttpTransport imports - Restore get_mcp_authorization_token_by_name_and_url import - Restore _create_mcp_transport function - Update get_tool_from_remote_mcp_server to support tenant_id and authorization_token - Update _call_mcp_tool to support authorization_token - Update _validate_mcp_tool_remote to get authorization token from database - Update get_all_mcp_tools to pass tenant_id
…description_zh - Update mock paths from tool_configuration_service to tool_local_service - Fix mock tool classes to use Field() for default values - Update test_tool_db.py mock module setup
- Restore MCP tools unique key logic (name&source&usage) in update_tool_table_from_scan_tool_list - Add user_id to MockToolInstanceClass in test - Fix mock path for get_local_tools_description_zh in test_tool_db.py
…ool_by_tool_info logic - Add ToolSourceEnum import to tool_db.py - Restore create_or_update_tool_by_tool_info to not filter by user_id - Restore proper ToolInstance creation logic
- Add tests for get_local_tools with description_zh extraction - Add tests for list_all_tools merging description_zh from SDK - Add tests for add_tool_field merging params and inputs description_zh - Add tests for get_local_tools_classes direct call - Add edge case tests for JSON decode errors and missing attributes
- Fix mock path from tool_local_service to tool_configuration_service for get_local_tools_classes - Fix mock path for get_local_tools_description_zh - Add @pytest.mark.asyncio for async list_all_tools tests - Mock importlib.import_module for get_local_tools_classes direct test
Pydantic V2 doesn't support Field(description_zh=...) directly. The actual tool classes use init_param_descriptions class attribute for parameter description_zh. Updated get_local_tools() to check init_param_descriptions as fallback when param.default.description_zh is not available.
…ls_providers # Conflicts: # sdk/nexent/core/models/openai_vlm.py
✨ Change provideRunSummary param from read-only to editable
…name ✨ Fix KnowledgeBaseSearchTool: Add index_names parameter to forward method and update tests accordingly
# Conflicts: # frontend/package.json
📝 Update docs to illustrate agent params including provideRunSummary
[Specification Details] 1. The directory is divided into charts, and each chart corresponds to one service. 2. Fixed an issue that would add an endpoint to the nexent-mcp pod after starting the containerized mcp service.
[Specification Details] 1. Remove the secrets generated in local env.
✨ Kubernetes Helm deployment directory reconstruction #2722
…matches the config
♻️ Ignore unexpected meta keys in SKILL.md
…ters in search tools - Changed default page index from 0 to 1 in DataMateClient and DataMateSearchTool for consistency. - Updated KnowledgeBaseSearchTool to accept index_names as a list instead of a comma-separated string. - Modified tests to reflect changes in parameter handling and pagination behavior.
…_connect 🐛 Bugfix Update pagination logic in DataMateClient and adjust related parame…
📝 Update readme to illustrate harness-engineering
🐛 Bugfix: exit create mode and select newly created agent after saving
# Conflicts: # frontend/hooks/agent/useSaveGuard.ts
✨ Support skill features, including creation, preview, set in agent version
|
|
||
| # Check if skill directory already exists locally | ||
| resolved = self._resolve_local_skills_dir_for_overlay() | ||
| if resolved and os.path.exists(os.path.join(resolved, skill_name)): |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
| local_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| local_path = os.path.join(local_dir, relative_path) | ||
| os.makedirs(os.path.dirname(local_path), exist_ok=True) | ||
| with open(local_path, "wb") as f: |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 8 hours ago
In general, to fix uncontrolled-path issues you must (a) strictly validate user-controlled components used in paths (e.g., skill_name), and (b) ensure that any final path derived from user input is normalized and checked to be contained within an expected root directory before use. This prevents directory traversal (..), absolute paths, or other manipulations from writing outside the intended storage area.
For this code, the best minimal fix is:
-
Introduce a helper in
backend/services/skill_service.pyto validate and normalizeskill_namewhen used as a directory name. This helper should:- Reject empty names.
- Reject names containing path separators (
/or\), drive letters,.., or starting with.or a separator. - Optionally enforce a simple regex (e.g., letters, digits,
_,-) to preserve current semantics while blocking traversal.
-
Introduce another helper to safely join any relative path under a base directory:
- Use
os.path.joinfollowed byos.path.normpath. - Compute the absolute path of both base and candidate.
- Ensure that the candidate path starts with the base path followed by a separator (or equal), otherwise raise an exception.
- Use
-
Apply these helpers within
_upload_zip_files:- Before using
skill_name, call the validation helper and use the sanitized version. - When computing
relative_pathfrom ZIP entries, useposixpathoperations to normalize them (ZIP uses/), remove any leading/, and eliminate./..segments; skip entries that resolve outside the intended tree. - Replace the plain
os.path.joincalls forlocal_dirandlocal_pathwith calls that use the secure join helper, rooting everything underself.skill_manager.local_skills_dir. This addresses both taintedskill_nameand any maliciousrelative_pathfrom the ZIP archive.
- Before using
These changes are all within backend/services/skill_service.py, in the existing helper section (near _normalize_zip_entry_path) and the _upload_zip_files method. You do not need to modify imports beyond adding posixpath and possibly re / os.path usage that is already present.
| try: | ||
| # Delete local skill files from filesystem | ||
| skill_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| if os.path.exists(skill_dir): |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 8 hours ago
To fix the problem, we need to ensure that any filesystem path derived from skill_name remains under a trusted root directory (self.skill_manager.local_skills_dir) and does not allow traversal or absolute-path tricks. The standard pattern is:
- Join the user-provided component with the base directory.
- Normalize / resolve the resulting path (e.g.,
os.path.realpathoros.path.normpath). - Verify that the normalized path is still within the base dir (e.g., using
os.path.commonpathor a prefix check after normalization). - Only then perform filesystem operations, otherwise raise an error.
In backend/services/skill_service.py, inside delete_skill, we should:
- Compute
base_dir = os.path.realpath(self.skill_manager.local_skills_dir). - Compute
candidate_dir = os.path.realpath(os.path.join(base_dir, skill_name)). - Use
os.path.commonpath([base_dir, candidate_dir]) == base_dirto verify containment. - If the containment check fails, log an error and raise
SkillException("Invalid skill name")instead of deleting anything. - Use
candidate_dir(renamed e.g. toskill_dir) for existence check and deletion.
This keeps existing functionality (deleting the corresponding local directory when given a valid skill name) while preventing path traversal or arbitrary directory deletion. No new external dependencies are required; os.path already provides what we need.
| @@ -1139,7 +1139,14 @@ | ||
| """ | ||
| try: | ||
| # Delete local skill files from filesystem | ||
| skill_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| base_dir = os.path.realpath(self.skill_manager.local_skills_dir) | ||
| candidate_dir = os.path.realpath(os.path.join(base_dir, skill_name)) | ||
| # Ensure the resolved path stays within the local_skills_dir to prevent path traversal | ||
| if os.path.commonpath([base_dir, candidate_dir]) != base_dir: | ||
| logger.error("Attempt to delete skill outside of local_skills_dir: %s", candidate_dir) | ||
| raise SkillException("Invalid skill name") | ||
|
|
||
| skill_dir = candidate_dir | ||
| if os.path.exists(skill_dir): | ||
| import shutil | ||
| shutil.rmtree(skill_dir) |
| skill_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| if os.path.exists(skill_dir): | ||
| import shutil | ||
| shutil.rmtree(skill_dir) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 8 hours ago
In general, the fix is to ensure that any user‑supplied component used in a filesystem path is validated and constrained so it cannot escape a designated root directory. For this case, we need to ensure that skill_name cannot cause skill_dir to point outside self.skill_manager.local_skills_dir and ideally restrict skill_name to a simple, safe identifier (e.g., no slashes, no .., no leading . etc.).
The least intrusive, backward‑compatible fix is to normalize the joined path and verify that the resulting real path is still inside self.skill_manager.local_skills_dir. If it is not, we should abort with a controlled exception instead of calling shutil.rmtree. Concretely, in SkillService.delete_skill in backend/services/skill_service.py, we will:
- Compute
base_dir = os.path.realpath(self.skill_manager.local_skills_dir). - Compute
skill_dir = os.path.realpath(os.path.join(base_dir, skill_name)). - Check that
os.path.commonpath([base_dir, skill_dir]) == base_dir. If not, raiseSkillException("Invalid skill name")(or similar) before any filesystem deletion. - Proceed to
shutil.rmtree(skill_dir)only if the check passes.
We already import os and SkillException at the top of skill_service.py, and shutil is imported locally in the function, so no new imports are strictly necessary beyond using existing ones. All changes are confined to the delete_skill method in backend/services/skill_service.py around lines 1140–1146.
| @@ -1139,7 +1139,11 @@ | ||
| """ | ||
| try: | ||
| # Delete local skill files from filesystem | ||
| skill_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| base_dir = os.path.realpath(self.skill_manager.local_skills_dir) | ||
| skill_dir = os.path.realpath(os.path.join(base_dir, skill_name)) | ||
| # Ensure the resolved skill_dir is within the allowed base directory | ||
| if os.path.commonpath([base_dir, skill_dir]) != base_dir: | ||
| raise SkillException(f"Invalid skill name: {skill_name}") | ||
| if os.path.exists(skill_dir): | ||
| import shutil | ||
| shutil.rmtree(skill_dir) |
| local_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| full_path = os.path.join(local_dir, file_path) | ||
|
|
||
| if not os.path.exists(full_path): |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 8 hours ago
In general, to fix uncontrolled-path issues you should: (1) define a trusted root directory, (2) construct the candidate path, (3) normalize/resolve it (e.g., with os.path.realpath or os.path.normpath), and (4) verify that the normalized path is still within the trusted root (e.g., with os.path.commonpath or a robust prefix comparison). Only then should you perform filesystem operations on it.
For this specific code, the best fix is to add a small helper inside SkillService to safely resolve a skill file path and then use it in get_skill_file_content (and, by extension, any future methods like delete/update that manipulate skill files) so that both skill_name and file_path cannot escape self.skill_manager.local_skills_dir. We will:
-
Add a private method
_resolve_skill_file_path(self, skill_name: str, file_path: str) -> strnear the other helper methods inSkillService. This method will:- Build
local_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name). - Compute
root = os.path.realpath(local_dir)andcandidate = os.path.realpath(os.path.join(local_dir, file_path)). - Use
os.path.commonpath([root, candidate]) == rootto ensurecandidateis insideroot. If not, raiseSkillException("Invalid file path"). - Return
candidate.
- Build
-
Update
get_skill_file_contentto use this method instead of directly joining the paths:- Replace:
with:
local_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) full_path = os.path.join(local_dir, file_path)
full_path = self._resolve_skill_file_path(skill_name, file_path)
- Keep the existing existence check and file reading logic unchanged.
- Replace:
No new imports are required: os and SkillException are already imported in backend/services/skill_service.py. The changes are confined to this file and specifically inside the shown snippets.
| @@ -1337,6 +1337,31 @@ | ||
| logger.error(f"Error getting skill file tree: {e}") | ||
| raise SkillException(f"Failed to get skill file tree: {str(e)}") from e | ||
|
|
||
| def _resolve_skill_file_path(self, skill_name: str, file_path: str) -> str: | ||
| """Resolve and validate a file path within a skill directory. | ||
|
|
||
| Ensures that the resolved path stays within the skill's root directory | ||
| under ``self.skill_manager.local_skills_dir`` to prevent directory traversal. | ||
| """ | ||
| # Base directory for this skill | ||
| local_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| root = os.path.realpath(local_dir) | ||
|
|
||
| # Candidate file path | ||
| candidate = os.path.realpath(os.path.join(local_dir, file_path)) | ||
|
|
||
| # Ensure the candidate path is within the skill's root directory | ||
| try: | ||
| common = os.path.commonpath([root, candidate]) | ||
| except ValueError: | ||
| # Raised if paths are on different drives (Windows) or otherwise incomparable | ||
| raise SkillException("Invalid file path") from None | ||
|
|
||
| if common != root: | ||
| raise SkillException("Invalid file path") | ||
|
|
||
| return candidate | ||
|
|
||
| def get_skill_file_content( | ||
| self, | ||
| skill_name: str, | ||
| @@ -1354,8 +1379,7 @@ | ||
| File content as string, or None if file not found | ||
| """ | ||
| try: | ||
| local_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| full_path = os.path.join(local_dir, file_path) | ||
| full_path = self._resolve_skill_file_path(skill_name, file_path) | ||
|
|
||
| if not os.path.exists(full_path): | ||
| logger.warning(f"File not found: {full_path}") |
| logger.warning(f"File not found: {full_path}") | ||
| return None | ||
|
|
||
| with open(full_path, "r", encoding="utf-8") as f: |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 8 hours ago
In general, to fix this kind of issue you must ensure that any path derived from user input is constrained to stay within a known safe root. The usual pattern is: (1) construct a candidate path under a trusted base directory, (2) normalize it with os.path.abspath/os.path.realpath or os.path.normpath, and (3) verify that the normalized path is still within the intended base directory, rejecting it otherwise. You can also separately validate the components (such as skill_name) against an allowlist (e.g. only alphanumeric, dashes, underscores) to rule out weird path characters.
For this code, the best minimal fix without changing existing functionality is:
- In
SkillService.get_skill_file_content, compute a base directory for the skill (local_dir) as before, but then:- Resolve both
local_dirand the finalfull_pathto absolute, normalized paths usingos.path.abspath. - Check that
full_pathis still insidelocal_dir. We can do this safely withos.path.commonpath([local_dir, full_path]) == local_dir. - If the check fails, log a warning and raise a
SkillException(or returnNone) indicating an invalid file path, instead of attempting to open the file.
- Resolve both
- Also ensure
file_pathis treated as a relative path (it might start with/); we can normalize and strip leading separators before joining, or simply rely on thecommonpathcheck to block absolute paths that escape the base. - Keep the rest of the behavior the same: if the file does not exist after validation, log and return
None; if it exists and is permitted, open and read it as before.
This change all happens inside backend/services/skill_service.py in the get_skill_file_content method, between the try: and the open(...) call. No changes are needed in backend/apps/skill_app.py for this specific vulnerability. We do not need extra imports because os.path and os are already imported at the top of the file.
| @@ -1354,10 +1354,22 @@ | ||
| File content as string, or None if file not found | ||
| """ | ||
| try: | ||
| # Construct base directory for this skill under the local skills root | ||
| local_dir = os.path.join(self.skill_manager.local_skills_dir, skill_name) | ||
| full_path = os.path.join(local_dir, file_path) | ||
| local_dir = os.path.abspath(local_dir) | ||
|
|
||
| if not os.path.exists(full_path): | ||
| # Build the requested file path and normalize to an absolute path | ||
| full_path = os.path.abspath(os.path.join(local_dir, file_path)) | ||
|
|
||
| # Ensure the resolved path is still within the skill's directory | ||
| if os.path.commonpath([local_dir, full_path]) != local_dir: | ||
| logger.warning( | ||
| "Attempted access to file outside skill directory: " | ||
| f"skill_name={skill_name}, file_path={file_path}, resolved={full_path}" | ||
| ) | ||
| raise SkillException("Invalid file path") | ||
|
|
||
| if not os.path.exists(full_path) or not os.path.isfile(full_path): | ||
| logger.warning(f"File not found: {full_path}") | ||
| return None | ||
|
|
No description provided.