diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/agent.py index 3a8cd8c96..129952dae 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/agent.py @@ -1,29 +1,22 @@ -"""`airtable` route: ``SubAgent`` spec for deepagents.""" +"""``airtable`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools come exclusively from MCP. The connector's own approval ruleset is +declared in :data:`tools.index.RULESET`; the orchestrator layers it into +a per-subagent :class:`PermissionMiddleware`. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "airtable" +from .tools.index import NAME, RULESET def build_subagent( @@ -31,26 +24,20 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles airtable tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + description = ( + read_md_file(__package__, "description").strip() + or "Handles airtable tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, - tools=tools, - interrupt_on=interrupt_on, + tools=list(mcp_tools or []), + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/tools/index.py index 37efbecf1..9eebd2395 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/tools/index.py @@ -1,27 +1,21 @@ +"""``airtable`` permission ruleset (rules over MCP tool names).""" + from __future__ import annotations -from typing import Any +from app.agents.new_chat.permissions import Rule, Ruleset -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, +NAME = "airtable" + +RULESET = Ruleset( + origin=NAME, + rules=[ + Rule(permission="list_bases", pattern="*", action="allow"), + Rule(permission="search_bases", pattern="*", action="allow"), + Rule(permission="list_tables_for_base", pattern="*", action="allow"), + Rule(permission="get_table_schema", pattern="*", action="allow"), + Rule(permission="list_records_for_table", pattern="*", action="allow"), + Rule(permission="search_records", pattern="*", action="allow"), + Rule(permission="create_records_for_table", pattern="*", action="ask"), + Rule(permission="update_records_for_table", pattern="*", action="ask"), + ], ) - - -def load_tools( - *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - _ = {**(dependencies or {}), **kwargs} - return { - "allow": [ - {"name": "list_bases"}, - {"name": "search_bases"}, - {"name": "list_tables_for_base"}, - {"name": "get_table_schema"}, - {"name": "list_records_for_table"}, - {"name": "search_records"}, - ], - "ask": [ - {"name": "create_records_for_table"}, - {"name": "update_records_for_table"}, - ], - } diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/agent.py index 3e62a5646..695013d2a 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/agent.py @@ -1,29 +1,22 @@ -"""`calendar` route: ``SubAgent`` spec for deepagents.""" +"""``calendar`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity with MCP-backed connectors. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "calendar" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles calendar tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles calendar tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/index.py index 28d871ef8..2570a51b2 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/index.py @@ -1,38 +1,39 @@ +"""``calendar`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies, so the +ruleset just falls through to the SurfSense allow-by-default rules. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .create_event import create_create_calendar_event_tool from .delete_event import create_delete_calendar_event_tool from .search_events import create_search_calendar_events_tool from .update_event import create_update_calendar_event_tool +NAME = "calendar" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - resolved_dependencies = {**(dependencies or {}), **kwargs} - session_dependencies = { - "db_session": resolved_dependencies["db_session"], - "search_space_id": resolved_dependencies["search_space_id"], - "user_id": resolved_dependencies["user_id"], - } - search = create_search_calendar_events_tool(**session_dependencies) - create = create_create_calendar_event_tool(**session_dependencies) - update = create_update_calendar_event_tool(**session_dependencies) - delete = create_delete_calendar_event_tool(**session_dependencies) - return { - "allow": [self_gated_tool_permission_row(search)], - "ask": [ - self_gated_tool_permission_row(create), - self_gated_tool_permission_row(update), - self_gated_tool_permission_row(delete), - ], +) -> list[BaseTool]: + d = {**(dependencies or {}), **kwargs} + common = { + "db_session": d["db_session"], + "search_space_id": d["search_space_id"], + "user_id": d["user_id"], } + return [ + create_search_calendar_events_tool(**common), + create_create_calendar_event_tool(**common), + create_update_calendar_event_tool(**common), + create_delete_calendar_event_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/agent.py index 2d09eda2b..1fec8aa0c 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/agent.py @@ -1,29 +1,22 @@ -"""`clickup` route: ``SubAgent`` spec for deepagents.""" +"""``clickup`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools come exclusively from MCP. The connector's own approval ruleset is +declared in :data:`tools.index.RULESET`; the orchestrator layers it into +a per-subagent :class:`PermissionMiddleware`. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "clickup" +from .tools.index import NAME, RULESET def build_subagent( @@ -31,26 +24,20 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles clickup tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + description = ( + read_md_file(__package__, "description").strip() + or "Handles clickup tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, - tools=tools, - interrupt_on=interrupt_on, + tools=list(mcp_tools or []), + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/tools/index.py index a0bc18a15..b2c523080 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/tools/index.py @@ -1,26 +1,20 @@ +"""``clickup`` permission ruleset (rules over MCP tool names).""" + from __future__ import annotations -from typing import Any +from app.agents.new_chat.permissions import Rule, Ruleset -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, +NAME = "clickup" + +RULESET = Ruleset( + origin=NAME, + rules=[ + Rule(permission="clickup_search", pattern="*", action="allow"), + Rule(permission="clickup_get_task", pattern="*", action="allow"), + Rule(permission="clickup_get_workspace_hierarchy", pattern="*", action="allow"), + Rule(permission="clickup_get_list", pattern="*", action="allow"), + Rule(permission="clickup_find_member_by_name", pattern="*", action="allow"), + Rule(permission="clickup_create_task", pattern="*", action="ask"), + Rule(permission="clickup_update_task", pattern="*", action="ask"), + ], ) - - -def load_tools( - *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - _ = {**(dependencies or {}), **kwargs} - return { - "allow": [ - {"name": "clickup_search"}, - {"name": "clickup_get_task"}, - {"name": "clickup_get_workspace_hierarchy"}, - {"name": "clickup_get_list"}, - {"name": "clickup_find_member_by_name"}, - ], - "ask": [ - {"name": "clickup_create_task"}, - {"name": "clickup_update_task"}, - ], - } diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/agent.py index 01c9e25f1..fab6ecbbe 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/agent.py @@ -1,29 +1,22 @@ -"""`confluence` route: ``SubAgent`` spec for deepagents.""" +"""``confluence`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "confluence" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles confluence tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles confluence tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/tools/index.py index 63fa5feb1..b38503c5c 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/tools/index.py @@ -1,37 +1,37 @@ +"""``confluence`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .create_page import create_create_confluence_page_tool from .delete_page import create_delete_confluence_page_tool from .update_page import create_update_confluence_page_tool +NAME = "confluence" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - resolved_dependencies = {**(dependencies or {}), **kwargs} - session_dependencies = { - "db_session": resolved_dependencies["db_session"], - "search_space_id": resolved_dependencies["search_space_id"], - "user_id": resolved_dependencies["user_id"], - "connector_id": resolved_dependencies.get("connector_id"), - } - create = create_create_confluence_page_tool(**session_dependencies) - update = create_update_confluence_page_tool(**session_dependencies) - delete = create_delete_confluence_page_tool(**session_dependencies) - return { - "allow": [], - "ask": [ - self_gated_tool_permission_row(create), - self_gated_tool_permission_row(update), - self_gated_tool_permission_row(delete), - ], +) -> list[BaseTool]: + d = {**(dependencies or {}), **kwargs} + common = { + "db_session": d["db_session"], + "search_space_id": d["search_space_id"], + "user_id": d["user_id"], + "connector_id": d.get("connector_id"), } + return [ + create_create_confluence_page_tool(**common), + create_update_confluence_page_tool(**common), + create_delete_confluence_page_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/agent.py index d4c936ff4..3bac0dc02 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/agent.py @@ -1,29 +1,22 @@ -"""`discord` route: ``SubAgent`` spec for deepagents.""" +"""``discord`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "discord" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles discord tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles discord tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/tools/index.py index 578d5b663..c69ef3e5c 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/tools/index.py @@ -1,35 +1,36 @@ +"""``discord`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .list_channels import create_list_discord_channels_tool from .read_messages import create_read_discord_messages_tool from .send_message import create_send_discord_message_tool +NAME = "discord" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: +) -> list[BaseTool]: d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], "search_space_id": d["search_space_id"], "user_id": d["user_id"], } - list_ch = create_list_discord_channels_tool(**common) - read_msg = create_read_discord_messages_tool(**common) - send = create_send_discord_message_tool(**common) - return { - "allow": [ - self_gated_tool_permission_row(list_ch), - self_gated_tool_permission_row(read_msg), - ], - "ask": [self_gated_tool_permission_row(send)], - } + return [ + create_list_discord_channels_tool(**common), + create_read_discord_messages_tool(**common), + create_send_discord_message_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/agent.py index e310454bc..dd5896869 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/agent.py @@ -1,29 +1,22 @@ -"""`dropbox` route: ``SubAgent`` spec for deepagents.""" +"""``dropbox`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "dropbox" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles dropbox tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles dropbox tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/tools/index.py index 6355ddae6..68e02866a 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/tools/index.py @@ -1,33 +1,34 @@ +"""``dropbox`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .create_file import create_create_dropbox_file_tool from .trash_file import create_delete_dropbox_file_tool +NAME = "dropbox" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: +) -> list[BaseTool]: d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], "search_space_id": d["search_space_id"], "user_id": d["user_id"], } - create = create_create_dropbox_file_tool(**common) - delete = create_delete_dropbox_file_tool(**common) - return { - "allow": [], - "ask": [ - self_gated_tool_permission_row(create), - self_gated_tool_permission_row(delete), - ], - } + return [ + create_create_dropbox_file_tool(**common), + create_delete_dropbox_file_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/agent.py index 39aacc872..fd5b64313 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/agent.py @@ -1,29 +1,22 @@ -"""`gmail` route: ``SubAgent`` spec for deepagents.""" +"""``gmail`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "gmail" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles gmail tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles gmail tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/index.py index e7c877e40..020089ebb 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/index.py @@ -1,13 +1,15 @@ +"""``gmail`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .create_draft import create_create_gmail_draft_tool from .read_email import create_read_gmail_email_tool @@ -16,31 +18,25 @@ from .send_email import create_send_gmail_email_tool from .trash_email import create_trash_gmail_email_tool from .update_draft import create_update_gmail_draft_tool +NAME = "gmail" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: +) -> list[BaseTool]: d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], "search_space_id": d["search_space_id"], "user_id": d["user_id"], } - search = create_search_gmail_tool(**common) - read = create_read_gmail_email_tool(**common) - draft = create_create_gmail_draft_tool(**common) - send = create_send_gmail_email_tool(**common) - trash = create_trash_gmail_email_tool(**common) - updraft = create_update_gmail_draft_tool(**common) - return { - "allow": [ - self_gated_tool_permission_row(search), - self_gated_tool_permission_row(read), - ], - "ask": [ - self_gated_tool_permission_row(draft), - self_gated_tool_permission_row(send), - self_gated_tool_permission_row(trash), - self_gated_tool_permission_row(updraft), - ], - } + return [ + create_search_gmail_tool(**common), + create_read_gmail_email_tool(**common), + create_create_gmail_draft_tool(**common), + create_send_gmail_email_tool(**common), + create_trash_gmail_email_tool(**common), + create_update_gmail_draft_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/agent.py index 93844e879..7acb7431d 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/agent.py @@ -1,29 +1,22 @@ -"""`google_drive` route: ``SubAgent`` spec for deepagents.""" +"""``google_drive`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "google_drive" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles google drive tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles google drive tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/tools/index.py index fc2195623..dd05374a1 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/tools/index.py @@ -1,33 +1,34 @@ +"""``google_drive`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .create_file import create_create_google_drive_file_tool from .trash_file import create_delete_google_drive_file_tool +NAME = "google_drive" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: +) -> list[BaseTool]: d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], "search_space_id": d["search_space_id"], "user_id": d["user_id"], } - create = create_create_google_drive_file_tool(**common) - delete = create_delete_google_drive_file_tool(**common) - return { - "allow": [], - "ask": [ - self_gated_tool_permission_row(create), - self_gated_tool_permission_row(delete), - ], - } + return [ + create_create_google_drive_file_tool(**common), + create_delete_google_drive_file_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/agent.py index 0ac52ac83..0ad47f45d 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/agent.py @@ -1,29 +1,22 @@ -"""`jira` route: ``SubAgent`` spec for deepagents.""" +"""``jira`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools come exclusively from MCP. The connector's own approval ruleset is +declared in :data:`tools.index.RULESET`; the orchestrator layers it into +a per-subagent :class:`PermissionMiddleware`. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "jira" +from .tools.index import NAME, RULESET def build_subagent( @@ -31,26 +24,20 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles jira tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + description = ( + read_md_file(__package__, "description").strip() + or "Handles jira tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, - tools=tools, - interrupt_on=interrupt_on, + tools=list(mcp_tools or []), + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/tools/index.py index 67e213430..13b2a073c 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/tools/index.py @@ -1,30 +1,24 @@ +"""``jira`` permission ruleset (rules over MCP tool names).""" + from __future__ import annotations -from typing import Any +from app.agents.new_chat.permissions import Rule, Ruleset -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, +NAME = "jira" + +RULESET = Ruleset( + origin=NAME, + rules=[ + Rule(permission="getAccessibleAtlassianResources", pattern="*", action="allow"), + Rule(permission="getVisibleJiraProjects", pattern="*", action="allow"), + Rule(permission="searchJiraIssuesUsingJql", pattern="*", action="allow"), + Rule(permission="getJiraIssue", pattern="*", action="allow"), + Rule(permission="getJiraProjectIssueTypesMetadata", pattern="*", action="allow"), + Rule(permission="getJiraIssueTypeMetaWithFields", pattern="*", action="allow"), + Rule(permission="getTransitionsForJiraIssue", pattern="*", action="allow"), + Rule(permission="lookupJiraAccountId", pattern="*", action="allow"), + Rule(permission="createJiraIssue", pattern="*", action="ask"), + Rule(permission="editJiraIssue", pattern="*", action="ask"), + Rule(permission="transitionJiraIssue", pattern="*", action="ask"), + ], ) - - -def load_tools( - *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - _ = {**(dependencies or {}), **kwargs} - return { - "allow": [ - {"name": "getAccessibleAtlassianResources"}, - {"name": "getVisibleJiraProjects"}, - {"name": "searchJiraIssuesUsingJql"}, - {"name": "getJiraIssue"}, - {"name": "getJiraProjectIssueTypesMetadata"}, - {"name": "getJiraIssueTypeMetaWithFields"}, - {"name": "getTransitionsForJiraIssue"}, - {"name": "lookupJiraAccountId"}, - ], - "ask": [ - {"name": "createJiraIssue"}, - {"name": "editJiraIssue"}, - {"name": "transitionJiraIssue"}, - ], - } diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/agent.py index a50f1bd68..0b0580704 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/agent.py @@ -1,29 +1,22 @@ -"""`linear` route: ``SubAgent`` spec for deepagents.""" +"""``linear`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools come exclusively from MCP. The connector's own approval ruleset is +declared in :data:`tools.index.RULESET`; the orchestrator layers it into +a per-subagent :class:`PermissionMiddleware`. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "linear" +from .tools.index import NAME, RULESET def build_subagent( @@ -31,26 +24,20 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles linear tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + description = ( + read_md_file(__package__, "description").strip() + or "Handles linear tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, - tools=tools, - interrupt_on=interrupt_on, + tools=list(mcp_tools or []), + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/tools/index.py index 69c272365..4a71a31b8 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/tools/index.py @@ -1,37 +1,31 @@ +"""``linear`` permission ruleset (rules over MCP tool names).""" + from __future__ import annotations -from typing import Any +from app.agents.new_chat.permissions import Rule, Ruleset -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, +NAME = "linear" + +RULESET = Ruleset( + origin=NAME, + rules=[ + Rule(permission="list_issues", pattern="*", action="allow"), + Rule(permission="get_issue", pattern="*", action="allow"), + Rule(permission="list_my_issues", pattern="*", action="allow"), + Rule(permission="list_issue_statuses", pattern="*", action="allow"), + Rule(permission="list_issue_labels", pattern="*", action="allow"), + Rule(permission="list_comments", pattern="*", action="allow"), + Rule(permission="list_users", pattern="*", action="allow"), + Rule(permission="get_user", pattern="*", action="allow"), + Rule(permission="list_teams", pattern="*", action="allow"), + Rule(permission="get_team", pattern="*", action="allow"), + Rule(permission="list_projects", pattern="*", action="allow"), + Rule(permission="get_project", pattern="*", action="allow"), + Rule(permission="list_project_labels", pattern="*", action="allow"), + Rule(permission="list_cycles", pattern="*", action="allow"), + Rule(permission="list_documents", pattern="*", action="allow"), + Rule(permission="get_document", pattern="*", action="allow"), + Rule(permission="search_documentation", pattern="*", action="allow"), + Rule(permission="save_issue", pattern="*", action="ask"), + ], ) - - -def load_tools( - *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - _ = {**(dependencies or {}), **kwargs} - return { - "allow": [ - {"name": "list_issues"}, - {"name": "get_issue"}, - {"name": "list_my_issues"}, - {"name": "list_issue_statuses"}, - {"name": "list_issue_labels"}, - {"name": "list_comments"}, - {"name": "list_users"}, - {"name": "get_user"}, - {"name": "list_teams"}, - {"name": "get_team"}, - {"name": "list_projects"}, - {"name": "get_project"}, - {"name": "list_project_labels"}, - {"name": "list_cycles"}, - {"name": "list_documents"}, - {"name": "get_document"}, - {"name": "search_documentation"}, - ], - "ask": [ - {"name": "save_issue"} - ], - } diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/agent.py index 807ecaca7..b1d3ecead 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/agent.py @@ -1,29 +1,22 @@ -"""`luma` route: ``SubAgent`` spec for deepagents.""" +"""``luma`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "luma" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles luma tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles luma tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/index.py index 92e372120..dbde01061 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/index.py @@ -1,35 +1,36 @@ +"""``luma`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .create_event import create_create_luma_event_tool from .list_events import create_list_luma_events_tool from .read_event import create_read_luma_event_tool +NAME = "luma" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: +) -> list[BaseTool]: d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], "search_space_id": d["search_space_id"], "user_id": d["user_id"], } - list_ev = create_list_luma_events_tool(**common) - read_ev = create_read_luma_event_tool(**common) - create = create_create_luma_event_tool(**common) - return { - "allow": [ - self_gated_tool_permission_row(list_ev), - self_gated_tool_permission_row(read_ev), - ], - "ask": [self_gated_tool_permission_row(create)], - } + return [ + create_list_luma_events_tool(**common), + create_read_luma_event_tool(**common), + create_create_luma_event_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/agent.py index cb3c3f52c..17c8466ab 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/agent.py @@ -1,29 +1,22 @@ -"""`notion` route: ``SubAgent`` spec for deepagents.""" +"""``notion`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "notion" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles notion tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles notion tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/tools/index.py index 175a5bcd1..0475e9dd0 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/tools/index.py @@ -1,36 +1,36 @@ +"""``notion`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .create_page import create_create_notion_page_tool from .delete_page import create_delete_notion_page_tool from .update_page import create_update_notion_page_tool +NAME = "notion" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: +) -> list[BaseTool]: d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], "search_space_id": d["search_space_id"], "user_id": d["user_id"], } - create = create_create_notion_page_tool(**common) - update = create_update_notion_page_tool(**common) - delete = create_delete_notion_page_tool(**common) - return { - "allow": [], - "ask": [ - self_gated_tool_permission_row(create), - self_gated_tool_permission_row(update), - self_gated_tool_permission_row(delete), - ], - } + return [ + create_create_notion_page_tool(**common), + create_update_notion_page_tool(**common), + create_delete_notion_page_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/agent.py index 1e446efbb..790d1a133 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/agent.py @@ -1,29 +1,22 @@ -"""`onedrive` route: ``SubAgent`` spec for deepagents.""" +"""``onedrive`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "onedrive" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles onedrive tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles onedrive tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/tools/index.py index 777560112..e09b43200 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/tools/index.py @@ -1,33 +1,34 @@ +"""``onedrive`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .create_file import create_create_onedrive_file_tool from .trash_file import create_delete_onedrive_file_tool +NAME = "onedrive" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: +) -> list[BaseTool]: d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], "search_space_id": d["search_space_id"], "user_id": d["user_id"], } - create = create_create_onedrive_file_tool(**common) - delete = create_delete_onedrive_file_tool(**common) - return { - "allow": [], - "ask": [ - self_gated_tool_permission_row(create), - self_gated_tool_permission_row(delete), - ], - } + return [ + create_create_onedrive_file_tool(**common), + create_delete_onedrive_file_tool(**common), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/agent.py index a1f108d62..d2426d92e 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/agent.py @@ -1,29 +1,22 @@ -"""`slack` route: ``SubAgent`` spec for deepagents.""" +"""``slack`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools come exclusively from MCP. The connector's own approval ruleset is +declared in :data:`tools.index.RULESET`; the orchestrator layers it into +a per-subagent :class:`PermissionMiddleware`. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "slack" +from .tools.index import NAME, RULESET def build_subagent( @@ -31,26 +24,20 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles slack tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + description = ( + read_md_file(__package__, "description").strip() + or "Handles slack tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, - tools=tools, - interrupt_on=interrupt_on, + tools=list(mcp_tools or []), + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/tools/index.py index 39751fa1b..44b96661c 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/tools/index.py @@ -1,25 +1,19 @@ +"""``slack`` permission ruleset (rules over MCP tool names).""" + from __future__ import annotations -from typing import Any +from app.agents.new_chat.permissions import Rule, Ruleset -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, +NAME = "slack" + +RULESET = Ruleset( + origin=NAME, + rules=[ + Rule(permission="slack_search_channels", pattern="*", action="allow"), + Rule(permission="slack_search_messages", pattern="*", action="allow"), + Rule(permission="slack_search_users", pattern="*", action="allow"), + Rule(permission="slack_read_channel", pattern="*", action="allow"), + Rule(permission="slack_read_thread", pattern="*", action="allow"), + Rule(permission="slack_send_message", pattern="*", action="ask"), + ], ) - - -def load_tools( - *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - _ = {**(dependencies or {}), **kwargs} - return { - "allow": [ - {"name": "slack_search_channels"}, - {"name": "slack_search_messages"}, - {"name": "slack_search_users"}, - {"name": "slack_read_channel"}, - {"name": "slack_read_thread"}, - ], - "ask": [ - {"name": "slack_send_message"}, - ], - } diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/agent.py index 05f3ab3af..4e39a52f8 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/agent.py @@ -1,29 +1,22 @@ -"""`teams` route: ``SubAgent`` spec for deepagents.""" +"""``teams`` route: ``SurfSenseSubagentSpec`` builder for deepagents. + +Tools self-gate inside their bodies via :func:`request_approval`; the +empty :data:`tools.index.RULESET` is layered into a per-subagent +:class:`PermissionMiddleware` for uniformity. +""" from __future__ import annotations from typing import Any -from deepagents import SubAgent from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( - middleware_gated_interrupt_on, -) -from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( - read_md_file, -) -from app.agents.multi_agent_chat.subagents.shared.subagent_builder import ( - pack_subagent, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, - merge_tools_permissions, -) +from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent -from .tools.index import load_tools - -NAME = "teams" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) - if row.get("tool") is not None - ] - interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket) - description = read_md_file(__package__, "description").strip() - if not description: - description = "Handles teams tasks for this workspace." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles teams tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/tools/index.py index 59a751ec2..41661651f 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/tools/index.py @@ -1,35 +1,36 @@ +"""``teams`` native tools and (empty) permission ruleset. + +Tools self-gate via :func:`request_approval` in their bodies. +""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .list_channels import create_list_teams_channels_tool from .read_messages import create_read_teams_messages_tool from .send_message import create_send_teams_message_tool +NAME = "teams" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: +) -> list[BaseTool]: d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], "search_space_id": d["search_space_id"], "user_id": d["user_id"], } - list_ch = create_list_teams_channels_tool(**common) - read_msg = create_read_teams_messages_tool(**common) - send = create_send_teams_message_tool(**common) - return { - "allow": [ - self_gated_tool_permission_row(list_ch), - self_gated_tool_permission_row(read_msg), - ], - "ask": [self_gated_tool_permission_row(send)], - } + return [ + create_list_teams_channels_tool(**common), + create_read_teams_messages_tool(**common), + create_send_teams_message_tool(**common), + ]