Skip middleware gate for native body-gated tools to restore approval-card context.

This commit is contained in:
CREDO23 2026-05-04 19:25:50 +02:00
parent 277bd50f37
commit 7735becd02
20 changed files with 72 additions and 27 deletions

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket)
description = read_md_file(__package__, "description").strip()
if not description:
description = "Handles deliverables tasks for this workspace."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket)
description = read_md_file(__package__, "description").strip()
if not description:
description = "Handles memory tasks for this workspace."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket)
description = read_md_file(__package__, "description").strip()
if not description:
description = "Handles research tasks for this workspace."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -14,6 +14,7 @@ from app.agents.multi_agent_with_deepagents.subagents.shared.md_file_reader impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolsPermissions,
merge_tools_permissions,
middleware_gated_interrupt_on,
)
from app.agents.multi_agent_with_deepagents.subagents.shared.subagent_builder import (
pack_subagent,
@ -38,7 +39,7 @@ def build_subagent(
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"])
if row.get("tool") is not None
]
interrupt_on = {r["name"]: True for r in merged_tools_bucket["ask"] if r.get("name")}
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."

View file

@ -21,7 +21,7 @@ from app.agents.multi_agent_with_deepagents.subagents.mcp_tools.permissions impo
from app.agents.multi_agent_with_deepagents.subagents.shared.permissions import (
ToolPermissionItem,
ToolsPermissions,
tool_permission_row,
mcp_tool_permission_row,
)
from app.agents.new_chat.tools.mcp_tool import load_mcp_tools
from app.db import SearchSourceConnector
@ -125,15 +125,15 @@ def _split_tools_by_permissions(
for t in tools:
meta: dict[str, Any] = getattr(t, "metadata", None) or {}
if meta.get("hitl") is False:
allow.append(tool_permission_row(t))
allow.append(mcp_tool_permission_row(t))
continue
key = _get_mcp_tool_name(t)
if key in allow_names:
allow.append(tool_permission_row(t))
allow.append(mcp_tool_permission_row(t))
elif key in ask_names:
ask.append(tool_permission_row(t))
ask.append(mcp_tool_permission_row(t))
else:
ask.append(tool_permission_row(t))
ask.append(mcp_tool_permission_row(t))
return {"allow": allow, "ask": ask}
@ -143,8 +143,14 @@ 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."""
flat = await load_mcp_tools(session, search_space_id)
"""Load MCP tools and split rows using ``TOOLS_PERMISSIONS_BY_AGENT`` name sets.
Pass ``bypass_internal_hitl=True`` so the subagent's
``HumanInTheLoopMiddleware`` is the single HITL gate.
"""
flat = await load_mcp_tools(
session, search_space_id, bypass_internal_hitl=True
)
id_map, name_map = await fetch_mcp_connector_metadata_maps(session, search_space_id)
buckets = partition_mcp_tools_by_connector(flat, id_map, name_map)
return {

View file

@ -2,16 +2,21 @@
from __future__ import annotations
from typing import NotRequired, TypedDict
from typing import Literal, NotRequired, TypedDict
from langchain_core.tools import BaseTool
# ``native`` rows self-gate via ``request_approval`` in the tool body;
# ``mcp`` rows are gated by ``HumanInTheLoopMiddleware`` via ``interrupt_on``.
ToolKind = Literal["native", "mcp"]
class ToolPermissionItem(TypedDict):
"""``name`` is always set; ``tool`` is present when a bound tool exists."""
"""``name`` is always set; ``tool`` is present when a bound tool exists; ``kind`` defaults to ``native`` when absent."""
name: str
tool: NotRequired[BaseTool]
kind: NotRequired[ToolKind]
class ToolsPermissions(TypedDict):
@ -26,6 +31,11 @@ def tool_permission_row(tool: BaseTool) -> ToolPermissionItem:
return {"name": getattr(tool, "name", "") or "", "tool": tool}
def mcp_tool_permission_row(tool: BaseTool) -> ToolPermissionItem:
"""Build one allow/ask row tagged ``kind="mcp"`` so it routes through ``HumanInTheLoopMiddleware``."""
return {"name": getattr(tool, "name", "") or "", "tool": tool, "kind": "mcp"}
def merge_tools_permissions(
base: ToolsPermissions,
extra: ToolsPermissions | None,
@ -37,3 +47,14 @@ def merge_tools_permissions(
"allow": [*base["allow"], *extra["allow"]],
"ask": [*base["ask"], *extra["ask"]],
}
def middleware_gated_interrupt_on(
bucket: ToolsPermissions,
) -> dict[str, bool]:
"""``interrupt_on`` for ``ask`` rows whose bodies don't self-gate via ``request_approval``."""
return {
r["name"]: True
for r in bucket["ask"]
if r.get("name") and r.get("kind") == "mcp"
}