From 5c9cb3a208a0a9b3e543c510753269073579adaa Mon Sep 17 00:00:00 2001 From: andienguyen-ecoligo Date: Sat, 21 Feb 2026 12:34:14 -0500 Subject: [PATCH 1/6] fix(security): prevent path traversal bypass via startswith check `startswith` string comparison allows bypassing directory restrictions. For example, `/home/user/workspace_evil` passes the check against `/home/user/workspace` because the string starts with the allowed path. Replace with `Path.relative_to()` which correctly validates that the resolved path is actually inside the allowed directory tree. Fixes #888 --- nanobot/agent/tools/filesystem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index 9c169e4..b87da11 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -13,8 +13,11 @@ def _resolve_path(path: str, workspace: Path | None = None, allowed_dir: Path | if not p.is_absolute() and workspace: p = workspace / p resolved = p.resolve() - if allowed_dir and not str(resolved).startswith(str(allowed_dir.resolve())): - raise PermissionError(f"Path {path} is outside allowed directory {allowed_dir}") + if allowed_dir: + try: + resolved.relative_to(allowed_dir.resolve()) + except ValueError: + raise PermissionError(f"Path {path} is outside allowed directory {allowed_dir}") return resolved From ef96619039c98ccb45c61481cf1b5203aa5864ac Mon Sep 17 00:00:00 2001 From: andienguyen-ecoligo Date: Sat, 21 Feb 2026 12:34:50 -0500 Subject: [PATCH 2/6] fix(slack): add exception handling to socket listener _handle_message() in _on_socket_request() had no try/except. If it throws (bus full, permission error, etc.), the exception propagates up and crashes the Socket Mode event loop, causing missed messages. Other channels like Telegram already have explicit error handlers. Fixes #895 --- nanobot/channels/slack.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/nanobot/channels/slack.py b/nanobot/channels/slack.py index 4fc1f41..d1c5895 100644 --- a/nanobot/channels/slack.py +++ b/nanobot/channels/slack.py @@ -179,18 +179,21 @@ class SlackChannel(BaseChannel): except Exception as e: logger.debug("Slack reactions_add failed: {}", e) - await self._handle_message( - sender_id=sender_id, - chat_id=chat_id, - content=text, - metadata={ - "slack": { - "event": event, - "thread_ts": thread_ts, - "channel_type": channel_type, - } - }, - ) + try: + await self._handle_message( + sender_id=sender_id, + chat_id=chat_id, + content=text, + metadata={ + "slack": { + "event": event, + "thread_ts": thread_ts, + "channel_type": channel_type, + } + }, + ) + except Exception as e: + logger.error("Error handling Slack message from {}: {}", sender_id, e) def _is_allowed(self, sender_id: str, chat_id: str, channel_type: str) -> bool: if channel_type == "im": From 54a0f3d038e2a7f18315e1768a351010401cf6c2 Mon Sep 17 00:00:00 2001 From: andienguyen-ecoligo Date: Sat, 21 Feb 2026 12:35:21 -0500 Subject: [PATCH 3/6] fix(session): handle errors in legacy session migration shutil.move() in _load() can fail due to permissions, disk full, or concurrent access. Without error handling, the exception propagates up and prevents the session from loading entirely. Wrap in try/except so migration failures are logged as warnings and the session falls back to loading from the legacy path on next attempt. Fixes #863 --- nanobot/session/manager.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index 18e23b2..19d4439 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -108,9 +108,12 @@ class SessionManager: if not path.exists(): legacy_path = self._get_legacy_session_path(key) if legacy_path.exists(): - import shutil - shutil.move(str(legacy_path), str(path)) - logger.info("Migrated session {} from legacy path", key) + try: + import shutil + shutil.move(str(legacy_path), str(path)) + logger.info("Migrated session {} from legacy path", key) + except Exception as e: + logger.warning("Failed to migrate session {}: {}", key, e) if not path.exists(): return None From b323087631561d4312affd17f57aa0643210a730 Mon Sep 17 00:00:00 2001 From: Yingwen Luo-LUOYW Date: Sun, 22 Feb 2026 12:42:33 +0800 Subject: [PATCH 4/6] feat(cli): add DingTalk, QQ, and Email to channels status output --- nanobot/cli/commands.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 6155463..f1f9b30 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -668,6 +668,33 @@ def channels_status(): slack_config ) + # DingTalk + dt = config.channels.dingtalk + dt_config = f"client_id: {dt.client_id[:10]}..." if dt.client_id else "[dim]not configured[/dim]" + table.add_row( + "DingTalk", + "✓" if dt.enabled else "✗", + dt_config + ) + + # QQ + qq = config.channels.qq + qq_config = f"app_id: {qq.app_id[:10]}..." if qq.app_id else "[dim]not configured[/dim]" + table.add_row( + "QQ", + "✓" if qq.enabled else "✗", + qq_config + ) + + # Email + em = config.channels.email + em_config = em.imap_host if em.imap_host else "[dim]not configured[/dim]" + table.add_row( + "Email", + "✓" if em.enabled else "✗", + em_config + ) + console.print(table) From b93b77a485d3a0c1632a5c3d590bc0b2a37c43bd Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sun, 22 Feb 2026 15:38:19 +0000 Subject: [PATCH 5/6] fix(slack): use logger.exception to capture full traceback --- nanobot/channels/slack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nanobot/channels/slack.py b/nanobot/channels/slack.py index d1c5895..b0f9bbb 100644 --- a/nanobot/channels/slack.py +++ b/nanobot/channels/slack.py @@ -192,8 +192,8 @@ class SlackChannel(BaseChannel): } }, ) - except Exception as e: - logger.error("Error handling Slack message from {}: {}", sender_id, e) + except Exception: + logger.exception("Error handling Slack message from {}", sender_id) def _is_allowed(self, sender_id: str, chat_id: str, channel_type: str) -> bool: if channel_type == "im": From 71de1899e6dcf9e5d01396b6a90e6a63486bc6de Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sun, 22 Feb 2026 15:40:17 +0000 Subject: [PATCH 6/6] fix(session): use logger.exception and move import to top --- nanobot/session/manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index 19d4439..5f23dc2 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -1,6 +1,7 @@ """Session management for conversation history.""" import json +import shutil from pathlib import Path from dataclasses import dataclass, field from datetime import datetime @@ -109,11 +110,10 @@ class SessionManager: legacy_path = self._get_legacy_session_path(key) if legacy_path.exists(): try: - import shutil shutil.move(str(legacy_path), str(path)) logger.info("Migrated session {} from legacy path", key) - except Exception as e: - logger.warning("Failed to migrate session {}: {}", key, e) + except Exception: + logger.exception("Failed to migrate session {}", key) if not path.exists(): return None