# 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_read`는 `co: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_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`: ```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.yml` — `co-gahusb` 서비스(build, `REDIS_URL`, `depends_on: redis`, `CO_BUS_KEY` env, `${RUNTIME_PATH}` 볼륨 불요(상태는 Redis)). 2. nginx `default.conf` — **public `location /api/co/`** 추가(7번째 등재 규칙; `/api/internal/` 불필요). 3. deploy 스크립트 SERVICES 화이트리스트에 `co-gahusb` 등재. 4. `${RUNTIME_PATH}` 절대경로 — 본 서비스는 영속 볼륨 없음(Redis 백엔드)이라 코드 디렉토리만. 5. frontend `depends_on` — 불필요(백엔드 전용 서비스). 6. `.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) 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 레벨 강제 — 어드바이저리로 충분(세션은 협조적).