Save inbound email attachments to the media directory with configurable
MIME type filtering (glob patterns like "image/*"), per-attachment size
limits, and max attachment count. Filenames are sanitized to prevent
path traversal. Controlled by allowed_attachment_types — empty (default)
means disabled, non-empty enables extraction for matching types.
When the primary transcription provider fails (bad key, API error, etc.),
automatically try the other provider if its API key is available.
Made-with: Cursor
- Add missing return after failed password login to prevent starting
sync loop with no credentials
- Replace f-strings in logger calls with loguru {} placeholders
- Fix stdlib import order (asyncio before json)
Made-with: Cursor
- Use import importlib.util (not bare importlib) for find_spec
- Warn and fall back to standard openai instead of crashing with
ImportError when LANGFUSE_SECRET_KEY is set but langfuse is missing
Made-with: Cursor
If _fetch_bot_open_id returns None the exact-match path would silently
disable all @mention detection. Restore the old heuristic as a fallback.
Add 6 unit tests for _is_bot_mentioned covering both paths.
Made-with: Cursor
Previously, _is_bot_mentioned used a heuristic (no user_id + open_id
prefix "ou_") which caused other bots in the same group to falsely
think they were mentioned. Now fetches the bot's own open_id via
GET /open-apis/bot/v3/info at startup and does an exact match.
When an agent task is cancelled (e.g. via /stop), the ExecTool was only
handling TimeoutError but not CancelledError. This left the child process
running as an orphan. Now CancelledError also triggers process.kill() and
waitpid cleanup before re-raising.
Allow config.json to reference environment variables via ${VAR_NAME}
syntax. Variables are resolved at runtime by resolve_config_env_vars(),
keeping the raw templates in the Pydantic model so save_config()
preserves them. This lets secrets live in a separate env file
(e.g. loaded by systemd EnvironmentFile=) instead of plain text
in config.json.
Seeding PATH in the env before bash -l caused /etc/profile
to skip its default PATH setup, breaking standard commands.
Move path_append to an inline export so the login shell
establishes a proper base PATH first.
Add regression test: ls still works when path_append is set.
Made-with: Cursor
The exec tool previously passed the full parent process environment to
child processes, which meant LLM-generated commands could access secrets
stored in env vars (e.g. API keys from EnvironmentFile=).
Switch from subprocess_shell with inherited env to bash login shell
with a minimal environment (HOME, LANG, TERM only). The login shell
sources the user's profile for PATH setup, making the pathAppend
config option a fallback rather than the primary PATH mechanism.
- Introduced a helper method `_for_each_hook_safe` to reduce code duplication in hook method implementations.
- Updated error logging to include the method name for better traceability.
- Improved the `SkillsLoader` class by adding a new method `_skill_entries_from_dir` to simplify skill listing logic.
- Enhanced skill loading and filtering logic, ensuring workspace skills take precedence over built-in ones.
- Added comprehensive tests for `SkillsLoader` to validate functionality and edge cases.
DDGS's internal `timeout=10` relies on `requests` read-timeout semantics,
which only measure the gap between bytes — not total wall-clock time.
When the underlying HTTP connection enters CLOSE-WAIT or the server
dribbles data slowly, this timeout never fires, causing `ddgs.text` to
hang indefinitely via `asyncio.to_thread`.
Since `asyncio.to_thread` cannot cancel the underlying OS thread, the
agent's session lock is never released, blocking all subsequent messages
on the same session (observed: 8+ hours of unresponsiveness).
Fix:
- Add `timeout` field to `WebSearchConfig` (default: 30s, configurable
via config.json or NANOBOT_TOOLS__WEB__SEARCH__TIMEOUT env var)
- Wrap `asyncio.to_thread` with `asyncio.wait_for` to enforce a hard
wall-clock deadline
Closes#2804
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The same test function name appeared twice; Python silently shadows the
first definition so it never ran. Keep the version that also asserts
the request URL contains "s.jina.ai".
Made-with: Cursor
Dream Phase 2 uses fail_on_tool_error=True, which terminates the entire
run on the first tool error (e.g. old_text not found in edit_file).
Normal agent runs default to False so the LLM can self-correct and retry.
Dream should behave the same way.