diff --git a/docs/superpowers/specs/2026-06-12-co-gahusb-team-bus-design.md b/docs/superpowers/specs/2026-06-12-co-gahusb-team-bus-design.md new file mode 100644 index 0000000..2eccba7 --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-co-gahusb-team-bus-design.md @@ -0,0 +1,127 @@ +# 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 레벨 강제 — 어드바이저리로 충분(세션은 협조적).