"What이 아니라 Why." 팀의 결정과 그 이유를 한곳에 모아, 검색하고 · 이력을 추적하고 · AI로 초안까지 만드는 지식 저장소입니다.
ADR(아키텍처 결정 기록), 설계 의도, 컨벤션 가이드처럼 "우리는 왜 이렇게 하기로 했나" 를 담은 문서를 정규화된 마크다운으로 중앙관리합니다. 사람은 웹 UI로, AI 에이전트는 MCP로, 다른 시스템은 HTTP API로 같은 지식에 접근합니다.
- 🔍 검색 — 키워드로 관련 문서를 찾습니다. (벡터·GPU 없이 가벼운 유사 RAG 방식)
- 📖 읽기·브라우징 — 프로젝트별 문서 목록, 렌더링된 마크다운, 관련 문서 추천을 웹에서 봅니다.
- ✍️ 쓰기 — 웹 에디터에서 직접 작성하거나, AI에게 초안 생성을 맡길 수 있습니다.
- 🤖 AI 멀티턴 생성 — 대화하며 문서 초안을 다듬고, 원하면 그대로 저장까지(펑션콜 기반).
- 🕐 이력 추적 — 문서가 언제·어디가·왜 바뀌었는지 자체 delta 이력으로 남깁니다. (git 불필요)
- ✅ 승인 워크플로우 — 모든 쓰기를 관리자 승인 대기 큐에 올려 검토 후 반영합니다.
- 🗂 멀티프로젝트 — 하나의 허브에서 여러 프로젝트의 지식을 프로젝트 단위로 나눠 관리합니다.
| 원칙 | 의미 |
|---|---|
| 모든 쓰기는 save 게이트를 거친다 | 정규화·lint 검사를 통과하지 못한 문서는 저장·색인되지 않습니다. |
| ADR엔 근거가 필수 | 배경·결정·근거·대안·결과 섹션이 없으면 저장을 거부합니다. (Why를 강제) |
| 한 코어, 세 인터페이스 | 웹 UI · MCP · HTTP가 모두 같은 서비스 로직을 호출합니다. |
| 가벼운 스택 | 임베딩·벡터DB·리랭커·GPU를 쓰지 않습니다. |
로컬은 별도 DB 없이 파일 + SQLite(FTS5) 만으로 동작합니다.
# 1) 의존성 설치 (Python 3.11+)
pip install -e .
# 2) (선택) AI 기능용 LLM 엔드포인트를 환경변수로 주입 — 미설정 시 AI 기능만 건너뜁니다.
export KNOWLEDGE_HUB_LLM_URL="https://<게이트웨이>/deployments/<모델>/generate"
export KNOWLEDGE_HUB_LLM_API_KEY="<Bearer 토큰>" # 게이트웨이가 요구 — 없으면 401
# 3) 웹 관리 서버 실행 → http://127.0.0.1:8000
python -m hub.interfaces.web
# 3) (선택) MCP 서버 실행 — AI 에이전트 접근용
python -m hub.interfaces.mcp_server # 기본 stdio (인증 off 로컬 전용 — 인증 on 이면 streamable-http 강제)
# 4) (선택) HTTP JSON API 만 띄우기 (웹 UI 없이 순수 JSON) → http://127.0.0.1:8000
python -m hub.interfaces.http_api브라우저에서 http://127.0.0.1:8000 을 열면 문서 목록 · 검색 · 작성 · AI 생성 · 승인함을 사용할 수 있습니다.
설정은 작업 디렉토리의
config.toml(또는KNOWLEDGE_HUB_CONFIG환경변수)에서 읽습니다. 예시는config.example.toml을 참고하세요. AI 기능(검색 요약·초안 생성·멀티턴 채팅)은 OpenAI 호환 단일 엔드포인트(vLLM)를 씁니다. LLM 엔드포인트 URL과 Bearer 토큰은 둘 다 시크릿이라 git에 커밋하지 않고 환경변수로 주입합니다 —KNOWLEDGE_HUB_LLM_URL(URL 하나, 스트리밍 여부는 body 의stream플래그로 구분)과KNOWLEDGE_HUB_LLM_API_KEY(게이트웨이가Authorization: Bearer <토큰>을 요구 — 없으면 본문을 보기도 전에 401). 인증이 없는 엔드포인트라면 토큰은 비워 두면 됩니다. 템플릿은.env.example을 복사해.env로 두면 됩니다(.env는 git 무시). 미설정 시 AI 관련 기능만 자동으로 건너뜁니다.model(옵션)/max_tokens는 비시크릿 튜닝값이라config.toml의[llm]에 둡니다.
python scripts/seed_knowledge.py # 템플릿·샘플 문서 시드파일 백엔드 대신 로컬 PostgreSQL 컨테이너를 쓰려면 config.local-pg.toml
프리셋을 지정합니다. 앱은 .env 를 자동 로드하지 않으니(자동 로드는 docker compose 만) 셸에
먼저 올립니다.
docker run -d --name why-hub-pg -p 5432:5432 \
-e POSTGRES_USER=hub -e POSTGRES_PASSWORD=hubpw -e POSTGRES_DB=knowledge_hub postgres:18
set -a; source .env; set +a # KNOWLEDGE_HUB_CONFIG=config.local-pg.toml 등
python -m hub.interfaces.web이 프리셋은 bootstrap_from_files = true 라, DB 가 비어 있으면 기동 시 knowledge/ 문서를
1회 자동 이관합니다(빈 화면 방지). DB 에 문서가 하나라도 있으면 건너뜁니다 — 낡은 파일이 UI
편집분과 이력을 덮어쓰지 않게 하는 방어선입니다. 자세한 계약은
구현스펙-postgres-배포.md §8.1.
배포 시에는 모든 상태를 PostgreSQL에 저장합니다(문서·이력·스냅샷·제출·FTS). 트랜잭션으로 안전하게 직렬화됩니다.
# 시크릿·엔드포인트는 .env 로 주입 (docker compose 가 자동 로드). .env.example 을 복사해 채웁니다.
cp .env.example .env # KNOWLEDGE_HUB_LLM_URL, PGPASSWORD 를 채운다 (.env 는 git 무시)
docker compose up --build
# → postgres + admin(웹 UI, :8000) + mcp(streamable-http, :8001)
# 최초 1회: 기존 로컬 knowledge/ 를 PostgreSQL 로 이관
docker compose run --rm admin python scripts/import_to_postgres.py- 관리 서버
http://localhost:8000— 웹 UI · HTTP API · 승인함 · 인증/JWT 발급 - MCP 서버
http://localhost:8001— 원격 AI 에이전트 접근(streamable-http, JWT 필수) - 배포 설정은
config.deploy.toml([storage] backend="postgres"), 로컬은config.toml(file)을 그대로 씁니다.
로그인·회원가입·PAT·JWT·역할(member/admin)을 지원합니다. 상세는 docs/specs/구현스펙-인증인가-RBAC.md.
- 웹(사람) = opaque 세션 쿠키(HttpOnly·SameSite=Lax·Secure) + CSRF. JWT를 브라우저에 저장하지 않습니다.
- MCP(에이전트) = Bearer JWT(RS256). admin 서버가 PAT를 단기 JWT로 발급하고, MCP 서버는 공개키로 검증만 합니다.
- 역할은 scope로 판정: member =
knowledge:read+knowledge:submit, admin = +knowledge:review.
로컬 개발 기본은 인증 off(
config.toml에[auth]없음 → 무마찰). 인증을 켜려면AUTH_ENABLED=true+ 아래 키/시크릿을 주입합니다. 배포(docker-compose)는 인증 on이 기본입니다.
# 1) RSA 키 생성 (admin 만 private, MCP 는 public 으로 검증)
mkdir -p secrets
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out secrets/jwt_private.pem
openssl rsa -pubout -in secrets/jwt_private.pem -out secrets/jwt_public.pem
# 2) 시크릿·키 경로를 환경변수로 주입하고 인증을 켠다
export AUTH_ENABLED=true
export AUTH_COOKIE_SECURE=false # 로컬(HTTP)은 false, 배포(HTTPS)는 true
export AUTH_PAT_PEPPER="$(python3 -c 'import secrets;print(secrets.token_urlsafe(48))')"
export AUTH_SESSION_SECRET="$(python3 -c 'import secrets;print(secrets.token_urlsafe(48))')"
export AUTH_PRIVATE_KEY_FILE=secrets/jwt_private.pem
export AUTH_PUBLIC_KEY_FILE=secrets/jwt_public.pem
python -m hub.interfaces.web # http://127.0.0.1:8000/ui/signup 에서 가입 → 로그인/ui/signup으로 가입하면 즉시 active member가 됩니다. /ui/account에서 비밀번호 변경, /ui/account/tokens에서 PAT를 관리합니다.
# 1) 웹 /ui/account/tokens 에서 PAT 발급 (원문은 생성 직후 1회만 표시됨: whp_<id>_<secret>)
# 2) PAT 를 단기 JWT 로 교환 (응답에 Cache-Control: no-store)
curl -s -X POST http://localhost:8000/api/auth/token/exchange \
-H "Authorization: Bearer whp_<id>_<secret>"
# → {"access_token":"<JWT>","token_type":"Bearer","expires_in":600,"scope":"knowledge:read knowledge:submit"}
# 3) JWT 를 MCP 요청의 Authorization 헤더로 전달 (streamable-http)
# 예: fastmcp Client(url="http://localhost:8001/mcp/", auth="<JWT>")
curl -s http://localhost:8001/mcp/ -H "Authorization: Bearer <JWT>" -H "Accept: text/event-stream"관리자 지정/해제 UI는 없습니다. DB에서 is_admin을 바꿉니다(변경 후 재로그인/신규 JWT부터 review 권한 반영).
-- 로컬(SQLite): sqlite3 knowledge/auth.sqlite
UPDATE users SET is_admin = 1 WHERE username = '관리자사용자명';
-- 배포(PostgreSQL): auth 스키마
UPDATE auth.users SET is_admin = true WHERE username = '관리자사용자명';- JWT 는 기본 10분(
AUTH_ACCESS_TOKEN_TTL_SECONDS) 유효합니다. MCP는 공개키만으로 무상태(stateless) 검증합니다. - PAT 를 폐기해도 이미 발급된 JWT 는 즉시 무효화되지 않고 JWT 만료(최대 10분)까지 유효할 수 있습니다(무상태 검증의 대가).
- 인증이 켜진 상태에서 MCP
stdiotransport 는 기동을 거부합니다 — stdio는 헤더 기반 Bearer 인증을 실을 수 없어 무인증 노출이 되기 때문입니다. 배포는streamable-http만 사용합니다. - 비밀번호 변경 시 현재 세션은 유지, 다른 모든 웹 세션은 로그아웃됩니다(PAT은 유지).
- 배포는 admin/mcp가 서로 다른 PostgreSQL 롤을 씁니다(admin=auth+knowledge, mcp=knowledge only). MCP 컨테이너에는 private key·PAT pepper를 전달하지 않습니다(공개키만).
전역 역할(member/admin) 위에 프로젝트별 역할을 부여합니다.
- 역할:
viewer(읽기) /editor(읽기+제출). admin 은 모든 프로젝트 전권. - 기본 프로젝트(
default_project)는 모든 로그인 사용자에게 공개(읽기+쓰기). 그 외 프로젝트는 명시적 부여가 필요하며, 부여받지 못하면 목록·검색·조회·쓰기 모두 차단됩니다(deny-by-default). - 관리(admin 전용):
/ui/projects에서 프로젝트를 생성·수정·삭제하고 멤버(접근 권한)를 추가/제거합니다. 관리자 지정처럼 별도 CLI 없이 웹에서 관리합니다. 프로젝트를 삭제하면 접근 권한(멤버십)은 회수되지만 문서는 남아 이후 관리자만 접근할 수 있고, 기본 프로젝트는 삭제할 수 없습니다. - 강제 위치: 웹 세션·MCP JWT 모두 인증 주체를 서비스에 전달해 동일하게 강제합니다. 권한 필터는 검색·랭킹 이전에 적용됩니다(누출 방지).
- MCP staleness: 프로젝트 멤버십은 웹에는 즉시 반영되지만, 이미 발급된 JWT 에는 만료(최대 10분) 후 반영됩니다(무상태 검증). 새 JWT를 교환하면 즉시 최신 권한이 적용됩니다.
프로젝트 멤버십도 DB에서 직접 조회/변경할 수 있습니다(로컬 SQLite auth.sqlite / 배포 PostgreSQL auth 스키마):
-- carol 에게 alpha 프로젝트 editor 권한 부여 (예시 — 보통은 /ui/projects 에서 관리)
INSERT INTO project_members(project_slug, user_id, role, created_at)
VALUES ('alpha', '<carol_user_id>', 'editor', '2026-07-15T00:00:00');- 멀티테넌트 분리·프로젝트별 리뷰어 역할은 이번 범위 밖(향후 G1 확장)입니다.
세 인터페이스 모두 동일한 서비스 코어를 호출합니다 — 로직 중복이 없습니다.
MCP 도구 (AI 에이전트용)
search_knowledge · get_document · list_documents · list_projects · get_history ·
get_docs_diff · get_related · save_document · ingest_source · curate ·
list_submissions · approve_submission · reject_submission
HTTP API (읽기 JSON + 쓰기/생성)
GET /search · GET /docs · GET /docs/{id} · PUT /docs/{id} · GET /docs/{id}/history ·
GET /docs/{id}/diff · GET /docs/{id}/related · POST /ingest · POST /generate ·
POST /chat · /chat/stream · /chat/apply · GET/POST /submissions...
웹 UI (사람용) — 로그인 · 회원가입 · 내 계정 · PAT 관리 · 목록 · 문서 뷰 · 검색 · 편집 · AI 생성 · 멀티턴 채팅 · 이력 · 승인함(admin)
인증이 켜지면 목록·검색·조회·작성·AI·승인함은 로그인 필수입니다. 무인증 공개 경로는 로그인·회원가입·정적·
/healthz·/.well-known/jwks.json·POST /api/auth/token/exchange(PAT 인증)뿐입니다. actor/approver는 요청/폼이 아니라 인증 세션에서만 결정됩니다.
why-hub/
├─ hub/
│ ├─ store/ # 저장 코어: 정규화·lint·앵커·diff·이력·스냅샷·FTS·락·reconcile
│ │ ├─ file_store.py # FileStore (로컬: 파일 + SQLite FTS5)
│ │ └─ pg_store.py # PostgresStore (배포: 전부 PostgreSQL)
│ ├─ service.py # 서비스 레이어 — 모든 인터페이스가 부르는 단일 코어
│ ├─ interfaces/ # web(UI) · http_api(JSON) · mcp_server(MCP)
│ ├─ chat.py, llm.py # AI 멀티턴 생성 · OpenAI 호환 LLM 클라이언트
│ ├─ eval/ # 검색 품질 평가 셋(골든 질의)
│ └─ tests/ # 테스트 (Phase별 · 계약 · 워크플로우)
├─ knowledge/ # 지식 저장소 (docs · .snapshots · docs-diff · history · index)
├─ web/ # UI 템플릿(HTMX) · 정적 자원(css/js)
├─ templates/ # ADR · 설계 의도 문서 템플릿
├─ scripts/ # seed · postgres 이관 · 검색 평가
├─ docs/ # 기획안 · 구현 스펙 · 개발 계획 · 프롬프트
├─ config.toml # 로컬 운영 설정 (이 저장소 도그푸딩용)
├─ docker-compose.yml # 배포: postgres + admin + mcp
└─ CLAUDE.md # AI 협업 규칙 · 불변식 · 확정 결정
docs/{adr,design-intent,guide}/*.md # 현재 문서 (원천)
.snapshots/*.md # diff 기준점
docs-diff/*.md # 의도된 변경 (스펙 선구동)
history/*.md # 자동 delta 이력 (append-only)
index.sqlite # FTS5 검색 인덱스 (FileStore)
pip install -e ".[dev]"
pytest # 전체
pytest hub/tests/test_p02_normalize_lint_anchors.py # 개별정규화 멱등성 · 앵커 무결성 · save 라운드트립(생성→save→검색→조회) 등 회귀 방지의 핵심을 검증합니다.
- CLAUDE.md — 프로젝트 불변식, 확정된 구현 결정, 모듈 경계 (AI·기여자 필독)
- docs/proposals/ — 왜/무엇: 기획안1(팀 내부 MVP) · 기획안2(범용 확장)
- docs/specs/ — 어떻게: 자체 delta 엔진 · 앵커 · 멀티턴 생성 · 승인 · 멀티프로젝트 · PostgreSQL 배포 상세 계약
- docs/개발-실행계획-산출물인덱스.md — 산출물 인덱스 · Phase 실행 순서 · 향후 범용 확장(G1~G5) 로드맵
현재는 팀 내부 MVP 단계입니다(M1M8 완료: 코어 · 인터페이스 · UI · AI 생성 · 협업 · PostgreSQL 배포).
다음 단계인 **범용 확장(G1G5)** — 멀티테넌트 · 세밀 권한 · 외부 커넥터 · (조건부) 벡터 검색 — 은
승격 트리거가 실제로 발생할 때 착수합니다. 자세한 계획은 위 개발 실행계획 문서를 참조하세요.