아키텍처 분석 · gosuda/flats v0.4.0
Flats ↔ 에이전트 연동 구조
에이전트는 Flats에 무엇으로 붙고, 어디까지 할 수 있으며, 어디서 사람이 반드시 개입하는가.
한 줄 요약 다섯 개
- 통로는 MCP 하나예요. 호스트가
/mcp에서 MCP 서버를 직접 띄우고, 도구 27개가 모두 같은core.Service를 호출해요. - 플러그인은 얇은 껍데기예요. 스킬 하나와
flats mcp를 실행하는.mcp.json뿐이고, 호스트 주소는 담지 않아요. flats mcp는 stdio↔HTTP 중계기예요. 표준입력 JSON-RPC를 호스트의 Streamable HTTP로 그대로 넘겨서, 도구·지침이 호스트와 항상 일치해요.- 지식은 호스트가 들고 있어요. 짧은 상시 지침 + 필요할 때 읽는
guide토픽 + 오류별refusal.*안내라서 스킬이 낡아도 동작이 안 바뀌어요. - 공개·활성화·삭제는 사람만 승인해요. 에이전트는 Draft 저장과 요청까지만 하고, 승인 도구는 MCP에 아예 없어요.
1. 전체 그림
에이전트 쪽에는 연결 정보만 있고, 기능과 규칙은 전부 Flats 호스트에 있어요. 아래 그림에서 왼쪽 파란 영역이 에이전트 머신, 오른쪽 초록 영역이 Flats 호스트, 보라색이 운영자(사람)예요.
핵심은 “입구는 여러 개, 서비스는 하나”예요. MCP, CLI, 콘솔이 모두 core.Service의 같은 메서드를 부르고, 호출 출처(via)만 다르게 넘겨요. 권한 검사는 각 입구가 아니라 서비스 안에서 via를 보고 해요. 그래서 MCP에 도구를 하나 더 붙여도 권한 규칙을 우회할 수 없어요.
2. 플러그인: 무엇이 설치되나
레포의 plugins/flats 하나가 세 클라이언트용 매니페스트를 모두 갖고 있어요. 내용물은 놀랄 만큼 작아요.
plugins/flats/
├── .claude-plugin/plugin.json # Claude Code
├── .codex-plugin/plugin.json # Codex (skills, mcpServers, UI 문구)
├── .cursor-plugin/plugin.json # Cursor
├── .mcp.json # {"flats": {"command": "flats", "args": ["mcp"]}}
└── skills/flats-deploy/SKILL.md # 연결 안내 + 승인 규칙 요약
스킬이 하는 일
“도구가 있으면 guide topic.index부터 읽어라, 없으면 flats connect로 호스트를 확인해라”가 전부예요. 실제 배포 방법은 적혀 있지 않아요.
.mcp.json이 하는 일
flats mcp를 stdio MCP 서버로 실행해요. 주소를 넣지 않아서, 플러그인 하나로 어떤 호스트에도 붙어요.
왜 이렇게 얇은가
도구 목록·설명·규칙이 호스트 버전과 함께 바뀌어요. 플러그인을 업데이트하지 않아도 호스트를 올리면 에이전트가 새 기능을 봐요.
플러그인 없이도 연결할 수 있어요. MCP 클라이언트에 http://127.0.0.1:7878/mcp(또는 콘솔의 tailnet 주소 + /mcp)를 HTTP 서버로 등록하면 되고, flats mcp-config가 클라이언트별 설정을 출력해요. MCP 자체가 없으면 <console>/llms.txt가 같은 안내를 텍스트로 줘요.
3. flats mcp 브리지
Claude Code, Codex, Cursor 플러그인이 공통으로 실행할 수 있는 MCP 형태는 stdio예요. 그래서 Flats는 호스트에 HTTP MCP를 두고, 클라이언트 쪽에는 그 앞에 붙는 stdio↔HTTP 중계기를 둬요. 중계기는 메시지를 해석하지 않고 그대로 넘겨요.
호스트 선택 순서
플러그인은 주소를 모르기 때문에, 브리지가 매번 아래 순서로 정해요. 실행 시 stderr에 flats mcp: relaying to … (출처)를 찍어요.
| 순위 | 출처 | 특징 |
|---|---|---|
| 1 | --url 플래그 | MCP 설정의 args에 직접 넣는 방식. 가장 명시적이에요. |
| 2 | FLATS_URL 환경변수 | MCP 클라이언트는 셸 환경을 넘기지 않는 경우가 많아 플러그인에서는 기대하기 어려워요. |
| 3 | flats connect 저장값 | <user config dir>/flats-client/connection.json. 저장 전에 그 주소가 Flats 호스트로 응답하는지 확인해요. 문서가 권장하는 방식이에요. |
| 4 | 기본값 | http://127.0.0.1:7878. 호스트 머신 자체에서 쓰는 경우예요. |
구현에서 눈여겨볼 점
- 타임아웃 없음. 도구 호출이 길 수 있어서 HTTP 클라이언트 타임아웃을 0으로 두고, stdin EOF나 시그널로만 끝나요.
- 큰 메시지 허용. 인라인 업로드가 base64라서 한 줄 최대 256 MiB까지 받아요. 실제 제한은 호스트가 걸어요.
- 순서 보장. 메시지마다 고루틴이 뜨지만 앞 메시지가 호스트에 쓰인 뒤에 다음 것을 보내요. initialize만은 응답까지 기다려 세션 ID를 받은 뒤 진행해요.
- 잘못된 입력. JSON이 아닌 줄은 호스트로 보내지 않고 JSON-RPC 오류
-32700을 바로 돌려줘요.
4. 호스트의 MCP 서버
호스트는 공식 Go SDK(modelcontextprotocol/go-sdk)로 /mcp에 Streamable HTTP 서버를 둬요. 설정이 꽤 보수적이에요.
Stateless + JSON 응답
서버 쪽 세션 상태에 기대지 않아요. 호스트 재시작이나 프록시가 끼어도 다시 붙기 쉬워요.
요청 크기 = 업로드 한도 기준
본문 한도를 업로드 한도 × 1.5 + 1 MiB로 요청마다 계산해요. base64 팽창분을 감안한 값이에요.
지침이 설정과 동기화
업로드 한도가 바뀌면 MCP 서버 객체를 다시 만들어요. 그래서 지침에 적힌 한도와 실제 한도가 항상 같아요.
loopback 판별
양 끝이 모두 loopback이고 Forwarded, X-Forwarded-For, X-Real-Ip, Tailscale-User-Login 헤더가 없을 때만 “이 머신의 프로세스”로 봐요.
save_version_from_dir(호스트 디스크의 디렉터리를 바로 업로드) 같은 도구를 호스트 자신에게만 허용하는 데 쓰여요. tailnet 너머의 에이전트는 파일을 인라인으로 보내야 해요.5. 지식 전달: 지침 → guide → refusal
Flats는 에이전트 컨텍스트를 아끼려고 지식을 세 층으로 나눠요. 항상 필요한 것만 처음에 주고, 나머지는 에이전트가 필요할 때 꺼내 읽게 해요.
docs/agent/ 한 곳이고 guide·llms.txt·참조 문서가 모두 여기서 나와요.이 구조 덕분에 스킬 파일이 아무리 오래돼도 에이전트가 읽는 규칙은 지금 돌아가는 호스트 버전의 것이에요. 스킬에 배포 절차를 써 두면 호스트가 바뀔 때 어긋나는데, Flats는 그걸 구조적으로 막았어요.
6. 도구 27개와 권한
v0.4.0 호스트가 노출하는 도구는 27개예요(핵심 24 + guide, get_runtime_reference, get_content_types). MCP 어노테이션으로 읽기 전용/쓰기/파괴적 동작을 표시해요. 중요한 건 “쓰기”라도 대부분 Draft만 바꾼다는 점이에요.
| 구분 | 도구 | 효과 |
|---|---|---|
| 읽기 | list_flats get_flat list_versions get_draft get_logs get_approval list_env get_network list_secrets guide get_runtime_reference get_content_types | 상태 조회. 비밀값은 이름만 보여요. |
| Draft 쓰기 | create_flat save_draft save_version save_version_from_dir* save_document open_preview set_env delete_env | 비공개 Draft·미리보기·일반 환경변수만 바꿔요. 라이브는 그대로예요. (*호스트 loopback 전용) |
| 승인 요청 | publish deploy rollback set_visibility delete_flat | pending_approval + approval_url만 돌려줘요. 승인 전에는 아무것도 안 바뀌어요. |
| 예외 | update_document | 실행 중인 문서 flat의 본문을 승인 없이 즉시 수정해요. Public이면 바로 모두에게 보여요. |
| 운영자 전용 | (MCP에 없음) | 승인 결정, 비밀값 설정·삭제, 외부 네트워크 허용 목록, 공급자(tailscale·funnel·portal) 권한. 서비스가 via를 보고 MCP 호출을 거절해요. |
7. 배포 흐름과 승인 게이트
guide의 topic.index가 제시하는 표준 흐름은 다섯 단계예요. 에이전트가 할 수 있는 구간과 사람이 해야 하는 구간이 명확히 나뉘어요.
- 승인은 무엇을 승인하는지 고정해요. Draft revision·hash, 버전, 스냅샷, 공개 범위, 공급자를 얼려두고, 그 사이 바뀌면
stale_approval로 실패해요. 같은 요청이 이미 대기 중이면 새로 만들지 않고 재사용해요. - 결과는 숫자로 와요.
get_approval의result_data에failure_code,data_impact(none/runtime_start/restore_data/unknown) 등이 담기고, 에이전트는 그대로 보고하게 돼 있어요. - Draft는 덮어쓰기예요. 매번 전체 빌드를 보내고,
expected_revision으로 동시 수정 충돌(conflict)을 잡아요.
8. 신뢰 경계와 보안
9. 배치 형태: 호스트에서 vs 원격에서
같은 플러그인이 두 가지 배치에서 모두 동작해요. 차이는 flats mcp가 어디로 중계하느냐와, 호스트 디스크를 직접 쓸 수 있느냐뿐이에요.
| 항목 | A. 호스트에서 직접 | B. 원격 클라이언트 |
|---|---|---|
| CLI 설치 | 설치 스크립트 기본값 (서비스 포함) | --no-service로 바이너리만 |
| 호스트 지정 | 필요 없음 (loopback 기본값) | flats connect <console URL> 또는 MCP 설정에 --url |
| 업로드 | 인라인 또는 save_version_from_dir | 인라인, 또는 CLI flats deploy <dir> |
| 접근 제어 | loopback | tailnet ACL (콘솔도 같은 경계) |
| 컨테이너로 운영할 때 | Linux에서는 host 네트워크를 쓰고, 콘솔 포트를 외부에 publish하지 말라고 문서가 권고해요. | |
플러그인 형식을 지원하지 않는 MCP 클라이언트도 같은 구성으로 붙을 수 있어요. 스킬 파일을 그 클라이언트의 스킬 위치에 두고, MCP 서버로 stdio flats mcp(또는 HTTP /mcp)를 등록하면 플러그인과 똑같이 동작해요.
10. 설계에서 배울 점과 주의점
배울 점: 지식은 서버에
도구 설명·규칙·오류 해설을 호스트가 버전과 함께 제공해요. 클라이언트 쪽 문서가 낡아서 생기는 사고가 없어요.
배울 점: 권한은 서비스 한 곳에서
via 하나로 MCP·CLI·콘솔을 구분하니, 입구가 늘어도 권한 검사가 흩어지지 않아요.
배울 점: 요청과 실행의 분리
에이전트는 결과물이 아니라 “요청”을 만들어요. 승인 시점에 대상을 고정해 바꿔치기도 막아요.
주의: 콘솔 = 신뢰 경계
콘솔에 닿을 수 있으면 승인할 수 있어요. tailnet ACL과 호스트에서 돌리는 프로세스 관리가 실제 보안이에요.
주의: update_document
유일하게 승인 없이 라이브를 바꾸는 도구예요. Public 문서 flat이면 즉시 공개돼요.
주의: 버전 맞추기
원격 클라이언트의 CLI는 호스트와 따로 업그레이드돼요. 브리지는 내용을 해석하지 않아 대부분 괜찮지만, CLI 명령은 버전 차이를 탈 수 있어요.