Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions astrbot/core/astr_main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,13 +498,11 @@ async def _ensure_persona_and_skills(
skill_manager = SkillManager()
skills = skill_manager.list_skills(active_only=True, runtime=runtime)
skills = _filter_skills_for_current_config(skills, cfg)
workspace_skills = (
skill_manager.list_workspace_skills(
workspace_skills = []
if runtime == "local" and not event.get_group_id():

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.

security-high high

While disabling workspace-scoped skills in group sessions is an excellent security improvement to prevent privilege-boundary vulnerabilities, a similar vulnerability exists with EXTRA_PROMPT.md.

In _apply_workspace_extra_prompt (called during _decorate_llm_request), the file EXTRA_PROMPT.md is loaded from the workspace path and appended to the system prompt. Since group sessions share the same workspace directory (derived from event.unified_msg_origin), any non-admin user in the group could plant or modify EXTRA_PROMPT.md to perform a prompt injection attack against other users (including admins) in the same group.

To fully mitigate this privilege-boundary vulnerability, please consider also disabling EXTRA_PROMPT.md loading for group sessions. For example, you can update _apply_workspace_extra_prompt as follows:

def _apply_workspace_extra_prompt(
    event: AstrMessageEvent,
    req: ProviderRequest,
) -> None:
    if event.get_group_id():
        return

    extra_prompt_path = _get_workspace_path_for_umo(event.unified_msg_origin) / (
        "EXTRA_PROMPT.md"
    )
    # ... rest of the function

workspace_skills = skill_manager.list_workspace_skills(
_get_workspace_path_for_umo(event.unified_msg_origin)
)
if runtime == "local"
else []
)

if skills or workspace_skills:
if persona and persona.get("skills") is not None:
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/test_astr_main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,63 @@ async def test_ensure_skills_includes_workspace_skills(
in req.system_prompt
)

@pytest.mark.asyncio
async def test_ensure_skills_skips_workspace_skills_for_group_sessions(
self,
monkeypatch,
tmp_path,
mock_event,
mock_context,
):
module = ama
data_dir = tmp_path / "data"
global_skills_dir = tmp_path / "global_skills"
plugins_dir = tmp_path / "plugins"
workspaces_dir = tmp_path / "workspaces"
for path in (data_dir, global_skills_dir, plugins_dir):
path.mkdir(parents=True, exist_ok=True)

mock_event.get_group_id.return_value = "group123"
mock_event.message_obj.group_id = "group123"
mock_event.unified_msg_origin = "test_platform:GroupMessage:group123"
workspace_root = workspaces_dir / module.normalize_umo_for_workspace(
mock_event.unified_msg_origin
)
workspace_skill_dir = workspace_root / "skills" / "workspace-skill"
workspace_skill_dir.mkdir(parents=True)
workspace_skill_dir.joinpath("SKILL.md").write_text(
"---\ndescription: Workspace scoped skill.\n---\n",
encoding="utf-8",
)

monkeypatch.setattr(
module,
"get_astrbot_workspaces_path",
lambda: str(workspaces_dir),
)
monkeypatch.setattr(
"astrbot.core.skills.skill_manager.get_astrbot_data_path",
lambda: str(data_dir),
)
monkeypatch.setattr(
"astrbot.core.skills.skill_manager.get_astrbot_skills_path",
lambda: str(global_skills_dir),
)
monkeypatch.setattr(
"astrbot.core.skills.skill_manager.get_astrbot_plugin_path",
lambda: str(plugins_dir),
)

req = ProviderRequest()
req.conversation = MagicMock(persona_id=None)

await module._ensure_persona_and_skills(
req, {"computer_use_runtime": "local"}, mock_context, mock_event
)

assert "Workspace scoped skill." not in req.system_prompt
assert "## Skills" not in req.system_prompt

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.

medium

The current test asserts that "Workspace scoped skill." and "## Skills" are not in the system prompt when workspace skills are skipped. However, this doesn't verify that global/plugin skills are still correctly loaded and not accidentally broken or overridden in group sessions.

To make this regression test more robust, we should also set up a global skill (similar to test_ensure_skills_includes_workspace_skills) and assert that:

  1. The global skill is loaded and present in the system prompt.
  2. The workspace skill (even if it has the same name as the global skill) is not loaded and does not override the global skill.
        global_skill_dir = global_skills_dir / "workspace-skill"
        global_skill_dir.mkdir(parents=True)
        global_skill_dir.joinpath("SKILL.md").write_text(
            "---\ndescription: Global scoped skill.\n---\n",
            encoding="utf-8",
        )

        workspace_skill_dir = workspace_root / "skills" / "workspace-skill"
        workspace_skill_dir.mkdir(parents=True)
        workspace_skill_dir.joinpath("SKILL.md").write_text(
            "---\ndescription: Workspace scoped skill.\n---\n",
            encoding="utf-8",
        )

        monkeypatch.setattr(
            module,
            "get_astrbot_workspaces_path",
            lambda: str(workspaces_dir),
        )
        monkeypatch.setattr(
            "astrbot.core.skills.skill_manager.get_astrbot_data_path",
            lambda: str(data_dir),
        )
        monkeypatch.setattr(
            "astrbot.core.skills.skill_manager.get_astrbot_skills_path",
            lambda: str(global_skills_dir),
        )
        monkeypatch.setattr(
            "astrbot.core.skills.skill_manager.get_astrbot_plugin_path",
            lambda: str(plugins_dir),
        )

        req = ProviderRequest()
        req.conversation = MagicMock(persona_id=None)

        await module._ensure_persona_and_skills(
            req, {"computer_use_runtime": "local"}, mock_context, mock_event
        )

        assert "Global scoped skill." in req.system_prompt
        assert "Workspace scoped skill." not in req.system_prompt


@pytest.mark.asyncio
async def test_ensure_skills_respects_empty_persona_skills_for_workspace(
self,
Expand Down
Loading