fix(isolation): 通用助手会话泄漏产线项目工具——5层修复
根因: 1. tool_registry 全局单例跨会话污染:产线会话注册的工具(如 list_projects) 永久残留,后续通用会话把全部工具塞给 LLM 2. generic 会话 pipeline_id 空被无条件回退 DEFAULT_ABILITY_ID(sdlc_general), _execute_ability_tool 拿到产线能力包直接执行 3. _h_list_projects 对 space='general' 不过滤,列出所有产线全部项目 4. GENERAL_TOOLS 本身含项目管理工具,通用会话无条件拿到 修复(纵深防御,schema 层+执行层双保险): - agent_loop_v2: ToolRegistry 改每 executor 独立实例(禁全局单例) - agent_loop_v2: generic 会话跳过默认产线回退 - agent_loop_v2: _execute_ability_tool 对 generic 直接返回 None - agent_loop_v2: _dispatch_sdlc_tool 对 generic 拒绝项目类工具(含别名归一) - agent_loop_v2: generic 记忆只读 global 域、技能只检索 global 层 - sdlc_ability: _h_list_projects 对 general 空间拒绝列项目 配套:pipeline-core load_agent_config generic 时剔除 category=project 工具
This commit is contained in:
parent
e88e0bfc37
commit
b99fb4cc13
@ -46,6 +46,15 @@ _PROJECT_TOOL_ALIASES = {
|
||||
|
||||
from .workspace import WORKSPACE_BASE, GENERAL_SPACE
|
||||
|
||||
# 产线/项目管理类工具(category=='project')。产线隔离铁律:纯通用会话
|
||||
# (无产线插件)绝不挂载——否则通用助手能看到并操作其他产线的项目
|
||||
# (2026-09-05 修复)。过滤同时作用于:① schema(LLM 看不见)
|
||||
# ② 执行层(看见也拒绝),纵深防御。
|
||||
_PROJECT_TOOL_NAMES = {
|
||||
"switch_project", "create_project", "project_info", "list_my_projects",
|
||||
"pause_project", "resume_project", "delete_project",
|
||||
}
|
||||
|
||||
|
||||
# ── 默认工具定义(在 pipeline-core 未加载时使用)──
|
||||
|
||||
@ -402,15 +411,17 @@ class AgentExecutor:
|
||||
pass
|
||||
|
||||
# 2. Tool Registry
|
||||
# 产线隔离(2026-09-05):必须每个 executor 独立实例,禁止全局单例。
|
||||
# 全局单例会让先启动的产线会话注册的工具(如 sdlc 的 list_projects)
|
||||
# 永久残留,之后通用会话把全部工具塞给 LLM → 通用助手看到并调用
|
||||
# 其他产线的工具。config.tools 已由 load_agent_config 按会话裁剪,
|
||||
# 这里只注册本会话自己的工具。
|
||||
try:
|
||||
from pipeline_core.tool_registry import get_tool_registry
|
||||
self._tool_registry = get_tool_registry()
|
||||
# 把 config.tools 注册进 registry(registry 是全局单例,可能为空)
|
||||
if self._tool_registry and self.config.tools:
|
||||
existing = set(self._tool_registry.get_tool_names())
|
||||
from pipeline_core.tool_registry import ToolRegistry
|
||||
self._tool_registry = ToolRegistry()
|
||||
if self.config.tools:
|
||||
for t in self.config.tools:
|
||||
if t.name not in existing:
|
||||
self._tool_registry.register(t)
|
||||
self._tool_registry.register(t)
|
||||
except ImportError:
|
||||
self._tool_registry = None
|
||||
|
||||
@ -433,7 +444,10 @@ class AgentExecutor:
|
||||
|
||||
# 5. pipeline_id 为空时 fallback 到默认产线(保证 slash/ability/skill 统一按产线挂载)
|
||||
# 入口已指定 default_pipeline_id 时优先用它(投标/开发产线独立入口),否则用引擎默认。
|
||||
if not self.pipeline_id:
|
||||
# 产线隔离(2026-09-05):纯通用会话(generic)绝不回退默认产线——
|
||||
# 回退会让 _execute_ability_tool 拿到 sdlc_general 能力包,通用助手
|
||||
# 就能调用 list_projects 等产线工具。
|
||||
if not self.pipeline_id and not self.generic:
|
||||
try:
|
||||
from pipeline_core import DEFAULT_ABILITY_ID
|
||||
self.pipeline_id = DEFAULT_ABILITY_ID
|
||||
@ -497,24 +511,32 @@ class AgentExecutor:
|
||||
prompt += f"\n当前项目: {proj_name}\n" if proj_name else ""
|
||||
|
||||
# 注入记忆(分域:通用 + 产线 + 项目 叠加)
|
||||
# 产线隔离(2026-09-05):纯通用会话只读 global 通用记忆,绝不带
|
||||
# 「产线+项目」叠加——否则通用助手会看到产线专属记忆。
|
||||
if self.config.memory.enabled and self._memory_store:
|
||||
mem_block = await self._memory_store.build_prompt_block(
|
||||
max_entries=15, scope="pipeline", scope_id=self.pipeline_id)
|
||||
if self.project_id:
|
||||
proj_block = await self._memory_store.build_prompt_block(
|
||||
max_entries=5, scope="project", scope_id=self.project_id)
|
||||
if proj_block:
|
||||
mem_block = (mem_block + "\n" + proj_block) if mem_block else proj_block
|
||||
if self.generic:
|
||||
mem_block = await self._memory_store.build_prompt_block(
|
||||
max_entries=15, scope="global")
|
||||
else:
|
||||
mem_block = await self._memory_store.build_prompt_block(
|
||||
max_entries=15, scope="pipeline", scope_id=self.pipeline_id)
|
||||
if self.project_id:
|
||||
proj_block = await self._memory_store.build_prompt_block(
|
||||
max_entries=5, scope="project", scope_id=self.project_id)
|
||||
if proj_block:
|
||||
mem_block = (mem_block + "\n" + proj_block) if mem_block else proj_block
|
||||
if mem_block:
|
||||
prompt += f"\n\n## 持久记忆\n{mem_block}"
|
||||
|
||||
# 注入技能(六级隔离:global→org→pipeline→role→project→user)
|
||||
if self.config.skills.enabled and self._skill_loader:
|
||||
pipeline_id = self.pipeline_id if self.config.skills.enable_pipeline else ""
|
||||
role = self.role if self.config.skills.enable_role else ""
|
||||
project_id = self.project_id if self.config.skills.enable_project else ""
|
||||
org_id = self.org_id if self.config.skills.enable_org else ""
|
||||
user_id = self.user_id if self.config.skills.enable_user else ""
|
||||
# 产线隔离(2026-09-05):纯通用会话只检索 global 技能,
|
||||
# 不挂产线/角色/项目/机构技能。
|
||||
pipeline_id = "" if self.generic else (self.pipeline_id if self.config.skills.enable_pipeline else "")
|
||||
role = "" if self.generic else (self.role if self.config.skills.enable_role else "")
|
||||
project_id = "" if self.generic else (self.project_id if self.config.skills.enable_project else "")
|
||||
org_id = "" if self.generic else (self.org_id if self.config.skills.enable_org else "")
|
||||
user_id = "" if self.generic else (self.user_id if self.config.skills.enable_user else "")
|
||||
# 技能检索必须 LLM 做(语义匹配,非关键词硬编码)
|
||||
catalog = self._skill_loader.get_skill_catalog(
|
||||
pipeline_id=pipeline_id, role=role, project_id=project_id,
|
||||
@ -745,6 +767,9 @@ class AgentExecutor:
|
||||
|
||||
async def _execute_ability_tool(self, tool_name: str, params: dict) -> Optional[str]:
|
||||
"""产线能力包工具:按 pipeline_id 从 PipelineAbility 注册表取 handler 执行。"""
|
||||
# 产线隔离(2026-09-05):纯通用会话不挂任何产线能力包,执行层直接拒绝。
|
||||
if self.generic:
|
||||
return None
|
||||
try:
|
||||
from pipeline_core import get_ability, DEFAULT_ABILITY_ID
|
||||
|
||||
@ -778,6 +803,13 @@ class AgentExecutor:
|
||||
p = params or {}
|
||||
pid = self.project_id
|
||||
|
||||
# 产线隔离(2026-09-05):纯通用会话拒绝一切项目管理工具——此时
|
||||
# tool_name 已经过 _PROJECT_TOOL_ALIASES 别名归一,别名变体同样被拦。
|
||||
# schema 层(load_agent_config 剔除)+ 执行层(这里)双保险。
|
||||
if self.generic and tool_name in _PROJECT_TOOL_NAMES:
|
||||
return ("FAIL: 当前是通用助手会话(未挂产线插件),不提供项目管理能力,"
|
||||
"无法查看或操作任何产线的项目。")
|
||||
|
||||
# 通用内建工具映射(core 内核,产线无关)。
|
||||
# 产线专属工具(create_task/diagnose_project 等)已迁移到 sdlc_ability 能力包,
|
||||
# 由 _execute_ability_tool 按 pipeline_id 动态挂载。
|
||||
|
||||
@ -1155,14 +1155,16 @@ async def _h_list_projects(sor, p, ctx):
|
||||
if _u:
|
||||
org_id = getattr(_u[0], "orgid", "") or ""
|
||||
status = (p.get("status", "") or "").strip()
|
||||
# 产线隔离(强制):真实产线会话(space≠general)只列本产线项目——
|
||||
# 投标/商机产线不串出开发产线的项目。通用助手会话不过滤。
|
||||
pipeline_filter = None
|
||||
# 产线隔离(强制,2026-09-05 收紧):真实产线会话(space≠general)只列本产线项目——
|
||||
# 投标/商机产线不串出开发产线的项目。
|
||||
# space=='general' 只会出现在纯通用会话(无产线插件),此时绝不列任何项目
|
||||
# (通用助手不得看到/操作其他产线的项目);上游 _execute_ability_tool 对
|
||||
# generic 会话已直接拒绝,这里是执行层兜底。
|
||||
space = ctx.get("space", "") or ""
|
||||
if space and space != "general":
|
||||
pipeline_filter = space
|
||||
if not space or space == "general":
|
||||
return "FAIL: 当前会话无产线上下文,不提供项目列表。"
|
||||
lst = await list_projects(org_id=org_id or None, status=status or None,
|
||||
pipeline_id=pipeline_filter)
|
||||
pipeline_id=space)
|
||||
if not lst:
|
||||
return "暂无项目"
|
||||
lines = []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user