8.6 KiB
8.6 KiB
co-gahusb — 세션 간 협업 팀 버스 설계
작성일: 2026-06-12
대상 repo: web-backend (서버) + web-ui/web-ai (클라이언트 배선)
목적: 독립 실행되는 4개 Claude Code 세션(FE/BE/AI/Producer)이 역할을 갖고 비동기로 소통·협업하되, 공유 DB/리소스는 동시 쓰기를 방지한다.
배경
web-ui / web-backend / web-ai 세션은 각각 독립 프로세스라 서로의 컨텍스트를 못 본다. 협업하려면 세 곳(서로 다른 머신 포함)에서 닿는 공유 메시지 버스가 필요하다. 사용자가 방식 B(독립 MCP 서버)를 선택했고, 민감한 공유 영역의 동시 쓰기 분리를 핵심 요구로 명시했다.
결정 사항 (브레인스토밍 확정)
- 호스팅: 신규 독립 컨테이너
co-gahusb, NAS, 포트 18920(18900 agent-office 옆, 미사용 확인). - 전송/인증: HTTP streamable MCP + 정적 Bearer 키(reference_webai_auth_pattern 재사용). nginx
/api/co/→co-gahusb:18920,Authorizationforward. - 백엔드: Redis(기존 공유 컨테이너
redis://redis:6379). 전 연산 원자적 → SQLite multi-writer 함정(reference_sqlite_concurrency) 회피. - 동시쓰기 분리: 소유권 파티션 + 어드바이저리 락.
- 역할: web-ui=FE, web-backend=BE, web-ai=AI, 이 세션=Producer.
- 수신: 각 세션 /loop 폴링(
read_inbox+list_tasks).
아키텍처
[FE 세션 web-ui] [BE 세션 web-backend] [AI 세션 web-ai(다른 머신)] [Producer 세션]
\ | / /
\ | / /
──────── .mcp.json HTTP + Bearer ───────────────────────────────
│
nginx /api/co/ (Authorization forward)
│
co-gahusb:18920 (FastMCP streamable-http)
│
Redis (원자적 연산)
서버 구현: Python mcp SDK(FastMCP) + streamable-http transport(모든 lab이 FastAPI/Python 스택과 일관). 단일 책임 모듈로 분리:
app/server.py— FastMCP 인스턴스 + 툴 등록 + ASGI 앱(streamable-http) + Bearer 인증 미들웨어app/store.py— Redis 데이터 액세스 레이어(메시지/작업/락), 전 함수 원자적app/locks.py— 락 Lua 스크립트(소유자 확인 후 release/heartbeat)app/models.py— 입출력 dataclass/스키마app/config.py— env(REDIS_URL, CO_BUS_KEY, 포트)
MCP 툴 표면 (MVP — YAGNI)
| 분류 | 툴 | 시그니처 → 반환 |
|---|---|---|
| 메시지 | post_message |
(from_role, to_role, body, thread_id?) → {message_id} |
| 메시지 | read_inbox |
(role, after_id?, mark_read?=false) → {messages:[{id, from_role, body, thread_id, ts}], cursor} |
| 작업 | create_task |
(title, assignee_role, detail?, created_by) → {task_id} |
| 작업 | claim_task |
(task_id, role) → {ok, task} (이미 claim 시 {ok:false, held_by}) |
| 작업 | update_task |
(task_id, status, role, note?) → {ok, task} (status ∈ open/in_progress/blocked/done) |
| 작업 | list_tasks |
(status?, assignee_role?) → {tasks:[...]} |
| 락 | acquire_lock |
(resource, role, ttl_sec=300) → {acquired, held_by?, ttl_remaining?} |
| 락 | release_lock |
(resource, role) → {released} (소유자 아니면 {released:false}) |
| 락 | heartbeat_lock |
(resource, role, ttl_sec=300) → {renewed} (소유자만) |
| 락 | list_locks |
() → {locks:[{resource, held_by, ttl_remaining}]} |
| 가시성 | team_log |
(after_id?) → {events:[...], cursor} (최근 활동 피드) |
Redis 데이터 모델 (전부 원자적)
- 메시지:
co:inbox:{role}= Redis Stream.post_message=XADD,read_inbox=XREAD(after_id커서, 비파괴).mark_read는co:read:{role}키에 마지막 id 저장. - 작업:
co:task:{id}Hash(title/assignee/status/detail/created_by/ts),co:tasksSet(id 목록),INCR co:taskseq로 id.claim_task/update_task는 Lua 스크립트로 read-modify-write 원자화(중복 claim/경합 방지). - 락: 획득 =
SET co:lock:{resource} {role} NX EX {ttl}(원자적).release_lock/heartbeat_lock= Lua로GET소유자 일치 확인 후DEL/EXPIRE(check-and-act 원자화 → 남의 락 조작 불가). - 활동로그:
co:log= 캡트 Stream(XADD ... MAXLEN ~ 500). 메시지·작업·락 이벤트 기록 → Producer 오버사이트.
동시 쓰기 분리 (핵심 요구)
1차 — 정적 소유권 파티션 (락 불필요한 자연 분리):
web-ui→ FE만,web-backend→ BE만,web-ai→ AI만 쓰기. 각 세션은 자기 repo만 편집 → git 충돌 원천 차단.
2차 — 교차 리소스 어드바이저리 락 (여러 역할이 건드릴 수 있는 민감 영역만):
- 예약 resource 명:
nas-deploy,stock-db-schema,lotto-db-schema,memory-mirror(web-ui↔web-ai 미러),nginx-conf,compose. - 규약: 위 리소스 변경 전
acquire_lock필수. 점유 중이면{acquired:false, held_by, ttl_remaining}→ 대기. TTL 자동 해제로 세션 사망 시 데드락 방지, 긴 작업은heartbeat_lock갱신. - 어드바이저리(협조적): 버스는 FS를 강제 잠그지 않음 → 각 세션 CLAUDE.md에 "공유 리소스 = 락 먼저" 규약 명문화로 강제.
클라이언트 배선
- 각 repo
.mcp.json:(키는 커밋 금지 — 각 머신 env/로컬에서 주입.{ "mcpServers": { "co-gahusb": { "type": "http", "url": "https://gahusb.synology.me/api/co/mcp", "headers": { "Authorization": "Bearer ${CO_BUS_KEY}" } } } }.mcp.json엔 placeholder, 실제 키는.env/환경변수.) - 각 repo CLAUDE.md에 역할 블록 추가: "너는 역할 X / 모든 co-gahusb 툴에 role=X / 공유 리소스 변경 전 acquire_lock /
/loop로 inbox·tasks 폴링". - web-ai는 다른 머신 → 해당 머신에서
.mcp.json적용(스펙에 절차 명시).
인프라 등재 (신규 컨테이너 추가 의무 위치 — reference_nas_url_routing, reference_deploy_nas_services_whitelist)
docker-compose.yml—co-gahusb서비스(build,REDIS_URL,depends_on: redis,CO_BUS_KEYenv,${RUNTIME_PATH}볼륨 불요(상태는 Redis)).- nginx
default.conf— publiclocation /api/co/추가(7번째 등재 규칙;/api/internal/불필요). - deploy 스크립트 SERVICES 화이트리스트에
co-gahusb등재. ${RUNTIME_PATH}절대경로 — 본 서비스는 영속 볼륨 없음(Redis 백엔드)이라 코드 디렉토리만.- frontend
depends_on— 불필요(백엔드 전용 서비스). .env—CO_BUS_KEY추가(커밋 금지).
에러 / 엣지 처리
- 인증 실패 → 401, 1회만 ERROR 로그 후 조용(reference_webai_auth_pattern).
- 락 획득 실패 → 예외 아닌
{acquired:false, held_by, ttl_remaining}정상 반환. - 만료 락 → Redis TTL 자동 소멸(별도 GC 불필요).
- 알 수 없는 role/resource → 명시적 에러 메시지.
- Redis 연결 실패 → 503 + 명확한 메시지.
테스트 (TDD, pytest + fakeredis)
- 락: 두 역할 같은 resource 획득 → 2번째 거부 / TTL 만료 후 획득 / 소유자 아닌 release·heartbeat 거부 / heartbeat 갱신 후 ttl 증가.
- 메시지: XADD 순서대로
after_id커서 읽기 / mark_read 후 재읽기 시 제외 / 다른 role 우편함 격리. - 작업: create→claim(중복 claim 거부)→update status 전이 / list 필터.
- 인증: 키 일치 통과 / 불일치 401.
- team_log: 이벤트 기록 + MAXLEN 캡.
구현 순서 (phase)
- 스캐폴드: 디렉토리/Dockerfile/requirements/config (기존 lab 구조 미러)
store.py+locks.py(TDD, fakeredis) — 락 → 메시지 → 작업 → team_logserver.py— FastMCP 툴 등록 + Bearer 인증 + ASGI- 인프라 등재 6위치 (compose/nginx/deploy/env)
- 클라이언트 배선: web-ui·web-backend
.mcp.json+ CLAUDE.md 역할 블록 (web-ai는 절차 문서화) - 배포(Gitea push → webhook) + 스모크 테스트(헬스/인증/락 경합)
비범위 (YAGNI)
- 실시간 push(텔레그램) — 후속. 우선 /loop 폴링.
- SQLite 감사로그 — Redis 캡트 스트림으로 충분.
- 웹 대시보드 — agent-office 오버사이트와 추후 통합 여지.
- 락의 FS 레벨 강제 — 어드바이저리로 충분(세션은 협조적).