refactor(core): MCP 역할별 접근 제어 + asyncio.Lock 추가

- 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 업데이트
This commit is contained in:
2026-03-12 22:03:00 +09:00
parent 092bd588be
commit 63818999d9
8 changed files with 112 additions and 80 deletions

View File

@@ -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,