mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-19 18:45:15 +02:00
multi_agent_chat/permissions: layer user allow-list into subagent compile
This commit is contained in:
parent
e99c06c887
commit
ef1152b80e
25 changed files with 304 additions and 62 deletions
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ def build_subagent(
|
|||
dependencies=dependencies,
|
||||
middleware_stack=middleware_stack,
|
||||
read_only=False,
|
||||
subagent_name=NAME,
|
||||
ruleset=KB_RULESET,
|
||||
),
|
||||
},
|
||||
|
|
@ -83,6 +84,7 @@ def build_readonly_subagent(
|
|||
dependencies=dependencies,
|
||||
middleware_stack=middleware_stack,
|
||||
read_only=True,
|
||||
subagent_name=READONLY_NAME,
|
||||
ruleset=None,
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,20 +33,46 @@ from app.agents.new_chat.filesystem_selection import FilesystemMode
|
|||
from app.agents.new_chat.permissions import Ruleset
|
||||
|
||||
|
||||
def _kb_user_allowlist(
|
||||
dependencies: dict[str, Any], subagent_name: str
|
||||
) -> Ruleset | None:
|
||||
"""Return the user's persisted allow-rules for ``subagent_name`` if any.
|
||||
|
||||
KB does not currently expose an "Always Allow" UI surface (the FE
|
||||
button is MCP-only today), but the wiring is symmetrical with the
|
||||
connector subagents so that adding KB trust later is a one-line
|
||||
backend change.
|
||||
"""
|
||||
by_subagent = dependencies.get("user_allowlist_by_subagent") or {}
|
||||
user_allowlist = by_subagent.get(subagent_name)
|
||||
if isinstance(user_allowlist, Ruleset) and user_allowlist.rules:
|
||||
return user_allowlist
|
||||
return None
|
||||
|
||||
|
||||
def build_kb_middleware(
|
||||
*,
|
||||
llm: BaseChatModel,
|
||||
dependencies: dict[str, Any],
|
||||
middleware_stack: dict[str, Any] | None,
|
||||
read_only: bool,
|
||||
subagent_name: str,
|
||||
ruleset: Ruleset | None = None,
|
||||
) -> list[Any]:
|
||||
"""Compose the KB subagent's middleware list.
|
||||
|
||||
``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.
|
||||
Args:
|
||||
subagent_name: Identity of the subagent being built (e.g.
|
||||
``"knowledge_base"``, ``"knowledge_base_readonly"``). Used to
|
||||
look up the user's persistent allow-list bucket in
|
||||
``dependencies["user_allowlist_by_subagent"]``.
|
||||
ruleset: 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. The user's persistent allow-list
|
||||
for ``subagent_name`` is layered after ``ruleset`` so user
|
||||
``allow`` rules override coded ``ask`` rules via
|
||||
last-match-wins.
|
||||
"""
|
||||
mws = middleware_stack or {}
|
||||
filesystem_mode: FilesystemMode = dependencies["filesystem_mode"]
|
||||
|
|
@ -61,11 +87,13 @@ def build_kb_middleware(
|
|||
)
|
||||
if m is not None
|
||||
]
|
||||
permission_mw = (
|
||||
build_permission_mw(flags=flags, extra_rulesets=[ruleset])
|
||||
if (ruleset is not None and flags is not None)
|
||||
else None
|
||||
)
|
||||
permission_mw = None
|
||||
if ruleset is not None and flags is not None:
|
||||
rulesets: list[Ruleset] = [ruleset]
|
||||
user_allowlist = _kb_user_allowlist(dependencies, subagent_name)
|
||||
if user_allowlist is not None:
|
||||
rulesets.append(user_allowlist)
|
||||
permission_mw = build_permission_mw(flags=flags, subagent_rulesets=rulesets)
|
||||
return [
|
||||
mws["todos"],
|
||||
build_kb_context_projection_mw(),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=list(mcp_tools or []),
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=list(mcp_tools or []),
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=list(mcp_tools or []),
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=list(mcp_tools or []),
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=list(mcp_tools or []),
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def build_subagent(
|
|||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
ruleset=RULESET,
|
||||
flags=dependencies["flags"],
|
||||
dependencies=dependencies,
|
||||
model=model,
|
||||
middleware_stack=middleware_stack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,10 +13,26 @@ from app.agents.multi_agent_chat.middleware.shared.permissions import (
|
|||
build_permission_mw,
|
||||
)
|
||||
from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec
|
||||
from app.agents.new_chat.feature_flags import AgentFeatureFlags
|
||||
from app.agents.new_chat.permissions import Ruleset
|
||||
|
||||
|
||||
def _user_allowlist_for(
|
||||
dependencies: dict[str, Any], subagent_name: str
|
||||
) -> Ruleset | None:
|
||||
"""Return the user's persisted allow-rules for ``subagent_name`` if any.
|
||||
|
||||
Populated by the agent factory from
|
||||
:func:`app.services.user_tool_allowlist.fetch_user_allowlist_rulesets`.
|
||||
Returning ``None`` is the common case (fresh accounts, non-MCP
|
||||
subagents, or no "Always Allow" interactions yet).
|
||||
"""
|
||||
by_subagent = dependencies.get("user_allowlist_by_subagent") or {}
|
||||
user_allowlist = by_subagent.get(subagent_name)
|
||||
if isinstance(user_allowlist, Ruleset) and user_allowlist.rules:
|
||||
return user_allowlist
|
||||
return None
|
||||
|
||||
|
||||
def pack_subagent(
|
||||
*,
|
||||
name: str,
|
||||
|
|
@ -24,23 +40,43 @@ def pack_subagent(
|
|||
system_prompt: str,
|
||||
tools: list[BaseTool],
|
||||
ruleset: Ruleset,
|
||||
flags: AgentFeatureFlags,
|
||||
dependencies: dict[str, Any],
|
||||
model: BaseChatModel | None = None,
|
||||
middleware_stack: dict[str, Any] | None = None,
|
||||
) -> SurfSenseSubagentSpec:
|
||||
"""Pack the route-local pieces into one sub-agent spec + its Ruleset.
|
||||
|
||||
Tool gating is uniformly performed by a per-subagent
|
||||
:class:`PermissionMiddleware` built from the subagent's own
|
||||
``ruleset`` (layered on top of the SurfSense defaults). The shared
|
||||
``permission`` slot from ``middleware_stack`` is dropped so each
|
||||
subagent owns its own rule surface.
|
||||
:class:`PermissionMiddleware`. Three rule layers are evaluated
|
||||
earliest-to-latest (last match wins):
|
||||
|
||||
1. SurfSense defaults — single ``allow */*`` rule (added by
|
||||
:func:`build_permission_mw`).
|
||||
2. ``ruleset`` — the subagent's coded approval rules (e.g. KB's
|
||||
destructive-FS ``ask`` rules, connector ``ask`` writes).
|
||||
3. The user's persisted allow-list for this subagent — pulled from
|
||||
``dependencies['user_allowlist_by_subagent'][name]``. User
|
||||
``allow`` rules layered last override coded ``ask`` rules,
|
||||
implementing the "Always Allow" UX without re-asking on the
|
||||
next turn.
|
||||
|
||||
The shared ``permission`` slot from ``middleware_stack`` is dropped
|
||||
so each subagent owns its own rule surface and cannot accidentally
|
||||
share state with the main agent's permission middleware.
|
||||
"""
|
||||
if not system_prompt.strip():
|
||||
msg = f"Subagent {name!r}: system_prompt is empty"
|
||||
raise ValueError(msg)
|
||||
|
||||
per_subagent_perm = build_permission_mw(flags=flags, extra_rulesets=[ruleset])
|
||||
flags = dependencies["flags"]
|
||||
user_allowlist = _user_allowlist_for(dependencies, name)
|
||||
subagent_rulesets: list[Ruleset] = [ruleset]
|
||||
if user_allowlist is not None:
|
||||
subagent_rulesets.append(user_allowlist)
|
||||
per_subagent_perm = build_permission_mw(
|
||||
flags=flags, subagent_rulesets=subagent_rulesets
|
||||
)
|
||||
|
||||
prepended: list[Any] = []
|
||||
for slot, mw in (middleware_stack or {}).items():
|
||||
if mw is None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue