diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/middleware/factory.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/middleware/factory.py index 4b760ec6f..bde5c1387 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/middleware/factory.py +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/middleware/factory.py @@ -9,7 +9,7 @@ matching OpenCode's ``permission/index.ts`` evaluation order): needs to *deny* what the user has explicitly forbidden; the default ``ask`` fallback would otherwise double-prompt every safe read-only call. -2. ``extra_rulesets`` — caller-supplied policies. The KB subagent contributes +2. ``extra_rulesets`` — caller-supplied rulesets. The KB subagent contributes its destructive-FS ``ask`` rules here; connectors will follow once they migrate off ``interrupt_on``. @@ -44,7 +44,7 @@ def build_permission_mw( flags: Feature toggles. ``enable_permission`` switches the engine on; ``disable_new_agent_stack`` overrides everything for safety. extra_rulesets: Caller-supplied rulesets layered after the defaults. - Subagents pass their own policy here so each subagent owns its + Subagents pass their own ruleset here so each subagent owns its rules without aliasing a shared engine. Presence of any extra ruleset forces the middleware on regardless of ``enable_permission`` — an explicit ``ask`` rule always asks. diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py index 9a99eae62..ac1e9cdb7 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py @@ -1,7 +1,7 @@ """``knowledge_base`` route: full and read-only ``SurfSenseSubagentSpec`` builders. -KB owns the destructive-FS approval policy: rules live in :data:`KB_RULESET` -and are layered into KB's :class:`PermissionMiddleware` (built inside +KB owns its destructive-FS approval ruleset (:data:`KB_RULESET`); rules +are layered into KB's :class:`PermissionMiddleware` (built inside ``build_kb_middleware``). The legacy ``interrupt_on`` kwarg is gone — one emitter, one wire format, one source of truth. """ diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py index bbfe38bc3..707ae535f 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py @@ -44,7 +44,7 @@ def build_kb_middleware( ) -> list[Any]: """Compose the KB subagent's middleware list. - ``ruleset`` is the KB-owned permission policy (typically the + ``ruleset`` is the KB-owned permission ruleset (typically the destructive-FS ask rules). When provided, a dedicated :class:`PermissionMiddleware` is appended so KB enforces approval at the rule layer instead of the legacy ``interrupt_on`` kwarg. diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/tools/__init__.py index 616dfc814..5a83c68a3 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/tools/__init__.py @@ -1 +1 @@ -"""Route-local tool policy for the ``knowledge_base`` subagent.""" +"""Route-local tool permissions for the ``knowledge_base`` subagent.""" diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/tools/index.py index eddde6ac2..55a9a4edf 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/tools/index.py @@ -1,4 +1,4 @@ -"""Route-local FS tool policy. +"""Route-local FS tool permissions. The KB subagent's actual ``BaseTool`` instances are provided at runtime by ``SurfSenseFilesystemMiddleware`` (mounted in ``agent.py``). This module 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 b7556771b..37efbecf1 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 @@ -11,4 +11,17 @@ def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any ) -> ToolsPermissions: _ = {**(dependencies or {}), **kwargs} - return {"allow": [], "ask": []} + 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/clickup/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/tools/index.py index b7556771b..a0bc18a15 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 @@ -11,4 +11,16 @@ def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any ) -> ToolsPermissions: _ = {**(dependencies or {}), **kwargs} - return {"allow": [], "ask": []} + 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/jira/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/tools/index.py index b7556771b..67e213430 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 @@ -11,4 +11,20 @@ def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any ) -> ToolsPermissions: _ = {**(dependencies or {}), **kwargs} - return {"allow": [], "ask": []} + 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/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/tools/index.py index b7556771b..69c272365 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 @@ -11,4 +11,27 @@ def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any ) -> ToolsPermissions: _ = {**(dependencies or {}), **kwargs} - return {"allow": [], "ask": []} + 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/slack/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/tools/index.py index b7556771b..39751fa1b 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 @@ -11,4 +11,15 @@ def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any ) -> ToolsPermissions: _ = {**(dependencies or {}), **kwargs} - return {"allow": [], "ask": []} + 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/mcp_tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/__init__.py index c8714cd04..40c6b9ccc 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/__init__.py @@ -1,11 +1,7 @@ -"""Load MCP tools, partition by connector agent, apply allow/ask name rules.""" +"""Load MCP tools, partition by connector agent, apply each subagent's allow/ask permissions.""" from __future__ import annotations -from app.agents.multi_agent_chat.subagents.mcp_tools.permissions import ( - TOOLS_PERMISSIONS_BY_AGENT, -) - from .index import ( fetch_mcp_connector_metadata_maps, load_mcp_tools_by_connector, @@ -13,7 +9,6 @@ from .index import ( ) __all__ = [ - "TOOLS_PERMISSIONS_BY_AGENT", "fetch_mcp_connector_metadata_maps", "load_mcp_tools_by_connector", "partition_mcp_tools_by_connector", diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/index.py index 028955791..004892f13 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/index.py @@ -1,4 +1,4 @@ -"""Discover MCP tools, bucket by connector agent, apply allow/ask from policy.""" +"""Discover MCP tools, bucket by connector agent, apply each subagent's allow/ask permissions.""" from __future__ import annotations @@ -15,8 +15,20 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.agents.multi_agent_chat.constants import ( CONNECTOR_TYPE_TO_CONNECTOR_AGENT_MAPS, ) -from app.agents.multi_agent_chat.subagents.mcp_tools.permissions import ( - TOOLS_PERMISSIONS_BY_AGENT, +from app.agents.multi_agent_chat.subagents.connectors.airtable.tools.index import ( + load_tools as _airtable_permissions, +) +from app.agents.multi_agent_chat.subagents.connectors.clickup.tools.index import ( + load_tools as _clickup_permissions, +) +from app.agents.multi_agent_chat.subagents.connectors.jira.tools.index import ( + load_tools as _jira_permissions, +) +from app.agents.multi_agent_chat.subagents.connectors.linear.tools.index import ( + load_tools as _linear_permissions, +) +from app.agents.multi_agent_chat.subagents.connectors.slack.tools.index import ( + load_tools as _slack_permissions, ) from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( middleware_gated_tool_permission_row, @@ -31,6 +43,15 @@ from app.db import SearchSourceConnector logger = logging.getLogger(__name__) +_MCP_PERMISSIONS_BY_AGENT: dict[str, ToolsPermissions] = { + "airtable": _airtable_permissions(), + "clickup": _clickup_permissions(), + "jira": _jira_permissions(), + "linear": _linear_permissions(), + "slack": _slack_permissions(), +} + + ## Helper functions for fetching connector metadata maps @@ -150,7 +171,7 @@ async def load_mcp_tools_by_connector( session: AsyncSession, search_space_id: int, ) -> dict[str, ToolsPermissions]: - """Load MCP tools and split rows using ``TOOLS_PERMISSIONS_BY_AGENT`` name sets. + """Load MCP tools and split rows per subagent's own allow/ask permissions. Pass ``bypass_internal_hitl=True`` so the subagent's ``HumanInTheLoopMiddleware`` is the single HITL gate. @@ -161,7 +182,7 @@ async def load_mcp_tools_by_connector( return { agent: _split_tools_by_permissions( tools, - TOOLS_PERMISSIONS_BY_AGENT.get(agent, {"allow": [], "ask": []}), + _MCP_PERMISSIONS_BY_AGENT.get(agent, {"allow": [], "ask": []}), ) for agent, tools in buckets.items() } diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/__init__.py deleted file mode 100644 index a328d704d..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Bundled MCP allow/ask name rows per connector agent (MCP-backed routes only).""" - -from __future__ import annotations - -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) - -from .airtable import TOOLS_PERMISSIONS as _AIRTABLE -from .clickup import TOOLS_PERMISSIONS as _CLICKUP -from .jira import TOOLS_PERMISSIONS as _JIRA -from .linear import TOOLS_PERMISSIONS as _LINEAR -from .slack import TOOLS_PERMISSIONS as _SLACK - -TOOLS_PERMISSIONS_BY_AGENT: dict[str, ToolsPermissions] = { - "airtable": _AIRTABLE, - "clickup": _CLICKUP, - "jira": _JIRA, - "linear": _LINEAR, - "slack": _SLACK, -} - -__all__ = ["TOOLS_PERMISSIONS_BY_AGENT"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/airtable.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/airtable.py deleted file mode 100644 index bf5ec7915..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/airtable.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Airtable MCP: which server tool names are allow vs ask.""" - -from __future__ import annotations - -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) - -TOOLS_PERMISSIONS: ToolsPermissions = { - "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/mcp_tools/permissions/clickup.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/clickup.py deleted file mode 100644 index 6c53106a8..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/clickup.py +++ /dev/null @@ -1,21 +0,0 @@ -"""ClickUp MCP: which server tool names are allow vs ask.""" - -from __future__ import annotations - -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) - -TOOLS_PERMISSIONS: ToolsPermissions = { - "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/mcp_tools/permissions/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/index.py deleted file mode 100644 index 91cf382ff..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/index.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Re-exports permission row types for MCP policy modules.""" - -from __future__ import annotations - -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolPermissionItem, - ToolsPermissions, -) - -__all__ = ["ToolPermissionItem", "ToolsPermissions"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/jira.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/jira.py deleted file mode 100644 index 6bb004a22..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/jira.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Jira MCP: which server tool names are allow vs ask.""" - -from __future__ import annotations - -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) - -TOOLS_PERMISSIONS: ToolsPermissions = { - "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/mcp_tools/permissions/linear.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/linear.py deleted file mode 100644 index f55460c46..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/linear.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Linear MCP: which server tool names are allow vs ask.""" - -from __future__ import annotations - -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) - -_TOOLS_ALLOW = ( - "list_issues", - "get_issue", - "list_my_issues", - "list_issue_statuses", - "list_issue_labels", - "list_comments", - "list_users", - "get_user", - "list_teams", - "get_team", - "list_projects", - "get_project", - "list_project_labels", - "list_cycles", - "list_documents", - "get_document", - "search_documentation", -) - -TOOLS_PERMISSIONS: ToolsPermissions = { - "allow": [{"name": n} for n in _TOOLS_ALLOW], - "ask": [{"name": "save_issue"}], -} diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/slack.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/slack.py deleted file mode 100644 index 155065f12..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/permissions/slack.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Slack MCP: which server tool names are allow vs ask.""" - -from __future__ import annotations - -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) - -TOOLS_PERMISSIONS: ToolsPermissions = { - "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/shared/spec.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/spec.py index e587b7b8c..797ab535b 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/spec.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/spec.py @@ -1,5 +1,4 @@ -"""SurfSense's subagent contribution: deepagents spec + permission policy. -""" +"""SurfSense's subagent contribution: deepagents spec + permission ruleset.""" from __future__ import annotations @@ -19,7 +18,7 @@ class SurfSenseSubagentSpec: only fields ``deepagents.SubAgent`` recognises. ruleset: Permission rules this subagent contributes. The orchestrator layers them into the subagent's :class:`PermissionMiddleware`, - so each subagent owns its own policy without aliasing the + so each subagent owns its own ruleset without aliasing the shared rule engine. """ diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/tool_kinds.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/tool_kinds.py index d3e9b1f17..9b6d824a7 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/tool_kinds.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/tool_kinds.py @@ -31,7 +31,7 @@ class ToolPermissionItem(TypedDict): """One allow/ask row. ``name`` is always set; ``tool`` is present when a bound BaseTool exists - (absent for name-only MCP policy rows). ``kind`` defaults to + (absent for name-only MCP allow/ask rows). ``kind`` defaults to ``self_gated`` when absent so existing connector factories keep working without explicit tagging. """