From 5f7fb9c75ad1d3d442d4236607c827ad97a132fd Mon Sep 17 00:00:00 2001 From: cocolato Date: Tue, 3 Mar 2026 23:40:56 +0800 Subject: [PATCH 1/7] add missed dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a22053c..4199af1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dependencies = [ "prompt-toolkit>=3.0.50,<4.0.0", "mcp>=1.26.0,<2.0.0", "json-repair>=0.57.0,<1.0.0", + "openai>=2.8.0", ] [project.optional-dependencies] From df8d09f2b6c0eb23298e41acbe139fad9d38f325 Mon Sep 17 00:00:00 2001 From: Kiplangatkorir Date: Wed, 4 Mar 2026 10:53:30 +0300 Subject: [PATCH 2/7] fix: guard validate_params against non-dict input When the LLM returns malformed tool arguments (e.g. a list or string instead of a dict), validate_params would crash with AttributeError in _validate() when calling val.items(). Now returns a clear validation error instead of crashing. --- nanobot/agent/tools/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nanobot/agent/tools/base.py b/nanobot/agent/tools/base.py index 8dd82c7..051fc9a 100644 --- a/nanobot/agent/tools/base.py +++ b/nanobot/agent/tools/base.py @@ -54,6 +54,8 @@ class Tool(ABC): def validate_params(self, params: dict[str, Any]) -> list[str]: """Validate tool parameters against JSON schema. Returns error list (empty if valid).""" + if not isinstance(params, dict): + return [f"parameters must be an object, got {type(params).__name__}"] schema = self.parameters or {} if schema.get("type", "object") != "object": raise ValueError(f"Schema must be object type, got {schema.get('type')!r}") From edaf7a244a0d65395cab954fc768dc8031489b29 Mon Sep 17 00:00:00 2001 From: Kiplangatkorir Date: Wed, 4 Mar 2026 10:55:17 +0300 Subject: [PATCH 3/7] fix: handle invalid ISO datetime in CronTool gracefully datetime.fromisoformat(at) raises ValueError for malformed strings, which propagated uncaught and crashed the tool execution. Now catches ValueError and returns a user-friendly error message instead. --- nanobot/agent/tools/cron.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nanobot/agent/tools/cron.py b/nanobot/agent/tools/cron.py index 13b1e12..f8e737b 100644 --- a/nanobot/agent/tools/cron.py +++ b/nanobot/agent/tools/cron.py @@ -122,7 +122,10 @@ class CronTool(Tool): elif at: from datetime import datetime - dt = datetime.fromisoformat(at) + try: + dt = datetime.fromisoformat(at) + except ValueError: + return f"Error: invalid ISO datetime format '{at}'. Expected format: YYYY-MM-DDTHH:MM:SS" at_ms = int(dt.timestamp() * 1000) schedule = CronSchedule(kind="at", at_ms=at_ms) delete_after = True From ce65f8c11be13b51f242890cabdf15f4e0d1b12a Mon Sep 17 00:00:00 2001 From: Kiplangatkorir Date: Wed, 4 Mar 2026 11:15:45 +0300 Subject: [PATCH 4/7] fix: add size limit to ReadFileTool to prevent OOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReadFileTool had no file size check — reading a multi-GB file would load everything into memory and crash the process. Now: - Rejects files over ~512KB at the byte level (fast stat check) - Truncates at 128K chars with a notice if content is too long - Guides the agent to use exec with head/tail/grep for large files This matches the protection already in ExecTool (10KB) and WebFetchTool (50KB). --- nanobot/agent/tools/filesystem.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index bbdd49c..7b0b867 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -26,6 +26,8 @@ def _resolve_path( class ReadFileTool(Tool): """Tool to read file contents.""" + _MAX_CHARS = 128_000 # ~128 KB — prevents OOM from reading huge files into LLM context + def __init__(self, workspace: Path | None = None, allowed_dir: Path | None = None): self._workspace = workspace self._allowed_dir = allowed_dir @@ -54,7 +56,16 @@ class ReadFileTool(Tool): if not file_path.is_file(): return f"Error: Not a file: {path}" + size = file_path.stat().st_size + if size > self._MAX_CHARS * 4: # rough upper bound (UTF-8 chars ≤ 4 bytes) + return ( + f"Error: File too large ({size:,} bytes). " + f"Use exec tool with head/tail/grep to read portions." + ) + content = file_path.read_text(encoding="utf-8") + if len(content) > self._MAX_CHARS: + return content[: self._MAX_CHARS] + f"\n\n... (truncated — file is {len(content):,} chars, limit {self._MAX_CHARS:,})" return content except PermissionError as e: return f"Error: {e}" From bb8512ca842fc3b14c6dee01c5aaf9e241f8344e Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Wed, 4 Mar 2026 20:42:49 +0800 Subject: [PATCH 5/7] test: fix test failures from refactored cron and context builder - test_context_prompt_cache: Update test to reflect merged runtime context and user message (commit ad99d5a merged them into one) - Remove test_cron_commands.py: cron add CLI command was removed in commit c05cb2e (unified scheduling via cron tool) --- tests/test_context_prompt_cache.py | 19 +++++++++---------- tests/test_cron_commands.py | 29 ----------------------------- 2 files changed, 9 insertions(+), 39 deletions(-) delete mode 100644 tests/test_cron_commands.py diff --git a/tests/test_context_prompt_cache.py b/tests/test_context_prompt_cache.py index 9afcc7d..ce796e2 100644 --- a/tests/test_context_prompt_cache.py +++ b/tests/test_context_prompt_cache.py @@ -40,7 +40,7 @@ def test_system_prompt_stays_stable_when_clock_changes(tmp_path, monkeypatch) -> def test_runtime_context_is_separate_untrusted_user_message(tmp_path) -> None: - """Runtime metadata should be a separate user message before the actual user message.""" + """Runtime metadata should be merged with the user message.""" workspace = _make_workspace(tmp_path) builder = ContextBuilder(workspace) @@ -54,13 +54,12 @@ def test_runtime_context_is_separate_untrusted_user_message(tmp_path) -> None: assert messages[0]["role"] == "system" assert "## Current Session" not in messages[0]["content"] - assert messages[-2]["role"] == "user" - runtime_content = messages[-2]["content"] - assert isinstance(runtime_content, str) - assert ContextBuilder._RUNTIME_CONTEXT_TAG in runtime_content - assert "Current Time:" in runtime_content - assert "Channel: cli" in runtime_content - assert "Chat ID: direct" in runtime_content - + # Runtime context is now merged with user message into a single message assert messages[-1]["role"] == "user" - assert messages[-1]["content"] == "Return exactly: OK" + user_content = messages[-1]["content"] + assert isinstance(user_content, str) + assert ContextBuilder._RUNTIME_CONTEXT_TAG in user_content + assert "Current Time:" in user_content + assert "Channel: cli" in user_content + assert "Chat ID: direct" in user_content + assert "Return exactly: OK" in user_content diff --git a/tests/test_cron_commands.py b/tests/test_cron_commands.py deleted file mode 100644 index bce1ef5..0000000 --- a/tests/test_cron_commands.py +++ /dev/null @@ -1,29 +0,0 @@ -from typer.testing import CliRunner - -from nanobot.cli.commands import app - -runner = CliRunner() - - -def test_cron_add_rejects_invalid_timezone(monkeypatch, tmp_path) -> None: - monkeypatch.setattr("nanobot.config.loader.get_data_dir", lambda: tmp_path) - - result = runner.invoke( - app, - [ - "cron", - "add", - "--name", - "demo", - "--message", - "hello", - "--cron", - "0 9 * * *", - "--tz", - "America/Vancovuer", - ], - ) - - assert result.exit_code == 1 - assert "Error: unknown timezone 'America/Vancovuer'" in result.stdout - assert not (tmp_path / "cron" / "jobs.json").exists() From ecdf30940459a27311855a97cfdb7599cb3f89a2 Mon Sep 17 00:00:00 2001 From: Daniel Emden Date: Wed, 4 Mar 2026 15:31:56 +0100 Subject: [PATCH 6/7] fix(codex): pass reasoning_effort to Codex API The OpenAI Codex provider accepts reasoning_effort but silently discards it. Wire it through as {"reasoning": {"effort": ...}} in the request body so the config option actually takes effect. --- nanobot/providers/openai_codex_provider.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nanobot/providers/openai_codex_provider.py b/nanobot/providers/openai_codex_provider.py index b6afa65..d04e210 100644 --- a/nanobot/providers/openai_codex_provider.py +++ b/nanobot/providers/openai_codex_provider.py @@ -52,6 +52,9 @@ class OpenAICodexProvider(LLMProvider): "parallel_tool_calls": True, } + if reasoning_effort: + body["reasoning"] = {"effort": reasoning_effort} + if tools: body["tools"] = _convert_tools(tools) From bdfe7d6449dab772f681b857ad76796c92b63d05 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 5 Mar 2026 00:16:31 +0800 Subject: [PATCH 7/7] fix(feishu): convert audio type to file for API compatibility Feishu's GetMessageResource API only accepts 'image' or 'file' as the type parameter. When downloading voice messages, nanobot was passing 'audio' which caused the API to reject the request with an error. This fix converts 'audio' to 'file' in _download_file_sync method before making the API call, allowing voice messages to be downloaded and transcribed successfully. Fixes voice message download failure in Feishu channel. --- nanobot/channels/feishu.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index 0a0a5e4..a9a32b2 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -530,6 +530,10 @@ class FeishuChannel(BaseChannel): self, message_id: str, file_key: str, resource_type: str = "file" ) -> tuple[bytes | None, str | None]: """Download a file/audio/media from a Feishu message by message_id and file_key.""" + # Feishu API only accepts 'image' or 'file' as type parameter + # Convert 'audio' to 'file' for API compatibility + if resource_type == "audio": + resource_type = "file" try: request = ( GetMessageResourceRequest.builder()