Skip to content

fix: keep is_background_task on permission-guarded plugin tools - #10611

Open
niansia wants to merge 1 commit into
AstrBotDevs:masterfrom
niansia:fix/10610
Open

niansia wants to merge 1 commit into
AstrBotDevs:masterfrom
niansia:fix/10610

Conversation

@niansia

@niansia niansia commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10610.

With the default setup, plugin tools declared with is_background_task=True run in the foreground, so the reply waits until the tool is done. Since the per-tool permissions in #8693, FunctionToolManager.get_full_tool_set() wraps every registered tool in _PermissionGuardedTool, and the wrapper didn't copy is_background_task. FunctionToolExecutor.execute() reads the flag from the tool it is given, which is the wrapper, so it was always False. Builtin and MCP tools never set the flag, so this only affects plugin tools. The default tool set (a persona without its own tool list) comes from get_full_tool_set(), so there plugin background tools haven't run in the background since v4.26.0.

Modifications / 改动点

  • astrbot/core/provider/func_tool_manager.py: _PermissionGuardedTool also copies is_background_task from the wrapped tool. With this, the wrapper mirrors every FunctionTool field except handler, which it leaves None on purpose so that every call goes through its call().

  • Plugin tools that set is_background_task=True run in the background again, as they did before v4.26.0. Other tools are unchanged.

  • The permission check still runs: the background task calls the tool through the wrapper's call(), the same as a foreground call. When it is denied, the usual "Permission denied" text reaches the main agent through the background result, like any other tool error.

  • Tests in tests/unit/test_astr_agent_tool_exec.py:

    • test_permission_guarded_plugin_tool_keeps_background_flag: a plugin tool taken from FunctionToolManager.get_full_tool_set(), so it is wrapped, is submitted as a background task when is_background_task=True and its result reaches the wake-up; with False it still runs in the foreground. The permission check runs once in both cases.
    • test_permission_guarded_background_tool_still_checks_permission: when the permission check fails, the wrapped tool is not called and the wake-up gets the "Permission denied" text.
    • The existing test_background_execution_lifecycle uses a bare FunctionTool, which is why it didn't catch this.
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

Verification steps:

  1. Install a plugin that registers a FunctionTool with is_background_task=True through context.add_llm_tools() (the demo plugin's generate_report takes about 10 seconds).
  2. Have the model call it.
  3. See when the user gets the first reply.

I used a scripted OpenAI-compatible endpoint and a small OneBot v11 client in place of NapCat; the rest is a normal python main.py instance. The plugin, the scripts and the log before the fix are in the zip attached to #10610. The full log after the fix:

bgtool_live_after.log

Before, on master @ dffe922. The reply waits 10s for the tool:

15:38:25 user 10001 -> AstrBot (private): 帮我生成这周的周报
[15:38:25.749] [Core] [INFO] [runners.tool_loop_agent_runner:1241]: 使用工具:generate_report,参数:{}
[15:38:35.754] [Core] [INFO] [runners.tool_loop_agent_runner:1438]: Tool `generate_report` Result: Weekly report: 42 new users, revenue up 8%, 3 open incidents.
  15:38:35 AstrBot -> user: 周报生成好了:Weekly report: 42 new users, revenue up 8%, 3 open incidents.

After. The user gets a reply right away, and the report 10s later:

15:40:50 user 10001 -> AstrBot (private): 帮我生成这周的周报
[15:40:50.427] [Core] [INFO] [runners.tool_loop_agent_runner:1241]: 使用工具:generate_report,参数:{}
[15:40:50.429] [Core] [INFO] [runners.tool_loop_agent_runner:1438]: Tool `generate_report` Result: Background task submitted. task_id=13f72a74c0c14552892ec316b15fa6e8
  15:40:50 AstrBot -> user: 好的,周报在后台生成,好了发给你。
[15:41:00.466] [Core] [INFO] [runners.tool_loop_agent_runner:1241]: 使用工具:send_message_to_user,参数:{'messages': [{'type': 'plain', 'text': 'Weekly report: 42 new users, revenue up 8%, 3 open incidents.'}]}
  15:41:00 AstrBot -> user: Weekly report: 42 new users, revenue up 8%, 3 open incidents.

Tests (Windows 11, Python 3.13):

uv run pytest tests/unit/test_astr_agent_tool_exec.py tests/unit/test_tool_permission.py tests/unit/test_func_tool_manager.py -q
102 passed, 20 skipped

uv run pytest tests -q --deselect tests/test_fastapi_v1_dashboard.py::test_config_update_revokes_only_affected_shell_sessions
4527 passed, 70 skipped, 2 deselected

uv run ruff format --check .
517 files already formatted

uv run ruff check .
All checks passed!

With the change to func_tool_manager.py reverted, the two background tests fail: the tool runs in the foreground and returns its result directly.

The deselected test hangs on my machine on master as well, and it passes in CI.


Checklist / 检查清单

  • 😊 No new feature is added in this PR; it is a bug fix for [Bug] 插件声明为后台任务的工具(is_background_task=True)总是被同步执行 #10610.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。

  • 📚 No WebUI navigation, page structure, or terminology changed, so no update to docs/zh / docs/en is needed.
    / 我已对照变化后的 WebUI 入口、页面结构和术语,核对并在本 PR 中更新 docs/zh 和 docs/en 的相关操作说明与截图(或说明无需更新文档的原因)。入口改名、移动或合并时,已在文档和 changelog 中补充 旧入口 → 新入口 对照。

  • 🤓 No new dependencies are introduced.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txt 和 pyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Restore background execution for permission-guarded plugin tools without changing permission enforcement.

Bug Fixes:

  • Restore background execution for plugin tools wrapped by per-tool permission guards by preserving each tool’s background-task setting.
  • Ensure permission checks continue to run for guarded background tools and prevent execution when access is denied.

Tests:

  • Add coverage for guarded plugin tools executing both in the background and foreground according to their configuration.
  • Add coverage confirming denied permissions prevent tool execution and propagate the permission error through the background result.

Plugin tools reach the tool executor wrapped in _PermissionGuardedTool,
which did not copy is_background_task, so tools declared as background
tasks always ran in the foreground and the reply waited for them.

Fixes AstrBotDevs#10610

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/func_tool_manager.py" line_range="245" />
<code_context>
         # Mirror mutable state from the underlying tool
         self.active = getattr(tool, "active", True)
         self.handler_module_path = getattr(tool, "handler_module_path", None)
+        self.is_background_task = getattr(tool, "is_background_task", False)

     async def call(self, context: Any, **kwargs: Any) -> Any:
</code_context>
<issue_to_address>
**Tool output gains system prompt authority**

When a user invokes a background plugin tool whose result contains attacker-controlled text, such as content fetched from an external source, `FunctionToolExecutor` now treats guarded plugin tools with this flag as background tasks. Their returned text is serialized into `BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT` (`astrbot/core/astr_agent_tool_exec.py:625`, `astrbot/core/astr_main_agent_resources.py:121-122`), where attacker-controlled instructions from an external source can steer the autonomous agent and its available tools.

Pass the result as untrusted tool/user data rather than interpolating it into the system prompt, and instruct the agent not to follow instructions contained in tool output.
</issue_to_address>

### Comment 2
<location path="astrbot/core/provider/func_tool_manager.py" line_range="245" />
<code_context>
         # Mirror mutable state from the underlying tool
         self.active = getattr(tool, "active", True)
         self.handler_module_path = getattr(tool, "handler_module_path", None)
+        self.is_background_task = getattr(tool, "is_background_task", False)

     async def call(self, context: Any, **kwargs: Any) -> Any:
</code_context>
<issue_to_address>
**Background tools lose temporary attachments**

When a background plugin tool uses an event-tracked temporary attachment after the foreground pipeline finishes, `PipelineScheduler.execute()` calls `event.cleanup_temporary_local_files()` when the foreground pipeline ends, while `_execute_background()` continues using the same event. Cleanup deletes tracked paths, so a plugin tool that reads an attachment after that point fails.

Keep event temporary files until the event’s background tasks finish, or give those tasks their own copies of files they need.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: astrbot/core/provider/func_tool_manager.py:245, astrbot/core/provider/func_tool_manager.py:245


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/core/provider/func_tool_manager.py
Comment thread astrbot/core/provider/func_tool_manager.py

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 插件声明为后台任务的工具(is_background_task=True)总是被同步执行

1 participant