멀티플레이어 — 신원, 멤버십, 프레즌스
Control Center는 운영자 한 명만으로도 동작하지만, 그 바탕에 깔린 모델은 처음부터 다중 행위자를 전제로 합니다. 사람과 에이전트는 같은 스페이스에서 메시지를 주고받고, 같은 티켓을 편집하며, 같은 명단에 나타납니다. 이것을 안전하고 이해 가능하게 만드는 일은 세 가지 결정으로 귀결됩니다: 누가 행위자로 세어지는가, 무엇이 접근을 통제하는가, 어떤 상태를 영속적으로 공유하고 어떤 상태를 실시간으로 방송할 뿐인가.
주체 — 사람과 에이전트는 동등한 행위자
섹션 제목: “주체 — 사람과 에이전트는 동등한 행위자”“누가 이것을 했는가”를 기록하는 모든 것 — 메시지, 티켓, 리뷰, 플랜, 실행 로그,
감사 항목 — 은 주체를 거쳐 해석됩니다. 주체는 UserPrincipal과
AgentPrincipal을 묶은 봉인된 유니온이며, 간결한 와이어 형식
(user:<id> / agent:<id>)을 갖습니다. 귀속은 N명의 사람(또는 N개의
에이전트)을 하나의 센티널 계정으로 뭉개지 않으므로, 에이전트가 남긴 리뷰와
사람이 남긴 리뷰는 그것이 드러나는 모든 곳에서 구분됩니다.
주체가 신원의 단위이므로, 격리의 단위이기도 합니다:
- 속도 제한은 세션이 아니라 주체 단위입니다. 원격 도구 호출에 걸린 폭주 방지 장치는 연결이 아니라 사용자 단위로 예산을 배정합니다: 한 멤버의 모든 기기가 하나의 공유 슬라이딩 윈도에서 한도를 끌어다 쓰므로(분당 120회 호출, 그중 최대 30회만 변경 가능), 기기를 세 대 열어도 누구의 허용량이 세 배가 되지 않고, 시끄러운 멤버 한 명이 다른 멤버의 몫을 빼앗지 못합니다.
- 되돌리기도 주체 단위입니다. 각 클라이언트의 작업 저널은 자신의 주체가 수행한 변경만 기록합니다. 자기 티켓 편집은 ⌘Z로 되돌릴 수 있지만, 동료의 편집은 되돌릴 수 없습니다 — 그것을 보는 시점에는 영속 레인이 이미 해결을 마쳤기 때문입니다(아래 참고).
접근 경계는 멤버십이며 페어링 키가 아닙니다
섹션 제목: “접근 경계는 멤버십이며 페어링 키가 아닙니다”두 개의 서로 다른 자격 증명이 두 개의 서로 다른 질문에 답합니다. 페어링 키(원격 제어와 모바일 참고)는 _기기_를 서버에 인증합니다. 멤버십은 그 기기의 사용자가 워크스페이스 안에서 행동하도록 권한을 부여합니다. 사용자가 어떤 워크스페이스에도 멤버십이 없는 페어링된 기기는 그 워크스페이스를 읽지도, 변경하지도 못합니다.
멤버십은 (워크스페이스, 사용자)마다 한 행이며, 그 워크스페이스 자체의 데이터베이스 파일에 저장됩니다 — 그래서 “이 워크스페이스에는 누가 있는가?“는 평범한 범위 지정 읽기이고, 반대 질문인 “나는 어느 워크스페이스에 속해 있는가?“는 필연적으로 여러 파일에 걸치며, 허용된 몇 안 되는 교차 워크스페이스 팬아웃 중 하나입니다. 워크스페이스 선택기가 아직 어떤 워크스페이스도 고르기 전에 실행하는 것이 바로 이것입니다.
각 행은 다섯 단계로 서열이 매겨진 사다리에서 역할 하나를 갖습니다:
| 역할 | 허용되는 작업 |
|---|---|
owner |
전체 제어, 워크스페이스 삭제 포함; 워크스페이스마다 정확히 하나 |
admin |
멤버, 초대, 설정, 파괴적 작업 관리 |
member |
일상적인 읽기·쓰기: 메시지, 티켓, 에이전트 실행 |
viewer |
워크스페이스 전반에서 읽기 전용; 비밀 제외 글롭이 코드가 있는 화면에서 일치하는 경로를 숨깁니다 |
guest |
뷰어와 같은 읽기 전용 하한, 사다리에서 가장 낮은 서열 |
모든 접근 결정은 “호출자의 역할이 최소 X 이상인가”로 환원됩니다. 하한은 작업이
명시적으로 선언하지 않는 한 작업의 종류에서 도출됩니다: 읽기에는 guest,
변경에는 member, 파괴적 작업에는 admin이 필요합니다. 아래의 저장소별
그랜트를 넘어서는 객체별 ACL은 의도적으로 범위 밖입니다.
초대는 설정 → 워크스페이스 → 멤버에서 멤버십을 부여하는 방법입니다.
관리자가 일회용 코드를(링크 또는 QR로) 발급하며, 기본적으로 7일 후 만료됩니다.
서버는 코드의 SHA-256 해시만 저장하고 코드 자체는 정확히 한 번만 표시됩니다.
코드를 사용하면 사용자가 JIT로 프로비저닝되고, 초대의 역할로 멤버십이
기록되며(owner는 절대 아닙니다 — 소유권은 명시적으로 이전될 뿐, 초대로 부여되지
않습니다), 기기 페어링으로 넘어갑니다. 페어링 후 발급되는 기기 자격 증명 자체도
30일로 기간이 제한되므로, 페어링이 영구 키가 되지는 않습니다.
저장소 그랜트는 멤버십 아래에 자리합니다. 서버가 전체 체크아웃을
보관하므로, 워크스페이스 멤버십만으로 forge보다 더 큰 권한을 갖게 해서는 안
됩니다: 파일, 디프, PR 리뷰, 코드 검색 등 코드가 있는 모든 화면은 _해당
저장소_에 대한 멤버의 그랜트를 확인합니다. 그랜트는 (워크스페이스, 사용자,
저장소)마다 존재하며 none → read → review → write 순으로 서열이
매겨지고, 여기서 write는 저장소에 쓰는 에이전트 실행을 구동하는 것을
뜻합니다. 행이 없으면 none이며, 소유자와 관리자는 연결된 모든 저장소에 대해
암묵적으로 write를 가지므로 행을 전혀 갖지 않습니다.
협업 상태의 두 레인
섹션 제목: “협업 상태의 두 레인”Control Center는 공유 상태를 서로 반대의 영속성 규칙을 가진 두 레인으로 나눕니다. 두 종류의 상태는 서로 반대 방식으로 잘못되기 때문입니다.
일시적인 프레즌스 레인
섹션 제목: “일시적인 프레즌스 레인”프레즌스 — 누가 온라인인지, 어디에 있는지, 무엇을 입력하는지, 에이전트가 지금
무엇을 하는지 — 는 한순간이라도 낡으면 가치가 없습니다. 이것을 영속화하면
재접속할 때마다 “데이터베이스에는 샘이 이 티켓을 편집 중이라고 나온다”는 말과
현실을 대조해야 할 것입니다. 그래서 프레즌스는 데이터베이스에 절대 기록되지
않습니다. 대신 서버의 메모리 내 프레즌스 허브에 존재합니다: 클라이언트는
presence.update로 발행하고, 피어는 presence.watch로 구독하며, 항목은
하트비트를 세 번 놓치면 자동으로 만료됩니다(하트비트는 10초마다 실행되고, 허브는
5초마다 정리합니다). 대조할 것은 없고, 그저 만료되도록 두면 됩니다.
이 레인을 신뢰할 수 있게 유지하는 두 가지 속성이 있습니다:
- 신원은 세션에서 오며, 클라이언트 인자에서는 절대 오지 않습니다. 서버가
연결의 인증된 기기를 사용자 id로 매핑하므로, 클라이언트는 자기 프레즌스만
갱신할 수 있고, 누구도 다른 참여자의 상태나 위치를 위조할 수 없습니다.
명시적인
offline갱신(연결 해제 또는 보이지 않기 토글)은 항목을 즉시 제거합니다. - 명단은 서버 허브를 거칩니다. 피어는 프레즌스를 직접 주고받지 않으므로, 모든 참여자가 토폴로지와 무관하게 같은 명단을 봅니다. 에이전트에는 클라이언트가 없으므로 서버가 실시간 실행 상태에서 에이전트의 항목을 _합성_합니다 — 생각 중, 실행 중, 승인 게이트에서 차단됨, 완료 — 이때 대표 실행 하나가 에이전트의 상태와 위치를 결정하므로 동시 실행이 명단을 요동치게 하지 않습니다.
팬아웃은 소비자별로 계층화됩니다: 데스크톱과 웹은 전체 계층(100ms마다 최대 한 번의 명단 방출)을, 휴대폰은 요약 계층(500ms마다 한 번)을 타므로, 커서 주기의 갱신이 PWA를 녹여 버릴 수 없습니다.
참여자의 위치 — 현재 어디에 있는지, 아티팩트 단위까지 — 는 봉인된
유니온입니다: Space, File(저장소 + 경로 + 선택적 줄), Pr, Ticket,
PlanNode. 모든 위치에 이동할 곳이 있는 것은 아닙니다: 파일 뷰어 라우트가
없으므로 File 위치는 목적지가 아니라 레이블로 렌더링됩니다. 이 레인은
소프트 클레임(“샘이 이 티켓을 편집 중”, “에이전트가 이 워크트리를 점유
중”)도 실어 나릅니다. 이는 잠금이 아니라 충돌 _가시성_입니다. 알 수 없는 위치
종류는 null로 파싱되므로, 구버전 클라이언트는 이해하지 못하는 것을 그냥
렌더링하지 않습니다.
영속적인 공유 레인
섹션 제목: “영속적인 공유 레인”반드시 살아남아야 하는 기록 — 티켓, 스페이스 공유 Notes 문서, 메시지 — 은 반대 경로를 탑니다: 권위가 있고, 정렬되어 있으며, 영속적입니다. 동시 편집은 서버 수신 순서 기준 마지막 작성자 승리로 해결되며, 클라이언트 시계는 절대 기준이 되지 않습니다. 해결 단위는 의도적으로 잘게 나뉩니다: 티켓 편집은 열 단위이므로 같은 티켓의 서로 다른 필드를 편집하는 두 사람 모두 반영되고 어느 쪽도 상대를 덮어쓰지 않습니다(워크플로 전환은 대신 낙관적 잠금을 유지합니다). 스페이스 Notes 문서는 열이 하나이므로 문서 전체가 LWW이고, 프레즌스 레인의 소프트 클레임이 동시 편집을 잠그는 대신 눈에 보이게 만듭니다. 이것은 의도적으로 CRDT가 아닙니다. 증명 책임은 CRDT에 있고, 수신 순서 LWW와 가시적 클레임이면 실제 협업 패턴을 충분히 감당합니다.
“마지막 작성자”는 클라이언트 시계가 아니라 서버 수신 순서를 뜻합니다 — 시계가 어긋난 노트북이 이미 진 편집을 이길 수는 없습니다. 영속 레인 뒤에는 동기화 피드가 있습니다: SQLite 트리거가 모든 변경을 쓰기 트랜잭션 내부에서 워크스페이스별 변경 로그에 덧붙이고, 변경 자체와 같은 트랜잭션에서 워크스페이스별 단조 증가 시퀀스 번호를 할당합니다. 서버 서비스가 그 로그를 순서대로 따라가며 버전이 붙은 델타 프레임으로 만듭니다. 클라이언트는 전체 스냅샷에서 델타로 나아갑니다. 시퀀스에 구멍이 생기면 그 스토어는 프레임을 잘못 적용하는 대신 스냅샷 모드로 되돌아갑니다.
팔로우, 스티어, 인수, 반환
섹션 제목: “팔로우, 스티어, 인수, 반환”프레즌스 레인은 단순한 명단이 아니라 협업 동사를 구동합니다:
- 팔로우 모드. 참여자를 클릭하면 클라이언트가 그 참여자의 위치를 따라 함께 이동합니다. 팔로우는 “직접 행동할 때까지”입니다: 스스로 이동하는 순간 팔로우가 풀립니다. 다른 참여자가 당신이 있는 스페이스를 스포트라이트(발표)할 때는 닫을 수 있는 배너로 알려 줍니다.
- 스티어. 실행 중인 에이전트에게 보낸 메시지는 스티어링으로 대기열에 들어가고, 에이전트는 다음 턴을 재개하기 전에 그것을 다시 읽습니다. 따라서 실행을 죽이지 않고도 비행 중에 방향을 살짝 틀 수 있습니다.
- 인수. 대화의 제어권을 가져오면 모든 활성 실행이 다음의 깨끗한 턴 경계에서 일시 중지됩니다(안전한 경계가 없는 Claude Code 실행은 중지됩니다). 그 앞에는 턴마다 남기는 git 스냅샷이 있어 인수를 되돌릴 수 있습니다. 영속 마커가 스페이스로 들어오는 새 디스패치를 막고 서버 재시작 후에도 남습니다 — 서버는 사람의 미완성 편집 위로 절대 자동 재개하지 않습니다. 워크트리마다 활성 편집자 하나라는 것은 소프트 클레임입니다: 두 번째 인수는 조용히 병합되지 않고 눈에 보이게 거부됩니다.
- 반환. 제어권을 돌려주면 워크트리의 구조화된 디프 요약을 스페이스에 게시하고, 일시 중지된 실행들의 스티어링으로 대기열에 넣은 뒤, 실행을 재개하고 마커를 해제합니다.
자율성 다이얼
섹션 제목: “자율성 다이얼”스티어링과 인수는 사후 개입입니다. 자율성 다이얼은 상시 설정입니다: 스페이스마다, 에이전트마다, 스페이스 헤더에서 설정하며, 에이전트가 게이트가 걸린 작업에 도달했을 때 무슨 일이 일어나는지를 결정합니다.
| 수준 | 와이어 값 | 게이트가 걸린 도구 호출에 미치는 영향 |
|---|---|---|
| 제안만 | proposeOnly |
이유와 함께 즉시 거부되고, 에이전트는 그 이유를 전달하라는 지시를 받습니다 — 행동을 설명할 수는 있지만 실행할 수는 없습니다 |
| 승인 후 실행 | actWithApproval |
실패 시 닫히는 승인 게이트: 연결된 운영자에게 확인 하나가 표시되고, 승인자가 없으면 거부됩니다 |
| 자유롭게 실행 | actFreely |
미리 승인되지만, 강한 deny 가드레일 규칙이 적용되는 경우는 예외입니다 — 거부 규칙이 항상 이깁니다 |
| (설정 안 함) | null |
승인 후 실행과 같습니다: 서버의 기본값 |
두 가지는 다이얼의 양쪽 바깥에 있습니다. 모드 자체의 출력 동사는 고정되어 있어
다이얼이 무엇을 말하든 거부될 수 없습니다 — 한때 오케스트레이트 모드는 그 동사가
선언하는 효과 클래스를 프리셋이 거부하는 바람에 자신의 유일한 산출물을 구조적으로
낼 수 없었습니다. 그리고 액션 가드레일의 강한 deny는 actFreely로도 재정의할
수 없습니다.
단독 사용에서도 회귀 없음
섹션 제목: “단독 사용에서도 회귀 없음”이 모든 것은 가산적입니다. 단독 운영자의 워크플로는 달라지지 않습니다: 사람 하나에 에이전트가 없으면 프레즌스 허브가 방송할 것이 없어서, 레인은 최초 스냅샷을 내보낸 뒤 트래픽 없이 유휴 상태로 있습니다. LWW와 동기화 기계는 언제나 그랬던 단일 작성자 경로 그대로이고, 페어링 키도 늘 그랬던 기기 자격 증명입니다. 멤버십은 이미 접근 경계입니다 — 사람이 하나면 소유자 행 하나뿐이고 명단 UI도 없습니다.
관련 개념
섹션 제목: “관련 개념”- 워크스페이스와 격리: 멤버십이 강제하는 경계
- 원격 제어와 모바일: 신원에서 페어링 키와 기기 자격 증명을 다루는 쪽
- 에이전트 모델: 주체 유니온에서 에이전트에 해당하는 절반
- 에이전트 디스패치 수명주기: 스티어링과 인수가 일시 중지하고 재개하는 실행
- 가드레일: 자율성 다이얼이 결합되는 효과 클래스 정책
- 도메인 이벤트: 실시간 업데이트가 타는 버스