mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
multi_agent_chat: scope MCP allow/ask permissions per subagent + drop "policy" synonym
This commit is contained in:
parent
0723702320
commit
67142e68b1
21 changed files with 117 additions and 180 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
"""Route-local tool policy for the ``knowledge_base`` subagent."""
|
||||
"""Route-local tool permissions for the ``knowledge_base`` subagent."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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"},
|
||||
],
|
||||
}
|
||||
|
|
@ -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"},
|
||||
],
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"},
|
||||
],
|
||||
}
|
||||
|
|
@ -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"}],
|
||||
}
|
||||
|
|
@ -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"},
|
||||
],
|
||||
}
|
||||
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue