diff --git a/pipeline_service/agent_loop_v2.py b/pipeline_service/agent_loop_v2.py index ff26e08..b6f2ff2 100644 --- a/pipeline_service/agent_loop_v2.py +++ b/pipeline_service/agent_loop_v2.py @@ -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 动态挂载。 diff --git a/pipeline_service/sdlc_ability.py b/pipeline_service/sdlc_ability.py index 98ed760..19c6a5e 100644 --- a/pipeline_service/sdlc_ability.py +++ b/pipeline_service/sdlc_ability.py @@ -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 = []