From df0bec8f7d33ab71503d4c266dae6836dd74a9d4 Mon Sep 17 00:00:00 2001 From: ymq Date: Mon, 24 Aug 2026 07:20:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=83=A8=E7=BD=B2=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E5=B8=88=20SSH=20=E7=94=A8=E6=9C=BA=E6=9E=84=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E5=AF=86=E9=92=A5=EF=BC=88=E6=AF=8F=E6=9C=BA=E6=9E=84=E4=B8=80?= =?UTF-8?q?=E6=8A=8A=EF=BC=8C=E9=98=B2=E5=8D=95=E7=82=B9=E6=B3=84=E9=9C=B2?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deploy_test/deploy_prod 的 run_shell 执行 ssh/scp 时,之前默认走 ~/.ssh/id_rsa(个人密钥),绕过 work_env 的机构密钥机制。现改为: - capability_ctx 注入 org_id - _inject_org_ssh_key:对 ssh/scp 命令自动注入 -i ~/.ssh/org_keys/{org_id}/id_ed25519 (已显式 -i 不覆盖;非 ssh/scp 命令不注入;机构密钥不存在则回退默认) - 仅部署角色(agent.deploy_test/deploy_prod)生效,不影响 develop 的 git ssh 等 --- pipeline_service/agent_loop.py | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index fce2592..04f5bb1 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -267,6 +267,50 @@ async def _run_shell(command, workdir, timeout=120): return {"rc": -1, "stdout": "", "stderr": str(e)[:500]} +def _inject_org_ssh_key(command: str, org_id: str) -> str: + """给 ssh/scp 命令注入机构独立密钥(每机构一把,防单点泄露)。 + + 部署工程师(deploy_test/deploy_prod)SSH 到目标机时,默认会走 ~/.ssh/id_rsa + (个人密钥),绕过机构密钥机制。这里在 ssh/scp 命令前注入 + `-i ~/.ssh/org_keys/{org_id}/id_ed25519`,让部署用机构密钥连目标机。 + + 规则: + - 命令含 ssh/scp 关键字才处理(避免影响 git 等其他命令) + - 已显式指定 -i 的不重复注入 + - 机构密钥文件不存在则不注入(回退默认密钥,保持原行为) + """ + if not command or not org_id: + return command + # 仅对 ssh/scp 命令注入(宽松匹配:命令以 ssh/scp 开头,或含 ' ssh ' 等) + stripped = command.lstrip() + if not (stripped.startswith("ssh ") or stripped.startswith("ssh\t") or + stripped.startswith("scp ") or stripped.startswith("scp\t")): + return command + if re.search(r'(^|\s)-i\s+\S+', command): + return command # 已显式指定密钥,不覆盖 + from .work_env import ensure_org_key + try: + info = ensure_org_key(org_id) + key_path = info.get("key_path", "") + except Exception: + return command + if not key_path or not os.path.exists(key_path): + return command + # 在 ssh/scp 动词后注入 -i 密钥(保留原有前导空白) + return command[:len(command) - len(command.lstrip())] + \ + _inject_prefix(stripped, f"-i {key_path} ") + + +def _inject_prefix(cmd: str, prefix: str) -> str: + """在 ssh/scp 命令动词后插入 prefix(密钥参数)。""" + parts = cmd.split(None, 1) + if not parts: + return cmd + verb = parts[0] # ssh 或 scp + rest = parts[1] if len(parts) > 1 else "" + return f"{verb} {prefix}{rest}" + + async def _write_code_file(filepath, content): """写代码文件,自动创建父目录。""" try: @@ -1067,6 +1111,12 @@ async def _exec_agent_tool(tool, params, workspace_dir, ctx=None): elif tool == 'run_shell': cmd = p.get('command', '') if not cmd: return 'FAIL: 需要命令' + # 部署角色(deploy_test/deploy_prod)SSH 到目标机时,注入机构独立密钥 + # (每机构一把,防单点泄露;否则默认走 ~/.ssh/id_rsa 个人密钥)。 + _who = (ctx or {}).get('who', '') or '' + _org = (ctx or {}).get('org_id', '') or '' + if _who in ('agent.deploy_test', 'agent.deploy_prod') and _org: + cmd = _inject_org_ssh_key(cmd, _org) r = await _run_shell(cmd, workspace_dir, timeout=120) return f"rc={r['rc']}\nSTDOUT:\n{r['stdout'][:2000]}\nSTDERR:\n{r['stderr'][:1000]}" elif tool == 'git_clone': @@ -1329,6 +1379,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): "who": role, "agent_id": agent_id, "task_id": task_id, + "org_id": org_id or '0', } system = (AGENT_SYSTEM_PROMPT