From 9d6ace16efa9c9fe3f8eb2d42fe6cd80ced18e2b Mon Sep 17 00:00:00 2001 From: chengyongru Date: Thu, 2 Jul 2026 11:54:36 +0800 Subject: [PATCH] fix(tools): hide exec compatibility aliases from schema --- nanobot/agent/tools/shell.py | 110 +++++++++++++++---------- tests/tools/test_exec_session_tools.py | 34 +++++++- 2 files changed, 98 insertions(+), 46 deletions(-) diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index dda5e15b6..7b5da559d 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -8,6 +8,7 @@ import re import shutil import sys from contextlib import suppress +from copy import deepcopy from dataclasses import dataclass from pathlib import Path from typing import Any @@ -15,7 +16,7 @@ from typing import Any from loguru import logger from pydantic import Field -from nanobot.agent.tools.base import Tool, ToolResult, tool_parameters +from nanobot.agent.tools.base import Schema, Tool, ToolResult, tool_parameters from nanobot.agent.tools.context import current_request_session_key from nanobot.agent.tools.exec_session import ( DEFAULT_EXEC_SESSION_MANAGER, @@ -73,51 +74,55 @@ class _PreparedCommand: login: bool -@tool_parameters( - tool_parameters_schema( - command=StringSchema("The shell command to execute"), - cmd=StringSchema("Compatibility alias for command"), - working_dir=StringSchema("Optional working directory for the command"), - workdir=StringSchema("Compatibility alias for working_dir"), - timeout=IntegerSchema( - 60, - description=( - "Timeout in seconds. Increase for long-running commands " - "like compilation or installation (default 60, max 600)." - ), - minimum=1, - maximum=600, +_EXEC_TOOL_PARAMETERS = tool_parameters_schema( + command=StringSchema("The shell command to execute"), + working_dir=StringSchema("Optional working directory for the command"), + timeout=IntegerSchema( + 60, + description=( + "Timeout in seconds. Increase for long-running commands " + "like compilation or installation (default 60, max 600)." ), - shell=StringSchema( - "Optional shell binary to launch. On Unix, supports sh, bash, or zsh.", - nullable=True, + minimum=1, + maximum=600, + ), + shell=StringSchema( + "Optional shell binary to launch. On Unix, supports sh, bash, or zsh.", + nullable=True, + ), + login=BooleanSchema( + description="Whether to run bash/zsh with login shell semantics (default false).", + default=False, + nullable=True, + ), + yield_time_ms=IntegerSchema( + description=( + "Optional milliseconds to wait before returning output. " + "When set, a still-running command returns a session_id that " + "can be polled or written to with write_stdin. Omit this field " + "to keep one-shot exec behavior." ), - login=BooleanSchema( - description="Whether to run bash/zsh with login shell semantics (default false).", - default=False, - nullable=True, + minimum=0, + maximum=MAX_YIELD_MS, + nullable=True, + ), + max_output_chars=IntegerSchema( + description=( + "Maximum output characters to return when yield_time_ms is used " + "(default 10000, max 50000)." ), - yield_time_ms=IntegerSchema( - description=( - "Optional milliseconds to wait before returning output. " - "When set, a still-running command returns a session_id that " - "can be polled or written to with write_stdin. Omit this field " - "to keep one-shot exec behavior." - ), - minimum=0, - maximum=MAX_YIELD_MS, - nullable=True, - ), - max_output_chars=IntegerSchema( - description=( - "Maximum output characters to return when yield_time_ms is used " - "(default 10000, max 50000)." - ), - minimum=1000, - maximum=MAX_OUTPUT_CHARS, - nullable=True, - ), - max_output_tokens=IntegerSchema( + minimum=1000, + maximum=MAX_OUTPUT_CHARS, + nullable=True, + ), +) + +_EXEC_TOOL_COMPAT_PARAMETERS = deepcopy(_EXEC_TOOL_PARAMETERS) +_EXEC_TOOL_COMPAT_PARAMETERS["properties"].update( + { + "cmd": StringSchema("Compatibility alias for command").to_json_schema(), + "workdir": StringSchema("Compatibility alias for working_dir").to_json_schema(), + "max_output_tokens": IntegerSchema( description=( "Compatibility alias for max_output_chars. The current runtime " "uses a character budget." @@ -125,9 +130,12 @@ class _PreparedCommand: minimum=1000, maximum=MAX_OUTPUT_CHARS, nullable=True, - ), - ) + ).to_json_schema(), + } ) + + +@tool_parameters(_EXEC_TOOL_PARAMETERS) class ExecTool(Tool): """Tool to execute shell commands.""" _scopes = {"core", "subagent"} @@ -244,6 +252,18 @@ class ExecTool(Tool): def exclusive(self) -> bool: return True + def cast_params(self, params: dict[str, Any]) -> dict[str, Any]: + return self._cast_object(params, _EXEC_TOOL_COMPAT_PARAMETERS) + + def validate_params(self, params: dict[str, Any]) -> list[str]: + if not isinstance(params, dict): + return [f"parameters must be an object, got {type(params).__name__}"] + return Schema.validate_json_schema_value( + params, + {**_EXEC_TOOL_COMPAT_PARAMETERS, "type": "object"}, + "", + ) + async def execute( self, command: str | None = None, cmd: str | None = None, working_dir: str | None = None, workdir: str | None = None, diff --git a/tests/tools/test_exec_session_tools.py b/tests/tools/test_exec_session_tools.py index e120b236c..f46df2d56 100644 --- a/tests/tools/test_exec_session_tools.py +++ b/tests/tools/test_exec_session_tools.py @@ -11,7 +11,7 @@ from nanobot.agent.tools.exec_session import ( ListExecSessionsTool, WriteStdinTool, ) -from nanobot.agent.tools.registry import is_tool_error_result +from nanobot.agent.tools.registry import ToolRegistry, is_tool_error_result from nanobot.agent.tools.shell import ExecTool @@ -53,6 +53,38 @@ def test_exec_accepts_command_aliases(tmp_path): assert "Exit code: 0" in result +def test_exec_schema_hides_compatibility_aliases(): + props = ExecTool().parameters["properties"] + + assert "command" in props + assert "working_dir" in props + assert "max_output_chars" in props + assert "cmd" not in props + assert "workdir" not in props + assert "max_output_tokens" not in props + + +def test_exec_registry_accepts_hidden_compatibility_aliases(tmp_path): + async def run() -> str: + registry = ToolRegistry() + registry.register(ExecTool(working_dir="/", timeout=5)) + command = _python_command("import os; print(os.getcwd()); print('A' * 2000)") + return await registry.execute( + "exec", + { + "cmd": command, + "workdir": str(tmp_path), + "max_output_tokens": 1000, + }, + ) + + result = asyncio.run(run()) + + assert str(tmp_path) in result + assert "chars truncated" in result + assert "Exit code: 0" in result + + def test_exec_returns_completed_session_output_when_yield_time_ms_is_used(tmp_path): async def run() -> str: manager = ExecSessionManager()