From 63818999d9af4175ebdb09b94b78bcc839368d13 Mon Sep 17 00:00:00 2001 From: Variet Agent Date: Thu, 12 Mar 2026 22:03:00 +0900 Subject: [PATCH] =?UTF-8?q?refactor(core):=20MCP=20=EC=97=AD=ED=95=A0?= =?UTF-8?q?=EB=B3=84=20=EC=A0=91=EA=B7=BC=20=EC=A0=9C=EC=96=B4=20+=20async?= =?UTF-8?q?io.Lock=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ROLE_MCP_ACCESS: agent만 MCP 도구 접근, 나머지 역할은 제거 - _settings_lock: settings.json 쓰기~프로세스 시작 직렬화 - agent.md: 쉘 명령 금지, MCP 도구 강제 사용 지시 추가 - config.py: WORKSPACE_BASE_DIR 경로 수정 (Variet-Worker) - run_bot.bat: conda 환경 variet-agent로 변경 - workspaces.json: orphan 정리 + 경로 수정 - known-issues: MCP 접근제어, yolo 자율성, Nyaa 카테고리 이슈 추가 - devlog: 002 entry 및 index 업데이트 --- .agent/references/known-issues.md | 18 ++++++++ config.py | 2 +- core/gemini_caller.py | 69 ++++++++++++++++++++--------- docs/devlog/2026-03-12.md | 3 ++ docs/devlog/entries/20260312-002.md | 24 ++++++++++ prompts/agent.md | 21 ++++++--- run_bot.bat | 2 +- workspaces.json | 53 +--------------------- 8 files changed, 112 insertions(+), 80 deletions(-) create mode 100644 docs/devlog/entries/20260312-002.md diff --git a/.agent/references/known-issues.md b/.agent/references/known-issues.md index c45df39..3820b9f 100644 --- a/.agent/references/known-issues.md +++ b/.agent/references/known-issues.md @@ -63,3 +63,21 @@ - **원인**: Gemini CLI는 `cwd` 기준으로 `.gemini/settings.json`을 탐색. cwd가 다른 워크스페이스면 MCP 설정 못 찾음 - **해결**: `~/.gemini/settings.json` (홈 레벨)에 mcpServers 등록. `_set_thinking_budget`에서 자동 관리 - **주의**: MCP 서버 설정은 반드시 홈 레벨 settings.json에 등록. 프로젝트 레벨은 불충분 + +### [2026-03-12] MCP 역할별 접근 제어 — 모든 역할이 MCP 도구 접근 +- **증상**: coder, reviewer 등 텍스트 전용 역할도 anime/infra MCP 도구에 접근 가능 +- **원인**: `_set_thinking_budget()`이 역할 무관하게 모든 MCP 서버를 settings.json에 등록 +- **해결**: `ROLE_MCP_ACCESS` dict 추가, agent만 MCP 등록, 나머지는 제거. `asyncio.Lock` 추가로 settings.json 레이스 방지 +- **주의**: settings.json은 글로벌 파일이므로, 역할 전환 시 반드시 이전 설정을 정리해야 함 + +### [2026-03-12] Gemini CLI yolo — 에이전트 자율성 위험 +- **증상**: 애니 다운로드 요청 시 에이전트가 음악/만화를 다운로드하고 엉뚱한 폴더 생성 +- **원인**: `--approval-mode yolo`는 MCP 도구 + 쉘 + 파일 조작 모두 무승인 허용. 프롬프트 제한은 강제력 없음 +- **해결 (검토중)**: MCP 대신 Python 도구를 소스코드로 직접 제공하여 Gemini CLI가 읽고 사용하는 방식 검토 +- **주의**: 프롬프트는 "부탁"이지 "강제"가 아님. 안전장치는 코드(도구) 레벨에 구현해야 함 + +### [2026-03-12] Nyaa 검색 — anime 카테고리 미지정 +- **증상**: 애니 검색 시 음악, 만화, 라이트노벨 등 무관한 토렌트가 다운로드됨 +- **원인**: `NyaaClient.search()` 기본 category가 `0_0`(전체). Music, Manga 등 포함 +- **해결 (예정)**: 기본 category를 `1_2`(Anime English) 또는 `1_0`(Anime 전체)로 변경 +- **주의**: 외부 검색 API 사용 시 반드시 카테고리/필터를 명시적으로 지정 diff --git a/config.py b/config.py index 8b329cf..bcbf38e 100644 --- a/config.py +++ b/config.py @@ -47,7 +47,7 @@ VIKUNJA_PROJECT_ID: int = int(os.getenv("VIKUNJA_PROJECT_ID", "7")) # === Workspace === WORKSPACE_BASE_DIR: str = os.getenv( - "WORKSPACE_BASE_DIR", r"c:\Users\Certes\Desktop\VW_Proj" + "WORKSPACE_BASE_DIR", r"c:\Users\Variet-Worker\Desktop\VW_Proj" ) # === qBittorrent === diff --git a/core/gemini_caller.py b/core/gemini_caller.py index 937df3a..33285b9 100644 --- a/core/gemini_caller.py +++ b/core/gemini_caller.py @@ -29,9 +29,23 @@ ROLE_THINKING: dict[str, int] = { } DEFAULT_THINKING = 4096 +# 역할별 MCP 서버 접근 허용 목록 +# agent만 외부 도구 사용 가능, 나머지 역할은 텍스트 전용 +ROLE_MCP_ACCESS: dict[str, list[str]] = { + "agent": ["anime", "infra"], + "coder": [], + "planner": [], + "reviewer": [], + "summarizer": [], + "unified": [], +} + # 동시 호출 제한 (Gemini AI Ultra 120RPM 고려) _semaphore = asyncio.Semaphore(4) +# settings.json 쓰기 경합 방지 (settings 쓰기 ~ 프로세스 시작 구간 직렬화) +_settings_lock = asyncio.Lock() + # Windows에서 PS ExecutionPolicy 우회 _IS_WIN = sys.platform == "win32" @@ -76,8 +90,13 @@ class GeminiCaller: } def _set_thinking_budget(self, role: str): - """역할별 thinkingBudget + MCP 서버 설정을 settings.json에 반영.""" + """역할별 thinkingBudget + MCP 서버 설정을 settings.json에 반영. + + MCP 서버는 ROLE_MCP_ACCESS에 따라 역할별로 필터링됩니다. + agent 역할만 MCP 도구에 접근 가능하고, 나머지 역할은 제거됩니다. + """ budget = ROLE_THINKING.get(role, DEFAULT_THINKING) + allowed_mcp = ROLE_MCP_ACCESS.get(role, []) try: if _SETTINGS_PATH.exists(): settings = json.loads(_SETTINGS_PATH.read_text(encoding="utf-8")) @@ -91,16 +110,22 @@ class GeminiCaller: thinking = default.setdefault("thinkingConfig", {}) thinking["thinkingBudget"] = budget - # MCP 서버 (홈 레벨에 등록 — cwd와 무관하게 항상 사용 가능) + # MCP 서버 — 역할별 접근 제어 mcp_servers = settings.setdefault("mcpServers", {}) - for name, config in self._MCP_SERVERS.items(): - mcp_servers[name] = config + for name, cfg in self._MCP_SERVERS.items(): + if name in allowed_mcp: + mcp_servers[name] = cfg + else: + mcp_servers.pop(name, None) _SETTINGS_PATH.write_text( json.dumps(settings, indent=2, ensure_ascii=False), encoding="utf-8", ) - logger.debug(f"thinkingBudget={budget} for role={role}") + logger.debug( + f"settings.json 업데이트: role={role}, budget={budget}, " + f"mcp={allowed_mcp or 'none'}" + ) except Exception as e: logger.warning(f"settings.json 업데이트 실패 (role={role}): {e}") @@ -132,7 +157,6 @@ class GeminiCaller: async def _call_text(self, role: str, context: str, timeout: int) -> str: """텍스트 전용 호출.""" - self._set_thinking_budget(role) prompt_file = ROLE_PROMPTS_DIR / f"{role}.md" if prompt_file.exists(): system_prompt = prompt_file.read_text(encoding="utf-8") @@ -147,12 +171,15 @@ class GeminiCaller: ) try: - proc = await asyncio.create_subprocess_exec( - *self._build_cmd(), - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) + # Lock: settings.json 쓰기 ~ 프로세스 시작 직렬화 + async with _settings_lock: + self._set_thinking_budget(role) + proc = await asyncio.create_subprocess_exec( + *self._build_cmd(), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) stdout, stderr = await asyncio.wait_for( proc.communicate(input=full_input.encode("utf-8")), timeout=timeout, @@ -207,7 +234,6 @@ class GeminiCaller: self, role: str, context: str, cwd: str, timeout: int, ) -> str: """에이전트 모드 구현.""" - self._set_thinking_budget(role) prompt_file = ROLE_PROMPTS_DIR / f"{role}.md" if prompt_file.exists(): system_prompt = prompt_file.read_text(encoding="utf-8") @@ -241,13 +267,16 @@ class GeminiCaller: ) try: - proc = await asyncio.create_subprocess_exec( - *self._build_cmd(), - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=cwd, # ★ 핵심: 프로젝트 디렉토리에서 실행 - ) + # Lock: settings.json 쓰기 ~ 프로세스 시작 직렬화 + async with _settings_lock: + self._set_thinking_budget(role) + proc = await asyncio.create_subprocess_exec( + *self._build_cmd(), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=cwd, # ★ 핵심: 프로젝트 디렉토리에서 실행 + ) stdout, stderr = await asyncio.wait_for( proc.communicate(input=full_input.encode("utf-8")), timeout=timeout, diff --git a/docs/devlog/2026-03-12.md b/docs/devlog/2026-03-12.md index 76d2641..7c006de 100644 --- a/docs/devlog/2026-03-12.md +++ b/docs/devlog/2026-03-12.md @@ -3,3 +3,6 @@ | # | 시간 | 작업 | 커밋 | 상태 | |---|------|------|------|------| | 001 | 16:45 | MCP 기반 에이전트 아키텍처 재설계 — unified.md 분류기 → Gemini CLI + MCP 자율 에이전트 전환 | `246d2a2` | ✅ | +| 002 | 18:30 | MCP 역할별 접근 제어 + asyncio.Lock 추가, run_bot.bat 환경 수정, 아키텍처 정밀 검토 | - | 🔧 | +| 003 | 19:30 | 봇 실행 테스트 — fastapi 설치, workspace 경로 수정, 에이전트 자율성 문제 발견 | - | 🔧 | +| 004 | 20:00 | 아키텍처 재분석 — MCP vs 구조화 파이프라인 vs Python 도구 직접 제공 방식 비교 검토 | - | 🔧 | diff --git a/docs/devlog/entries/20260312-002.md b/docs/devlog/entries/20260312-002.md new file mode 100644 index 0000000..4a8ed29 --- /dev/null +++ b/docs/devlog/entries/20260312-002.md @@ -0,0 +1,24 @@ +# MCP 역할별 접근 제어 + 아키텍처 재분석 + +- **시간**: 2026-03-12 18:30~21:59 +- **Commit**: (이번 커밋에 포함) +- **Vikunja**: 신규 태스크 생성 예정 + +## 결정 사항 + +### MCP 역할별 접근 제어 (구현 완료) +- `ROLE_MCP_ACCESS` dict로 agent만 MCP 도구 접근 허용 +- `asyncio.Lock`으로 settings.json 쓰기~프로세스 시작 구간 직렬화 +- 나머지 역할(coder, reviewer 등)은 MCP 제거됨 + +### 아키텍처 방향성 (검토중) +- **MCP 방식의 문제**: `--approval-mode yolo`에서 에이전트가 쉘/파일 조작 무제한 → 음악/만화 다운 등 예측 불가 동작 +- **검토 중인 대안**: MCP 대신 Python 도구를 소스코드로 직접 제공 → Gemini CLI가 읽고 이해하여 사용 +- **핵심 원칙**: "프롬프트는 부탁, 코드는 법률" → 안전장치는 도구 코드 레벨에 구현 + +## 미완료 +- [ ] 아키텍처 최종 결정 (MCP → Python 도구 직접 제공 방식 전환) +- [ ] Nyaa 검색 카테고리 `1_2`로 변경 +- [ ] NAS 기존 폴더 매칭 로직 추가 +- [ ] discord_bot.py dead code ~572줄 정리 +- [ ] config.py .env 따옴표 처리 diff --git a/prompts/agent.md b/prompts/agent.md index 6994b69..da1d054 100644 --- a/prompts/agent.md +++ b/prompts/agent.md @@ -10,26 +10,35 @@ - 여러 도구를 **순서대로** 사용해야 할 때도 있습니다. - 도구 호출 결과가 불충분하면 **다른 도구를 시도**하거나 **다른 파라미터**로 재호출하세요. +## ⛔ 절대 금지 + +- **쉘 명령어로 직접 다운로드하지 마세요** (curl, wget, pip install 등) +- **파일을 직접 생성/수정하지 마세요** — MCP 도구만 사용하세요 +- 사용자가 요청하지 않은 작업을 임의로 수행하지 마세요 + ## 사용 가능한 도구 영역 -### 🎬 anime 서버 +### 🎬 anime 서버 — 애니메이션 관련은 반드시 이 도구만 사용 - `anime_search` — 애니 검색 (제목, 자막, 토렌트) -- `anime_download` — 애니 다운로드 (자막+영상) +- `anime_download` — 애니 다운로드 (자막+영상). 한 번에 **하나의 작품**만 다운로드. - `anime_schedule` — 편성표 조회 - `anime_download_status` — qBittorrent 상태 - `anime_nas_list` — NAS 다운로드 목록 -### 🔧 infra 서버 +### 🔧 infra 서버 — Git/태스크 관련은 반드시 이 도구만 사용 - `gitea_commits`, `gitea_prs`, `gitea_issues`, `gitea_branches` — Git 관리 - `vikunja_tasks`, `vikunja_create_task`, `vikunja_complete_task` — 태스크 관리 -### 💻 내장 도구 -- 프로젝트 파일 읽기/쓰기, 쉘 명령 실행 등 Gemini CLI 내장 도구 사용 가능 +## 복수 작품 처리 방법 + +사용자가 "여러 작품 다운로드" 등 복수 작업을 요청하면: +1. 먼저 `anime_nas_list`로 대상 목록을 확인하세요 +2. 각 작품마다 `anime_download`를 **개별 호출**하세요 +3. 진행 상황과 결과를 정리하여 보고하세요 ## 응답 규칙 - **한국어**로 응답하세요. - 도구 실행 결과를 사용자에게 **알기 쉽게 정리**하세요. -- 파일 변경 시 **변경 요약**을 제공하세요. - 에러 발생 시 **원인과 대안**을 안내하세요. - 불필요하게 길지 않게, **핵심만** 전달하세요. diff --git a/run_bot.bat b/run_bot.bat index 13504d9..5791ced 100644 --- a/run_bot.bat +++ b/run_bot.bat @@ -7,7 +7,7 @@ echo Variet Agent 시작 echo ========================================== echo. -C:\ProgramData\miniforge3\envs\agent_chat\python.exe main.py +C:\ProgramData\miniforge3\envs\variet-agent\python.exe main.py echo. echo 봇이 종료되었습니다. 아무 키나 누르면 창을 닫습니다. diff --git a/workspaces.json b/workspaces.json index 8a3bce0..ce4e722 100644 --- a/workspaces.json +++ b/workspaces.json @@ -1,58 +1,7 @@ { - "5608566207": { - "name": "test_1_orphan_20260307", - "path": "c:\\Users\\Certes\\Desktop\\VW_Proj\\test_1_orphan_20260307", - "channel_id": 0, - "git": { - "url": "", - "token": "", - "repo": "", - "branch": "main" - }, - "vikunja": { - "url": "", - "token": "", - "project_id": 0 - }, - "docs_path": "docs/wiki" - }, - "8350378037": { - "name": "test_2_orphan_20260307", - "path": "c:\\Users\\Certes\\Desktop\\VW_Proj\\test_2_orphan_20260307", - "channel_id": 0, - "git": { - "url": "", - "token": "", - "repo": "", - "branch": "main" - }, - "vikunja": { - "url": "", - "token": "", - "project_id": 0 - }, - "docs_path": "docs/wiki" - }, - "1479610776502403186": { - "name": "test_1", - "path": "c:\\Users\\Certes\\Desktop\\VW_Proj\\test_1", - "channel_id": 1479610776502403186, - "git": { - "url": "", - "token": "", - "repo": "", - "branch": "main" - }, - "vikunja": { - "url": "", - "token": "", - "project_id": 0 - }, - "docs_path": "docs/wiki" - }, "1480113683849023661": { "name": "variet-agent", - "path": "c:\\Users\\Certes\\Desktop\\VW_Proj\\variet-agent", + "path": "c:\\Users\\Variet-Worker\\Desktop\\VW_Proj\\variet-agent", "channel_id": 1480113683849023661, "git": { "url": "",