Files
web-page-backend/docs/superpowers/specs/2026-06-12-co-gahusb-team-bus-design.md
2026-06-12 01:26:11 +09:00

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, Authorization forward.
  • 백엔드: 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_readco:read:{role} 키에 마지막 id 저장.
  • 작업: co:task:{id} Hash(title/assignee/status/detail/created_by/ts), co:tasks Set(id 목록), INCR co:taskseq로 id. claim_task/update_taskLua 스크립트로 read-modify-write 원자화(중복 claim/경합 방지).
  • : 획득 = SET co:lock:{resource} {role} NX EX {ttl}(원자적). release_lock/heartbeat_lock = LuaGET 소유자 일치 확인 후 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:
    { "mcpServers": { "co-gahusb": {
        "type": "http",
        "url": "https://gahusb.synology.me/api/co/mcp",
        "headers": { "Authorization": "Bearer ${CO_BUS_KEY}" } } } }
    
    (키는 커밋 금지 — 각 머신 env/로컬에서 주입. .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)

  1. docker-compose.ymlco-gahusb 서비스(build, REDIS_URL, depends_on: redis, CO_BUS_KEY env, ${RUNTIME_PATH} 볼륨 불요(상태는 Redis)).
  2. nginx default.confpublic location /api/co/ 추가(7번째 등재 규칙; /api/internal/ 불필요).
  3. deploy 스크립트 SERVICES 화이트리스트에 co-gahusb 등재.
  4. ${RUNTIME_PATH} 절대경로 — 본 서비스는 영속 볼륨 없음(Redis 백엔드)이라 코드 디렉토리만.
  5. frontend depends_on — 불필요(백엔드 전용 서비스).
  6. .envCO_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)

  1. 스캐폴드: 디렉토리/Dockerfile/requirements/config (기존 lab 구조 미러)
  2. store.py + locks.py (TDD, fakeredis) — 락 → 메시지 → 작업 → team_log
  3. server.py — FastMCP 툴 등록 + Bearer 인증 + ASGI
  4. 인프라 등재 6위치 (compose/nginx/deploy/env)
  5. 클라이언트 배선: web-ui·web-backend .mcp.json + CLAUDE.md 역할 블록 (web-ai는 절차 문서화)
  6. 배포(Gitea push → webhook) + 스모크 테스트(헬스/인증/락 경합)

비범위 (YAGNI)

  • 실시간 push(텔레그램) — 후속. 우선 /loop 폴링.
  • SQLite 감사로그 — Redis 캡트 스트림으로 충분.
  • 웹 대시보드 — agent-office 오버사이트와 추후 통합 여지.
  • 락의 FS 레벨 강제 — 어드바이저리로 충분(세션은 협조적).