From 33583481967346cc06277feec9d46edcb05061c2 Mon Sep 17 00:00:00 2001 From: JuYoung Jeong Date: Tue, 31 Mar 2026 19:39:03 +0900 Subject: [PATCH] docs: add comprehensive Korean source code analysis (19 documents, 6,926 lines) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete Korean-language technical documentation analyzing Claude Code CLI internals, organized in a leveled guide structure (Level 1-3 + Appendix). Level 1 (입문): architecture overview, request lifecycle, key concepts Level 2 (시스템): QueryEngine, Tool system, Command system, Permission system, Agent coordinator, UI/Ink components Level 3 (심화): MCP/LSP integration, IDE bridge, Plugin/Skill system, Context compression, OAuth/Auth, Telemetry Appendix: Korean-English glossary, file map, design patterns Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/ko/README.md | 165 +++++ docs/ko/appendix/design-patterns.md | 483 +++++++++++++ docs/ko/appendix/file-map.md | 282 ++++++++ docs/ko/appendix/glossary.md | 160 +++++ docs/ko/level-1-overview/architecture.md | 209 ++++++ docs/ko/level-1-overview/key-concepts.md | 204 ++++++ docs/ko/level-1-overview/request-lifecycle.md | 332 +++++++++ docs/ko/level-2-systems/agent-coordinator.md | 561 ++++++++++++++++ docs/ko/level-2-systems/command-system.md | 459 +++++++++++++ docs/ko/level-2-systems/permission-system.md | 365 ++++++++++ docs/ko/level-2-systems/query-engine.md | 402 +++++++++++ docs/ko/level-2-systems/tool-system.md | 633 ++++++++++++++++++ docs/ko/level-2-systems/ui-ink-components.md | 396 +++++++++++ docs/ko/level-3-internals/bridge-ide.md | 263 ++++++++ .../level-3-internals/context-compression.md | 326 +++++++++ .../level-3-internals/mcp-lsp-integration.md | 410 ++++++++++++ docs/ko/level-3-internals/oauth-auth.md | 391 +++++++++++ .../level-3-internals/plugin-skill-system.md | 530 +++++++++++++++ docs/ko/level-3-internals/telemetry.md | 355 ++++++++++ 19 files changed, 6926 insertions(+) create mode 100644 docs/ko/README.md create mode 100644 docs/ko/appendix/design-patterns.md create mode 100644 docs/ko/appendix/file-map.md create mode 100644 docs/ko/appendix/glossary.md create mode 100644 docs/ko/level-1-overview/architecture.md create mode 100644 docs/ko/level-1-overview/key-concepts.md create mode 100644 docs/ko/level-1-overview/request-lifecycle.md create mode 100644 docs/ko/level-2-systems/agent-coordinator.md create mode 100644 docs/ko/level-2-systems/command-system.md create mode 100644 docs/ko/level-2-systems/permission-system.md create mode 100644 docs/ko/level-2-systems/query-engine.md create mode 100644 docs/ko/level-2-systems/tool-system.md create mode 100644 docs/ko/level-2-systems/ui-ink-components.md create mode 100644 docs/ko/level-3-internals/bridge-ide.md create mode 100644 docs/ko/level-3-internals/context-compression.md create mode 100644 docs/ko/level-3-internals/mcp-lsp-integration.md create mode 100644 docs/ko/level-3-internals/oauth-auth.md create mode 100644 docs/ko/level-3-internals/plugin-skill-system.md create mode 100644 docs/ko/level-3-internals/telemetry.md diff --git a/docs/ko/README.md b/docs/ko/README.md new file mode 100644 index 0000000..b54bec5 --- /dev/null +++ b/docs/ko/README.md @@ -0,0 +1,165 @@ +# Claude Code 소스코드 분석 문서 + +## 프로젝트 소개 + +이 프로젝트는 Anthropic의 **Claude Code CLI** 소스코드를 체계적으로 분석한 한국어 기술 문서입니다. Claude Code는 1,900개 이상의 파일과 512,000줄 이상의 TypeScript 코드로 구성된 대규모 개발 도구입니다. npm 소스맵을 통해 공개된 원본 코드를 바탕으로, 각 시스템의 설계 원리, 구현 방식, 데이터 흐름을 상세히 기술합니다. 이 문서는 Claude Code의 내부 동작 방식을 이해하고자 하는 개발자, 아키텍처 설계자, 그리고 고급 사용자를 위해 작성되었습니다. + +--- + +## 기술 스택 한눈에 보기 + +Claude Code는 다음과 같은 핵심 기술 스택으로 구성되어 있습니다: + +| 계층 | 기술 | 용도 | +|------|------|------| +| **런타임** | Bun | JavaScript/TypeScript 실행 엔진 | +| **언어** | TypeScript | 타입 안전 및 대규모 코드베이스 관리 | +| **CLI 프레임워크** | Commander.js | 커맨드 라인 인터페이스 및 인자 파싱 | +| **UI 라이브러리** | React/Ink | 터미널 기반 사용자 인터페이스 | +| **검증** | Zod v4 | 런타임 데이터 스키마 검증 | +| **프로토콜** | MCP SDK | Machine Context Protocol 구현 | +| **통신** | LSP (Language Server Protocol) | IDE 브릿지 및 언어 서버 지원 | +| **모니터링** | OpenTelemetry | 분산 추적 및 성능 관찰 | +| **번들링** | esbuild | TypeScript 컴파일 및 번들 최적화 | + +--- + +## 읽기 가이드 + +이 문서는 세 가지 학습 경로를 제공합니다. 목표와 시간 여건에 따라 선택하세요. + +### 경로 1: 빠른 개요 (30분) +Claude Code의 전체 개념을 빠르게 파악하고 싶다면 이 경로를 추천합니다. +1. [architecture.md](./level-1-overview/architecture.md) - 전체 아키텍처 구조 +2. [request-lifecycle.md](./level-1-overview/request-lifecycle.md) - 사용자 요청이 처리되는 전 과정 + +### 경로 2: 특정 시스템 이해 (1-2시간) +특정 기능이나 시스템에 깊게 들어가고 싶다면 이 경로를 추천합니다. +1. [key-concepts.md](./level-1-overview/key-concepts.md) - 핵심 개념 정리 +2. Level 2 문서에서 관심 있는 시스템 선택 + +### 경로 3: 전체 정독 (4-8시간) +Claude Code 전체를 마스터하고 싶은 개발자를 위한 완전한 학습 경로입니다. +- Level 1 → Level 2 → Level 3 순서로 진행 +- 각 문서는 이전 수준의 개념을 바탕으로 구성 + +--- + +## 문서 목차 + +### Level 1: 입문 (기초 개념) + +개발자가 Claude Code를 이해하기 위해 필수적인 개념과 전체 구조를 다룹니다. + +| 문서 | 설명 | 읽기 시간 | +|------|------|---------| +| [architecture.md](./level-1-overview/architecture.md) | Claude Code의 전체 아키텍처 조감도, 주요 컴포넌트 관계도, 계층 구조 | 15분 | +| [request-lifecycle.md](./level-1-overview/request-lifecycle.md) | 사용자 입력부터 응답까지의 전체 데이터 흐름, 각 단계별 처리 프로세스 | 20분 | +| [key-concepts.md](./level-1-overview/key-concepts.md) | QueryEngine, Tool, Command, Permission, Agent 등 핵심 개념 정리 | 15분 | + +### Level 2: 서브시스템 분석 (시스템 이해) + +각 주요 기능과 시스템의 설계 원리, 구현 방식, 상호 작용을 상세히 분석합니다. + +| 문서 | 설명 | 읽기 시간 | +|------|------|---------| +| [query-engine.md](./level-2-systems/query-engine.md) | QueryEngine의 구조, 쿼리 파싱, 실행 엔진, 최적화 기법 | 25분 | +| [tool-system.md](./level-2-systems/tool-system.md) | Tool 레지스트리, Tool 로딩 메커니즘, 권한 검증, 실행 아키텍처 | 25분 | +| [command-system.md](./level-2-systems/command-system.md) | Commander.js 통합, 커맨드 파싱, 서브커맨드 체계, 헬프 생성 | 20분 | +| [permission-system.md](./level-2-systems/permission-system.md) | 권한 모델, 정책 평가, 감사 로깅, 사용자 제약 관리 | 25분 | +| [agent-coordinator.md](./level-2-systems/agent-coordinator.md) | 에이전트 오케스트레이션, 상태 관리, 태스크 스케줄링, 병렬 실행 | 25분 | +| [ui-ink-components.md](./level-2-systems/ui-ink-components.md) | React/Ink 통합, 컴포넌트 계층, 상태 렌더링, 터미널 UI 아키텍처 | 20분 | + +### Level 3: 심화 분석 (고급 구현) + +프로토콜 통합, 보안, 성능 최적화 등 고급 주제를 다룹니다. + +| 문서 | 설명 | 읽기 시간 | +|------|------|---------| +| [mcp-lsp-integration.md](./level-3-internals/mcp-lsp-integration.md) | MCP 프로토콜 구현, LSP 서버 통합, 프로토콜 메시징, 핸들러 등록 | 30분 | +| [bridge-ide.md](./level-3-internals/bridge-ide.md) | IDE 통합 메커니즘, 언어 서버 브릿지, 에디터 협력 프로토콜 | 25분 | +| [plugin-skill-system.md](./level-3-internals/plugin-skill-system.md) | 플러그인 아키텍처, Skill 시스템, 동적 로딩, 버전 관리 | 30분 | +| [context-compression.md](./level-3-internals/context-compression.md) | 컨텍스트 크기 관리, 토큰 최적화, 압축 알고리즘, 메모리 효율화 | 25분 | +| [oauth-auth.md](./level-3-internals/oauth-auth.md) | OAuth 2.0 플로우, 토큰 관리, 갱신 메커니즘, 보안 모범 사례 | 30분 | +| [telemetry.md](./level-3-internals/telemetry.md) | OpenTelemetry 구현, 추적 설정, 메트릭 수집, 성능 모니터링 | 20분 | + +### Appendix: 참고 자료 + +기술 용어, 파일 구조, 설계 패턴 참고 자료입니다. + +| 문서 | 설명 | +|------|------| +| [glossary.md](./appendix/glossary.md) | 한영 기술 용어 대조표, 개념 정의, 약자 해설 | +| [file-map.md](./appendix/file-map.md) | 주요 파일 경로 인덱스, 모듈 위치, 디렉토리 구조 | +| [design-patterns.md](./appendix/design-patterns.md) | Claude Code에서 사용된 설계 패턴, 구현 사례, 적용 방법 | + +--- + +## 기여 방법 + +이 분석 문서는 교육 및 연구 목적으로 작성된 것으로, 다음과 같은 방식으로 개선할 수 있습니다: + +1. **오류 신고**: 잘못된 정보나 부정확한 설명을 발견하면 이슈로 등록해주세요. +2. **내용 추가**: 누락된 시스템이나 더 깊이 있는 분석이 필요한 부분을 제시해주세요. +3. **번역 검수**: 기술 용어의 적절한 표현이나 더 나은 번역 제안을 환영합니다. +4. **예제 개선**: 이해하기 쉬운 코드 예제나 다이어그램을 제시해주세요. + +--- + +## 라이선스 및 면책 + +### 저작권 및 라이선스 + +이 문서는 **교육 목적의 기술 분석**으로 작성되었습니다. 원본 Claude Code 소스코드의 저작권은 **Anthropic Inc.**에 있습니다. 이 분석 문서는 공개된 npm 소스맵 정보를 바탕으로 한국 개발자 커뮤니티의 학습을 목적으로 작성되었습니다. + +### 면책 사항 + +- **완전성 보장 안 함**: 이 문서는 Claude Code의 일부 측면을 분석한 것으로, 전체 기능을 포괄하지 않을 수 있습니다. +- **버전 의존성**: Claude Code는 지속적으로 업데이트되므로, 이 문서의 내용은 특정 버전을 기준으로 작성되었습니다. +- **상업적 용도 제한**: 이 문서는 개인 학습, 학술 연구, 비영리 교육 목적으로만 사용하세요. +- **보증 부재**: 이 문서의 정보 사용으로 인한 어떤 손해나 문제도 저자는 책임지지 않습니다. + +### 원저작권자 + +Claude Code의 원본 소스코드는 [Anthropic Inc.](https://anthropic.com)의 저작물입니다. + +--- + +## 시작하기 + +### 추천 학습 순서 + +1. 이 README를 완독하여 문서 구조를 파악합니다. +2. 본인의 목표에 맞춰 위의 "읽기 가이드"에서 경로를 선택합니다. +3. 선택한 경로의 문서를 순서대로 읽습니다. +4. 특정 시스템이 궁금하면 해당 Level 2 문서로 이동합니다. +5. 구현 세부사항이 필요하면 Level 3 문서를 참고합니다. +6. 용어나 개념이 불명확하면 Appendix의 용어사전과 설계 패턴을 확인합니다. + +### 선수 지식 + +이 문서를 효과적으로 이해하기 위해서는 다음의 선수 지식이 도움됩니다: + +- **TypeScript**: 기본 문법 및 타입 시스템 이해 +- **Node.js/Bun**: JavaScript 런타임 개념 +- **CLI 애플리케이션**: 커맨드 라인 도구의 기본 구조 +- **React 기초**: 컴포넌트 기반 아키텍처 개념 +- **네트워크 프로토콜**: HTTP, 소켓 통신의 기본 이해 + +선수 지식이 부족하더라도 Level 1부터 시작하면 필요한 개념을 학습하며 진행할 수 있습니다. + +--- + +## 문서 유지 관리 + +### 최종 수정일 + +이 README는 **2026년 3월 31일** 기준으로 작성되었습니다. Claude Code의 버전 변화에 따라 내용이 변경될 수 있습니다. + +### 피드백 및 연락 + +분석 내용에 대한 피드백, 오류 신고, 개선 제안은 프로젝트 이슈 트래커를 통해 제출해주세요. + +--- + +**이 문서로 Claude Code의 내부 구조를 깊이 있게 이해하고, 더 나은 개발자가 되기를 바랍니다.** diff --git a/docs/ko/appendix/design-patterns.md b/docs/ko/appendix/design-patterns.md new file mode 100644 index 0000000..e818da1 --- /dev/null +++ b/docs/ko/appendix/design-patterns.md @@ -0,0 +1,483 @@ +# Claude Code 설계 패턴 모음 + +> Claude Code 소스코드에서 발견된 핵심 설계 패턴을 정리한다. 각 패턴에 대해 목적, 코드 예시, 장점과 트레이드오프를 기술한다. + +--- + +## 1. 병렬 Prefetch 패턴 (Parallel Prefetch Pattern) + +### 설명 + +`main.tsx` 진입 직후, 독립적인 초기화 작업(MDM 읽기, 키체인 읽기, 환경 설정 적용)을 **모듈 임포트 단계에서 즉시 비동기적으로 시작**한다. 이후 실제로 해당 값이 필요한 시점까지 I/O가 백그라운드에서 완료되므로 스타트업 레이턴시가 크게 감소한다. + +### 코드 예시 + +```typescript +// src/main.tsx - 최상단 사이드이펙트 구간 + +import { profileCheckpoint } from './utils/startupProfiler.js'; +profileCheckpoint('main_tsx_entry'); // 1. 스타트업 타임스탬프 기록 + +import { startMdmRawRead } from './utils/settings/mdm/rawRead.js'; +startMdmRawRead(); // 2. MDM 서브프로세스 즉시 실행 (plutil/reg query) + +import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js'; +startKeychainPrefetch(); // 3. macOS 키체인 읽기 병렬 시작 + // (OAuth + 레거시 API 키 동시 조회) + +// 이후 ~135ms의 모듈 임포트가 진행되는 동안 +// 위 세 작업이 백그라운드에서 완료됨 +``` + +```typescript +// 나중에 실제로 필요할 때 완료 대기 +await ensureKeychainPrefetchCompleted() +``` + +### 장점 + +- MDM 읽기와 키체인 읽기를 직렬로 수행할 경우 약 65ms의 동기 블로킹이 발생하는데, 병렬화로 이를 제거 +- 스타트업 크리티컬 패스에서 I/O 대기 시간을 숨김 (latency hiding) +- 각 프리페치 모듈이 독립적이어서 실패가 격리됨 + +### 트레이드오프 + +- 사이드이펙트가 임포트 단계에서 발생하므로 `eslint-disable custom-rules/no-top-level-side-effects` 주석이 필요 +- 프리페치 완료 전에 해당 값에 접근하면 블로킹이 발생하므로 호출 순서를 신중히 관리해야 함 +- 테스트에서 모킹이 까다로움 + +--- + +## 2. Dead Code Elimination 패턴 (Bundle Feature Flag Pattern) + +### 설명 + +Bun 번들러의 `feature()` 함수를 사용해 컴파일 타임에 피처 플래그 값을 평가하고, `false`인 브랜치의 코드를 번들에서 완전히 제거한다. 결과적으로 프로덕션 번들에 불필요한 코드가 포함되지 않으며, 피처 플래그별로 서로 다른 번들을 생성할 수 있다. + +### 코드 예시 + +```typescript +// src/main.tsx + +import { feature } from 'bun:bundle' // Bun 번들러 전용 모듈 + +// 번들 타임에 COORDINATOR_MODE가 false이면 +// 아래 require()와 모든 참조가 번들에서 제거됨 +const coordinatorModeModule = feature('COORDINATOR_MODE') + ? require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js') + : null + +// Kairos(어시스턴트) 모드도 동일 패턴 +const assistantModule = feature('KAIROS') + ? require('./assistant/index.js') as typeof import('./assistant/index.js') + : null +``` + +```typescript +// src/tools.ts - 도구 레지스트리에서도 동일 패턴 +const SleepTool = feature('PROACTIVE') || feature('KAIROS') + ? require('./tools/SleepTool/SleepTool.js').SleepTool + : null + +const cronTools = feature('AGENT_TRIGGERS') + ? [ + require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool, + // ... + ] + : [] +``` + +```typescript +// src/types/permissions.ts - 타입 레벨에서도 사용 +export const INTERNAL_PERMISSION_MODES = [ + ...EXTERNAL_PERMISSION_MODES, + ...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)), +] as const satisfies readonly PermissionMode[] +``` + +### 장점 + +- 피처별로 최소 크기의 번들 생성 가능 (번들 크기 감소) +- 사용하지 않는 기능의 코드가 런타임 메모리를 차지하지 않음 +- 피처 플래그가 코드 분기가 아닌 번들 분기이므로 런타임 오버헤드 없음 + +### 트레이드오프 + +- Bun 번들러에 특화된 API이므로 다른 런타임에서는 동작하지 않음 +- `require()` 방식의 동적 임포트로 인해 TypeScript 타입 추론이 복잡해짐 (`as typeof import(...)` 캐스팅 필요) +- 번들 타임 플래그와 런타임 GrowthBook 플래그를 혼동하지 않도록 주의 필요 + +--- + +## 3. 지연 로딩 패턴 (Lazy Loading for Circular Dependency Resolution) + +### 설명 + +순환 의존성(circular dependency)이 발생하는 모듈은 `require()`를 함수로 감싸 **실제 사용 시점까지 임포트를 지연**한다. 모듈 평가 순서 문제를 회피하면서도 타입 안전성을 유지한다. + +### 코드 예시 + +```typescript +// src/main.tsx + +// teammate.ts → AppState.tsx → ... → main.tsx 순환 의존성 방지 +const getTeammateUtils = () => + require('./utils/teammate.js') as typeof import('./utils/teammate.js') + +const getTeammatePromptAddendum = () => + require('./utils/swarm/teammatePromptAddendum.js') as typeof import('./utils/swarm/teammatePromptAddendum.js') + +const getTeammateModeSnapshot = () => + require('./utils/swarm/backends/teammateModeSnapshot.js') as typeof import('./utils/swarm/backends/teammateModeSnapshot.js') + +// 사용 시점에 호출 +const teammateUtils = getTeammateUtils() +await teammateUtils.someFunction() +``` + +### 장점 + +- 순환 의존성으로 인한 `undefined` 참조 오류를 런타임에서 방지 +- `as typeof import(...)` 패턴으로 완전한 타입 추론 유지 +- 해당 기능이 실제로 사용되지 않으면 모듈 로딩 비용 없음 + +### 트레이드오프 + +- 함수 호출마다 `require()` 캐시 조회가 발생 (Node.js는 캐시하므로 실제 비용은 미미) +- ESLint `no-require-imports` 규칙 비활성화 주석이 필요 +- IDE의 "Go to Definition" 기능이 `typeof import()` 패턴에서 완벽히 동작하지 않을 수 있음 + +--- + +## 4. 레지스트리 패턴 (Registry Pattern) + +### 설명 + +도구(Tool)와 커맨드(Command)를 중앙 레지스트리 함수(`getTools()`, `getCommands()`)에서 배열로 관리한다. 런타임 설정, 피처 플래그, 사용자 권한에 따라 활성화할 항목을 동적으로 결정한다. + +### 코드 예시 + +```typescript +// src/tools.ts - 도구 레지스트리 + +import { AgentTool } from './tools/AgentTool/AgentTool.js' +import { BashTool } from './tools/BashTool/BashTool.js' +import { FileEditTool } from './tools/FileEditTool/FileEditTool.js' +// ... 모든 도구 임포트 + +export function getTools(options: GetToolsOptions): Tools { + const tools: Tool[] = [ + new BashTool(), + new FileEditTool(), + new FileReadTool(), + new FileWriteTool(), + new GlobTool(), + new GrepTool(), + // ... + ] + + // 조건부 도구 추가 (피처 플래그, 사용자 타입 등) + if (REPLTool) tools.push(new REPLTool()) + if (SleepTool) tools.push(new SleepTool()) + if (cronTools.length) tools.push(...cronTools.map(T => new T())) + + return tools +} +``` + +```typescript +// src/Tool.ts - 도구 인터페이스 +export interface Tool { + name: string + description: string + inputSchema: ToolInputJSONSchema + call(input: unknown, context: ToolUseContext): Promise +} +``` + +### 장점 + +- 새 도구 추가 시 레지스트리에 한 줄만 추가하면 됨 (확장 용이) +- 도구 목록을 런타임에 동적으로 필터링 가능 +- LLM에 전달되는 도구 스키마와 실제 구현이 동일 객체에서 관리됨 + +### 트레이드오프 + +- 모든 도구 클래스가 `tools.ts`에 임포트되어 번들 크기가 증가 (Dead Code Elimination으로 완화) +- 도구 이름 충돌 시 런타임에서야 감지됨 (정적 분석 불가) + +--- + +## 5. 에이전트 루프 패턴 (Agent Loop Pattern) + +### 설명 + +`QueryEngine.ts`가 구현하는 핵심 제어 흐름으로, LLM 응답을 받아 도구 호출 요청을 추출하고, 도구를 실행하고, 결과를 다시 LLM에 피드백하는 루프를 반복한다. 루프는 도구 호출이 없는 최종 응답이 오거나 중단 신호를 받을 때 종료된다. + +### 코드 예시 + +``` +QueryEngine 실행 흐름: + +1. 사용자 입력 수신 + │ + ▼ +2. 시스템 프롬프트 + 대화 히스토리 + 사용 가능 도구 목록 구성 + │ + ▼ +3. LLM API 호출 (스트리밍) + │ + ├─ 텍스트 응답 → UI에 스트리밍 출력 + │ + └─ 도구 호출 요청 (ToolUseBlock) + │ + ▼ +4. CanUseTool 권한 검사 + ├─ deny → 거부 메시지 생성 + ├─ ask → 사용자 승인 UI 표시 + └─ allow → 도구 실행 + │ + ▼ +5. 도구 결과를 ToolResultBlock으로 변환 + │ + ▼ +6. 대화 히스토리에 추가 → 3번으로 돌아감 + │ + └─ 도구 호출 없음 → 루프 종료 +``` + +```typescript +// src/QueryEngine.ts - 루프 핵심 구조 (개념적 표현) +while (true) { + const response = await query(messages, tools, options) + + if (response.stop_reason === 'end_turn') break + if (response.stop_reason !== 'tool_use') break + + const toolResults = await Promise.all( + response.content + .filter(block => block.type === 'tool_use') + .map(block => executeTool(block, canUseTool)) + ) + + messages.push( + { role: 'assistant', content: response.content }, + { role: 'user', content: toolResults }, + ) +} +``` + +### 장점 + +- 임의 깊이의 도구 체인 실행 가능 (도구가 다른 도구를 호출하는 AgentTool 포함) +- 루프 각 단계가 독립적으로 취소 가능 (AbortController) +- 스트리밍과 루프 제어 로직이 분리되어 있어 테스트 용이 + +### 트레이드오프 + +- 루프 깊이에 따라 컨텍스트 윈도우가 소모됨 (컴팩트 메커니즘으로 완화) +- 무한 루프 방지를 위한 최대 반복 횟수 제한이 필요 +- 병렬 도구 실행 시 결과 순서 보장 로직이 복잡해짐 + +--- + +## 6. 스트리밍 파이프라인 패턴 (Streaming Pipeline Pattern) + +### 설명 + +Claude API의 SSE(Server-Sent Events) 스트림을 구독하여 토큰 단위로 응답을 수신하고, 즉시 Ink 컴포넌트를 통해 터미널에 렌더링한다. 파싱, 축적, 렌더링이 파이프라인으로 연결되어 첫 토큰부터 사용자에게 표시된다. + +### 코드 예시 + +``` +SSE 스트림 + │ + ▼ +event: message_delta ← HTTP 청크 수신 + │ + ▼ +파싱: content_block_delta ← text / tool_use 블록 분류 + │ + ├─ text_delta + │ │ + │ ▼ + │ React state 업데이트 + │ │ + │ ▼ + │ Ink Reconciler + │ │ + │ ▼ + │ 터미널 출력 (log-update 방식) + │ + └─ tool_use_delta + │ + ▼ + JSON 입력 축적 + │ + ▼ + 도구 실행 단계로 전달 +``` + +```typescript +// src/services/api/claude.ts (개념적 표현) +for await (const event of stream) { + if (event.type === 'content_block_delta') { + if (event.delta.type === 'text_delta') { + onText(event.delta.text) // UI 즉시 업데이트 + } else if (event.delta.type === 'input_json_delta') { + accumulateToolInput(event.delta.partial_json) + } + } + if (event.type === 'message_stop') { + break + } +} +``` + +### 장점 + +- 첫 번째 토큰 표시 시간(TTFT, Time to First Token)이 최소화됨 +- 긴 응답에서도 사용자가 즉각적인 피드백을 받음 +- 스트림 중단(Ctrl+C) 시 즉시 취소 가능 + +### 트레이드오프 + +- 스트리밍 도중 도구 입력 JSON이 불완전한 상태로 축적되므로 파싱 전략이 필요 +- 렌더링 빈도를 조절하지 않으면 터미널 깜박임(flicker) 발생 (`tengu_flicker` 이벤트로 추적) +- 네트워크 중단 시 부분 응답 처리 로직이 필요 + +--- + +## 7. 권한 인터셉터 패턴 (Permission Interceptor Pattern) + +### 설명 + +모든 도구 실행 전에 `CanUseTool` 함수가 권한을 검사하는 게이트 역할을 한다. 권한 모드(Permission Mode), 사용자 설정, 정책 제한을 조합하여 `allow`, `deny`, `ask` 중 하나를 반환한다. `ask`의 경우 UI를 통해 사용자에게 실시간으로 승인을 요청한다. + +### 코드 예시 + +```typescript +// src/hooks/useCanUseTool.ts (개념적 표현) +export type CanUseToolFn = ( + tool: Tool, + input: unknown, + context: ToolUseContext, +) => Promise + +// 권한 결정 흐름 +async function canUseTool(tool, input, context): Promise { + // 1. 정책 제한 확인 (관리자 강제 규칙) + if (isPolicyDenied(tool.name)) return { behavior: 'deny', reason: 'policy' } + + // 2. bypassPermissions 모드: 모두 허용 + if (permissionMode === 'bypassPermissions') return { behavior: 'allow' } + + // 3. 사용자 설정에서 규칙 조회 + const rule = findMatchingRule(tool.name, input, userSettings) + if (rule) return { behavior: rule.behavior } + + // 4. 기본값: 사용자에게 확인 요청 + return { behavior: 'ask' } +} +``` + +```typescript +// 에이전트 루프에서의 사용 +const permission = await canUseTool(tool, input, context) + +switch (permission.behavior) { + case 'allow': + return await tool.call(input, context) + case 'deny': + return createDenialResult(permission.reason) + case 'ask': + const userDecision = await showPermissionPrompt(tool, input) + if (userDecision === 'allow') return await tool.call(input, context) + return createDenialResult('user_rejected') +} +``` + +### 장점 + +- 권한 로직이 도구 구현과 완전히 분리됨 (SRP) +- 새로운 권한 모드 추가 시 도구 코드를 수정할 필요 없음 +- 영구/임시 승인 선택을 통해 사용자 설정에 자동 저장 가능 + +### 트레이드오프 + +- `ask` 모드에서 사용자 응답 대기로 인해 에이전트 루프가 블로킹됨 +- 권한 결정이 비동기이므로 병렬 도구 실행 시 여러 프롬프트가 동시에 나타날 수 있음 +- 권한 규칙 매칭 로직이 복잡해질수록 성능 영향 가능성 + +--- + +## 8. DeepImmutable 패턴 (Deep Immutable Type Pattern) + +### 설명 + +타입 레벨에서 중첩 객체까지 완전한 불변성을 강제하는 유틸리티 타입 패턴이다. `Readonly`는 최상위 속성만 보호하지만, `DeepImmutable`는 모든 중첩 객체와 배열까지 `readonly`로 만들어 의도치 않은 변경을 컴파일 타임에 차단한다. + +### 코드 예시 + +```typescript +// src/utils/ (개념적 구현) +type DeepImmutable = + T extends (infer U)[] + ? ReadonlyArray> + : T extends object + ? { readonly [K in keyof T]: DeepImmutable } + : T + +// 사용 예시: AppState를 불변으로 전달 +function renderUI(state: DeepImmutable): JSX.Element { + // state.messages.push(...) → 컴파일 에러 (readonly array) + // state.config.model = '...' → 컴파일 에러 (readonly property) + return +} +``` + +```typescript +// src/services/analytics/index.ts 의 마커 타입도 유사 원리 +// never 타입을 사용해 "이 타입은 직접 값을 가질 수 없다"는 불변 제약 표현 +export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never +``` + +### 장점 + +- 전역 상태가 컴포넌트에서 변경되는 것을 컴파일 타임에 방지 +- `Object.freeze()`와 달리 런타임 오버헤드 없음 (타입 전용) +- 함수형 프로그래밍 스타일(불변 업데이트)을 타입 시스템이 강제 + +### 트레이드오프 + +- 복잡한 재귀 타입으로 인해 TypeScript 컴파일 시간이 증가할 수 있음 +- 의도적으로 변경이 필요한 경우 `as Mutable` 캐스팅이 필요하여 가독성이 저하됨 +- 깊은 중첩 타입에서 타입 에러 메시지가 읽기 어려워짐 + +--- + +## 패턴 간 상호작용 + +``` +병렬 Prefetch (패턴 1) + └─ 스타트업 완료 + │ + ▼ +Dead Code Elimination (패턴 2) + └─ 피처별 도구 포함/제외 + │ + ▼ +레지스트리 패턴 (패턴 4) + └─ 활성 도구 목록 구성 + │ + ▼ +에이전트 루프 (패턴 5) + └─ LLM ↔ 도구 반복 + │ + ┌──────┴──────┐ + │ │ +스트리밍 파이프라인 (패턴 6) 권한 인터셉터 (패턴 7) + └─ UI 실시간 업데이트 └─ 도구 실행 전 승인 게이트 +``` + +각 패턴은 독립적으로 동작하지만, Claude Code의 전체 실행 흐름에서 위와 같이 순차적으로 조합된다. 지연 로딩(패턴 3)과 DeepImmutable(패턴 8)은 횡단 관심사(cross-cutting concern)로서 여러 패턴에 걸쳐 적용된다. diff --git a/docs/ko/appendix/file-map.md b/docs/ko/appendix/file-map.md new file mode 100644 index 0000000..cad39c7 --- /dev/null +++ b/docs/ko/appendix/file-map.md @@ -0,0 +1,282 @@ +# 주요 파일 경로 인덱스 + +> Claude Code `src/` 디렉토리의 핵심 파일 및 서브디렉토리에 대한 한국어 설명과 문서 역참조 테이블이다. + +--- + +## src/ 디렉토리 트리 + +``` +src/ +├── main.tsx # CLI 진입점. 스타트업 시퀀스, Commander.js 명령어 등록 +├── Tool.ts # Tool 인터페이스 타입 정의 +├── tools.ts # 도구 레지스트리. getTools()로 활성 도구 배열 반환 +├── commands.ts # 슬래시 커맨드 등록 및 라우팅 +├── QueryEngine.ts # 에이전트 루프 핵심 엔진. LLM → Tool → LLM 반복 +├── query.ts # 단일 LLM API 쿼리 실행 함수 +├── context.ts # 시스템/사용자 컨텍스트 구성 +├── history.ts # 대화 히스토리 관리 +├── cost-tracker.ts # API 호출 비용 추적 +├── costHook.ts # 비용 훅 바인딩 +├── setup.ts # 초기 설정 마법사 +├── ink.ts # Ink 렌더러 공개 API 재내보내기 +├── tasks.ts # 태스크 시스템 공개 API +├── Task.ts # Task 타입 정의 +├── replLauncher.tsx # 인터랙티브 REPL 진입점 +├── interactiveHelpers.tsx # 대화 UI 헬퍼 함수 +├── dialogLaunchers.tsx # 모달 다이얼로그 실행기 +├── projectOnboardingState.ts # 프로젝트 온보딩 상태 관리 +│ +├── assistant/ # Kairos(어시스턴트) 모드 구현 +│ ├── index.ts # 어시스턴트 모드 진입점 +│ ├── gate.ts # Kairos 피처 게이트 체크 +│ └── sessionHistory.ts # 어시스턴트 세션 히스토리 +│ +├── bootstrap/ +│ └── state.ts # 앱 전역 상태 (세션 ID, 권한 수락 여부 등) +│ +├── bridge/ # 외부 환경 브릿지 (Chrome, 원격 세션) +│ +├── buddy/ # 버디(짝 프로그래밍) 기능 +│ +├── cli/ # CLI 서브커맨드 구현 +│ +├── commands.ts # 슬래시 커맨드 정의 +│ +├── components/ # Ink React 컴포넌트 +│ └── Spinner.ts # 로딩 스피너 컴포넌트 +│ +├── constants/ # 상수 정의 +│ ├── keys.ts # GrowthBook 클라이언트 키 등 +│ ├── oauth.ts # OAuth 엔드포인트 설정 +│ ├── product.ts # 제품명, 원격 세션 URL +│ ├── querySource.ts # 쿼리 소스 타입 +│ ├── tools.ts # 도구명 상수 +│ └── xml.ts # XML 태그 상수 +│ +├── context/ # React Context 프로바이더 +│ ├── fpsMetrics.tsx # FPS 성능 지표 컨텍스트 +│ ├── mailbox.tsx # 메시지 메일박스 컨텍스트 +│ ├── modalContext.tsx # 모달 상태 컨텍스트 +│ ├── notifications.tsx # 알림 컨텍스트 +│ ├── overlayContext.tsx # UI 오버레이 컨텍스트 +│ ├── promptOverlayContext.tsx # 프롬프트 오버레이 컨텍스트 +│ ├── QueuedMessageContext.tsx # 대기 메시지 큐 컨텍스트 +│ ├── stats.tsx # 통계 컨텍스트 (토큰, 비용) +│ └── voice.tsx # 음성 입력 컨텍스트 +│ +├── coordinator/ +│ └── coordinatorMode.ts # 코디네이터 모드 활성화/도구 필터링 로직 +│ +├── entrypoints/ # 진입점별 초기화 로직 +│ ├── agentSdkTypes.ts # Agent SDK 타입 정의 +│ └── init.ts # 공통 초기화 (텔레메트리, 신뢰 다이얼로그) +│ +├── hooks/ # React 커스텀 훅 +│ └── useCanUseTool.ts # 도구 실행 권한 검사 훅 +│ +├── ink/ # Ink 터미널 UI 엔진 (커스텀 구현) +│ ├── ink.tsx # Ink 코어 렌더러 +│ ├── reconciler.ts # React Reconciler 구현 +│ ├── renderer.ts # 터미널 렌더링 파이프라인 +│ ├── dom.ts # 가상 DOM 노드 타입 +│ ├── output.ts # 출력 버퍼 관리 +│ ├── root.ts # 루트 컴포넌트 +│ ├── screen.ts # 터미널 화면 관리 +│ ├── terminal.ts # 터미널 인터페이스 +│ ├── termio.ts # 터미널 I/O 제어 +│ ├── parse-keypress.ts # 키 입력 파싱 +│ ├── styles.ts # 레이아웃 스타일 시스템 +│ ├── wrap-text.ts # 텍스트 줄바꿈 +│ ├── colorize.ts # ANSI 색상 처리 +│ ├── optimizer.ts # 렌더링 최적화 +│ ├── selection.ts # 텍스트 선택 +│ ├── searchHighlight.ts # 검색어 하이라이트 +│ └── vim/ # Vim 키바인딩 지원 +│ +├── keybindings/ # 키 바인딩 시스템 +│ ├── schema.ts # 키 바인딩 스키마 +│ ├── match.ts # 키 입력 매칭 로직 +│ ├── resolver.ts # 바인딩 우선순위 해석 +│ └── loadUserBindings.ts # 사용자 정의 바인딩 로드 +│ +├── memdir/ # CLAUDE.md 메모리 디렉토리 시스템 +│ ├── memdir.ts # 메모리 프롬프트 로드 +│ └── paths.ts # 메모리 파일 경로 관리 +│ +├── migrations/ # 설정 마이그레이션 스크립트 +│ ├── migrateLegacyOpusToCurrent.ts +│ ├── migrateSonnet45ToSonnet46.ts +│ └── ... # 버전별 마이그레이션 +│ +├── native-ts/ # Node.js 네이티브 바인딩 +│ +├── plugins/ # 플러그인 시스템 +│ └── bundled/ # 기본 내장 플러그인 +│ └── index.ts # 빌트인 플러그인 초기화 +│ +├── query/ # 쿼리 서브시스템 +│ +├── remote/ # 원격 세션 지원 +│ +├── schemas/ # Zod 스키마 정의 +│ +├── screens/ # 전체 화면 UI 컴포넌트 +│ +├── server/ # 로컬 서버 (SDK 모드) +│ +├── services/ # 외부 서비스 통합 +│ ├── analytics/ # 텔레메트리 시스템 +│ │ ├── index.ts # 공개 API (logEvent, attachAnalyticsSink) +│ │ ├── config.ts # 분석 비활성화 조건 +│ │ ├── sink.ts # 이벤트 라우팅 레이어 +│ │ ├── datadog.ts # Datadog HTTP Intake 백엔드 +│ │ ├── growthbook.ts # GrowthBook 피처 플래그 +│ │ ├── firstPartyEventLogger.ts # OpenTelemetry 1P 로거 +│ │ ├── firstPartyEventLoggingExporter.ts # 1P HTTP 익스포터 +│ │ ├── metadata.ts # 이벤트 메타데이터 풍부화 +│ │ └── sinkKillswitch.ts # 싱크 킬스위치 +│ │ +│ ├── api/ # Anthropic API 클라이언트 +│ │ ├── claude.ts # Claude API 호출 +│ │ ├── bootstrap.ts # 부트스트랩 데이터 페치 +│ │ ├── errors.ts # API 에러 분류 +│ │ ├── filesApi.ts # 파일 API +│ │ └── logging.ts # API 호출 로깅 +│ │ +│ ├── mcp/ # MCP 클라이언트 +│ │ ├── client.ts # MCP 서버 연결 관리 +│ │ ├── types.ts # MCP 타입 정의 +│ │ └── officialRegistry.ts # 공식 MCP 서버 레지스트리 +│ │ +│ ├── oauth/ # OAuth 클라이언트 +│ │ └── client.ts # OAuth 토큰 관리 +│ │ +│ ├── policyLimits/ # 정책 제한 서비스 +│ └── remoteManagedSettings/ # 원격 관리 설정 +│ +├── skills/ # 스킬 시스템 +│ └── bundled/ # 기본 내장 스킬 +│ └── index.ts # 빌트인 스킬 초기화 +│ +├── state/ # 앱 상태 관리 +│ └── AppState.ts # 전역 앱 상태 타입 +│ +├── tasks/ # 태스크 시스템 +│ ├── types.ts # 태스크 타입 정의 +│ ├── LocalMainSessionTask.ts # 로컬 세션 태스크 +│ └── stopTask.ts # 태스크 중지 로직 +│ +├── tools/ # 도구 구현체 +│ ├── AgentTool/ # 서브에이전트 실행 도구 +│ ├── BashTool/ # 배시 명령어 실행 +│ ├── FileEditTool/ # 파일 편집 (Edit) +│ ├── FileReadTool/ # 파일 읽기 (Read) +│ ├── FileWriteTool/ # 파일 쓰기 (Write) +│ ├── GlobTool/ # 파일 패턴 검색 +│ ├── GrepTool/ # 내용 검색 +│ ├── LSPTool/ # LSP 코드 지능 +│ ├── MCPTool/ # MCP 프록시 도구 +│ ├── NotebookEditTool/ # Jupyter 노트북 편집 +│ ├── SkillTool/ # 스킬 실행 래퍼 +│ ├── TodoWriteTool/ # 할 일 목록 관리 +│ ├── WebFetchTool/ # URL 페치 +│ ├── WebSearchTool/ # 웹 검색 +│ ├── EnterWorktreeTool/ # Git Worktree 진입 +│ ├── ExitWorktreeTool/ # Git Worktree 종료 +│ ├── SyntheticOutputTool/ # 합성 출력 도구 (SDK) +│ └── TaskStopTool/ # 태스크 종료 도구 +│ +├── types/ # 공유 타입 정의 +│ ├── command.ts # 커맨드 타입 +│ ├── hooks.ts # 훅 타입 +│ ├── ids.ts # UUID 타입 별칭 +│ ├── logs.ts # 로그 옵션 타입 +│ ├── permissions.ts # 권한 모드/결과 타입 +│ ├── plugin.ts # 플러그인 타입 +│ ├── textInputTypes.ts # 텍스트 입력 타입 +│ └── generated/ # protobuf 생성 타입 +│ +├── upstreamproxy/ # 업스트림 프록시 지원 +│ ├── upstreamproxy.ts # 프록시 설정 파싱 +│ └── relay.ts # 프록시 릴레이 +│ +├── utils/ # 유틸리티 함수 +│ ├── auth.ts # 인증 (OAuth, 구독 타입) +│ ├── config.ts # 전역 설정 읽기/쓰기 +│ ├── context.ts # 컨텍스트 윈도우 계산 +│ ├── cwd.ts # 현재 작업 디렉토리 +│ ├── debug.ts # 디버그 로깅 +│ ├── env.ts # 환경 변수 헬퍼 +│ ├── envUtils.ts # 환경 유틸리티 (isEnvTruthy 등) +│ ├── errors.ts # 에러 처리 유틸리티 +│ ├── git.ts # Git 유틸리티 +│ ├── log.ts # 로그 함수 +│ ├── messages.ts # 메시지 생성 유틸리티 +│ ├── model/ # 모델 관련 유틸리티 +│ │ ├── model.ts # 모델 이름 처리 +│ │ ├── providers.ts # API 제공자 감지 +│ │ └── deprecation.ts # 모델 지원 종료 경고 +│ ├── permissions/ # 권한 검사 로직 +│ ├── platform.ts # 플랫폼 감지 (WSL, distro) +│ ├── secureStorage/ # 키체인/보안 스토리지 +│ │ └── keychainPrefetch.ts # 스타트업 키체인 프리페치 +│ ├── settings/ # 설정 시스템 +│ │ ├── mdm/ # MDM 설정 로드 +│ │ │ └── rawRead.ts # MDM 원시 읽기 (병렬 실행) +│ │ └── changeDetector.ts # 설정 변경 감지 +│ ├── signal.ts # 신호 유틸리티 +│ ├── slowOperations.ts # JSON 파싱/직렬화 (성능 주의 표시) +│ ├── startupProfiler.ts # 스타트업 성능 프로파일러 +│ ├── swarm/ # 에이전트 스웜 유틸리티 +│ ├── teammate.ts # 팀메이트 에이전트 유틸리티 +│ └── thinking.ts # 확장 사고(extended thinking) 설정 +│ +├── vim/ # Vim 키바인딩 엔진 +│ ├── motions.ts # 이동 명령 +│ ├── operators.ts # 편집 연산자 +│ ├── textObjects.ts # 텍스트 객체 +│ ├── transitions.ts # 모드 전환 +│ └── types.ts # Vim 타입 정의 +│ +└── voice/ # 음성 입력 지원 +``` + +--- + +## 파일 → 문서 역참조 테이블 + +| 소스파일 | 관련 문서 | +|---------|----------| +| `src/services/analytics/index.ts` | [텔레메트리 시스템](../level-3-internals/telemetry.md) §2 | +| `src/services/analytics/config.ts` | [텔레메트리 시스템](../level-3-internals/telemetry.md) §3 | +| `src/services/analytics/datadog.ts` | [텔레메트리 시스템](../level-3-internals/telemetry.md) §4 | +| `src/services/analytics/growthbook.ts` | [텔레메트리 시스템](../level-3-internals/telemetry.md) §6, [용어집](./glossary.md#피처-플래그--ab-테스트) | +| `src/services/analytics/sink.ts` | [텔레메트리 시스템](../level-3-internals/telemetry.md) §7 | +| `src/services/analytics/firstPartyEventLogger.ts` | [텔레메트리 시스템](../level-3-internals/telemetry.md) §5 | +| `src/services/analytics/firstPartyEventLoggingExporter.ts` | [텔레메트리 시스템](../level-3-internals/telemetry.md) §5.3 | +| `src/main.tsx` | [설계 패턴](./design-patterns.md) §1, §2, §3 | +| `src/tools.ts` | [설계 패턴](./design-patterns.md) §4, [용어집](./glossary.md#핵심-아키텍처) | +| `src/Tool.ts` | [용어집](./glossary.md#핵심-아키텍처) | +| `src/QueryEngine.ts` | [설계 패턴](./design-patterns.md) §5, [용어집](./glossary.md#핵심-아키텍처) | +| `src/types/permissions.ts` | [용어집](./glossary.md#권한-시스템), [설계 패턴](./design-patterns.md) §7 | +| `src/coordinator/coordinatorMode.ts` | [용어집](./glossary.md#핵심-아키텍처) | +| `src/utils/secureStorage/keychainPrefetch.ts` | [설계 패턴](./design-patterns.md) §1 | +| `src/utils/settings/mdm/rawRead.ts` | [설계 패턴](./design-patterns.md) §1 | +| `src/ink/` | [용어집](./glossary.md#uirendering), [설계 패턴](./design-patterns.md) §6 | +| `src/services/mcp/` | [용어집](./glossary.md#외부-서비스--프로토콜) | +| `src/utils/teammate.ts` | [용어집](./glossary.md#핵심-아키텍처) | +| `src/memdir/memdir.ts` | [용어집](./glossary.md#컨텍스트--메모리) | +| `src/services/analytics/sinkKillswitch.ts` | [용어집](./glossary.md#분석--모니터링) | + +--- + +## 주요 진입점 요약 + +| 진입점 | 파일 | 설명 | +|--------|------|------| +| CLI 메인 | `src/main.tsx` | Commander.js 기반 CLI 명령어 파싱 및 스타트업 | +| REPL | `src/replLauncher.tsx` | 인터랙티브 대화 세션 진입 | +| SDK 서버 | `src/server/` | Agent SDK 연동 서버 모드 | +| 어시스턴트 모드 | `src/assistant/index.ts` | Kairos 어시스턴트 모드 (피처 플래그 제어) | +| 초기화 | `src/entrypoints/init.ts` | 텔레메트리, 신뢰 다이얼로그, 공통 초기화 | diff --git a/docs/ko/appendix/glossary.md b/docs/ko/appendix/glossary.md new file mode 100644 index 0000000..2a01c83 --- /dev/null +++ b/docs/ko/appendix/glossary.md @@ -0,0 +1,160 @@ +# 한영 용어 대조표 + +> Claude Code 소스코드에서 사용되는 주요 기술 용어의 한국어 번역, 영문 원어, 정의, 소스코드 위치를 정리한 참조 테이블이다. + +--- + +## 핵심 아키텍처 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 도구 | Tool | LLM이 호출할 수 있는 기능 단위. `name`, `description`, `inputSchema`, `call()` 메서드를 가진다 | `src/Tool.ts` | +| 도구 등록소 | Tool Registry | 활성화된 도구 집합을 중앙에서 관리하는 패턴. `getTools()`가 도구 배열을 반환 | `src/tools.ts` | +| 에이전트 | Agent | 독립적인 LLM 루프를 실행하는 서브프로세스. AgentTool을 통해 생성 | `src/tools/AgentTool/` | +| 서브에이전트 | Sub-agent | 코디네이터 또는 부모 에이전트로부터 위임받은 작업을 독립 실행하는 에이전트 | `src/tools/AgentTool/AgentTool.ts` | +| 질의 엔진 | QueryEngine | LLM API 호출, 도구 실행, 대화 루프를 조율하는 중앙 엔진 | `src/QueryEngine.ts` | +| 쿼리 | Query | 단일 LLM API 호출 + 응답 처리 사이클 | `src/query.ts` | +| 에이전트 루프 | Agent Loop | LLM 응답 → 도구 실행 → 결과 피드백 → LLM 응답을 반복하는 제어 흐름 | `src/QueryEngine.ts` | +| 코디네이터 모드 | Coordinator Mode | 여러 서브에이전트를 조율하는 상위 에이전트 실행 모드 | `src/coordinator/coordinatorMode.ts` | +| 팀메이트 | Teammate | 스웜(swarm) 아키텍처에서 협력하는 에이전트 인스턴스 | `src/utils/teammate.ts` | + +--- + +## 권한 시스템 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 권한 모드 | Permission Mode | 도구 실행 시 사용자 승인 요구 수준. `default`, `acceptEdits`, `bypassPermissions`, `plan`, `dontAsk`, `auto` | `src/types/permissions.ts` | +| 권한 결과 | PermissionResult | 도구 실행 전 권한 검사 결과. `allow`, `deny`, `ask` | `src/types/permissions.ts` | +| 권한 인터셉터 | Permission Interceptor | 도구 실행 전 승인 여부를 결정하는 게이트 로직 | `src/hooks/useCanUseTool.ts` | +| 바이패스 권한 | Bypass Permissions | 모든 도구 실행을 자동 승인하는 최고 신뢰 모드 | `src/types/permissions.ts` | +| 계획 모드 | Plan Mode | 파일 수정 없이 계획만 수립하는 읽기 전용 권한 모드 | `src/tools/EnterPlanModeTool/` | +| 정책 제한 | Policy Limits | 관리자 또는 MDM을 통해 부과되는 외부 제약 | `src/services/policyLimits/` | + +--- + +## UI/렌더링 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 인크 컴포넌트 | Ink Component | React 기반 터미널 UI 컴포넌트. Ink 라이브러리의 커스텀 구현 | `src/ink/` | +| 스트리밍 | Streaming | LLM 응답을 토큰 단위로 실시간 수신·렌더링하는 기법 (SSE 기반) | `src/services/api/claude.ts` | +| SSE | Server-Sent Events | LLM API가 스트리밍 응답을 전송하는 HTTP 프로토콜 | `src/services/api/` | +| REPL | Read-Eval-Print Loop | 대화형 입력-처리-출력 반복 루프. 인터랙티브 세션 진입점 | `src/replLauncher.tsx` | +| 오버레이 | Overlay | 메인 UI 위에 표시되는 모달 또는 프롬프트 레이어 | `src/context/overlayContext.tsx` | + +--- + +## 명령어 시스템 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 슬래시 커맨드 | Slash Command | `/` 접두사로 시작하는 사용자 입력 명령어 | `src/commands.ts` | +| 스킬 | Skill | 슬래시 커맨드 형태로 등록되는 재사용 가능한 작업 템플릿 | `src/skills/` | +| 스킬 도구 | Skill Tool | 스킬을 LLM 도구로 노출하는 래퍼 | `src/tools/SkillTool/SkillTool.ts` | +| 빌트인 플러그인 | Bundled Plugin | 기본 내장된 플러그인. 동적 로딩 없이 번들에 포함 | `src/plugins/bundled/` | +| 플러그인 | Plugin | 기능을 확장하는 모듈 단위. 도구, 커맨드, 스킬을 제공 | `src/types/plugin.ts` | + +--- + +## 컨텍스트 & 메모리 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 컨텍스트 윈도우 | Context Window | LLM이 한 번에 처리할 수 있는 최대 토큰 수 | `src/utils/context.ts` | +| 토큰 예산 | Token Budget | 단일 쿼리 또는 세션에서 사용 가능한 최대 토큰 할당량 | `src/utils/thinking.ts` | +| 컴팩트 | Compact | 긴 대화 히스토리를 요약하여 컨텍스트 윈도우를 확보하는 작업 | `src/QueryEngine.ts` | +| 메모리 프롬프트 | Memory Prompt | `CLAUDE.md` 등 파일에서 로드되는 지속적 지시사항 | `src/memdir/memdir.ts` | +| 세션 | Session | UUID로 식별되는 단일 대화 세션. 트랜스크립트 저장 단위 | `src/bootstrap/state.ts` | +| 트랜스크립트 | Transcript | 세션 내 모든 메시지와 도구 결과의 기록 | `src/utils/sessionStorage.ts` | + +--- + +## 로딩 & 최적화 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 지연 로딩 | Lazy Loading | 모듈을 즉시 임포트하지 않고 필요 시점에 `require()`로 동적 로드하는 패턴 | `src/main.tsx:69-82` | +| 병렬 프리페치 | Parallel Prefetch | 독립적인 초기화 작업을 동시에 실행하는 스타트업 최적화 기법 | `src/main.tsx:16-20` | +| 데드 코드 제거 | Dead Code Elimination | `feature()` 플래그 기반으로 번들 타임에 미사용 코드를 제거하는 최적화 | `src/tools.ts:14-52` | +| 메모이제이션 | Memoization | 동일 인자에 대한 함수 결과를 캐시하여 재계산을 방지하는 기법 | `src/services/analytics/datadog.ts` | + +--- + +## 외부 서비스 & 프로토콜 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| MCP | Model Context Protocol | 외부 서버가 도구와 리소스를 Claude에 제공하는 표준 프로토콜 | `src/services/mcp/` | +| LSP | Language Server Protocol | 코드 지능(정의 이동, 참조 검색 등)을 제공하는 표준 프로토콜 | `src/tools/LSPTool/` | +| OAuth | OAuth 2.0 | Claude.ai 계정 인증에 사용하는 표준 인증 위임 프로토콜 | `src/services/oauth/` | +| JWT | JSON Web Token | OAuth 인증 흐름에서 사용하는 토큰 포맷 | `src/utils/auth.ts` | +| gRPC | gRPC | 1P 이벤트 로깅 익스포터가 사용하는 원격 프로시저 호출 프로토콜 (HTTP/2 기반) | `src/services/analytics/firstPartyEventLoggingExporter.ts` | +| OpenTelemetry | OpenTelemetry | 1P 이벤트 로깅에 사용하는 벤더 중립적 관측성 프레임워크 | `src/services/analytics/firstPartyEventLogger.ts` | + +--- + +## 보안 & 인증 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 키체인 | Keychain | macOS 보안 자격증명 저장소. OAuth 토큰과 API 키를 보관 | `src/utils/secureStorage/` | +| 키체인 프리페치 | Keychain Prefetch | 스타트업 레이턴시 감소를 위해 키체인 읽기를 병렬로 미리 실행 | `src/utils/secureStorage/keychainPrefetch.ts` | +| MDM | Mobile Device Management | 기업 환경에서 Claude Code 설정을 중앙 관리하는 시스템 | `src/utils/settings/mdm/` | +| 신뢰 다이얼로그 | Trust Dialog | 새 프로젝트 디렉토리에 대한 사용자 신뢰 확인 UI | `src/bootstrap/state.ts` | + +--- + +## 피처 플래그 & A/B 테스트 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| GrowthBook | GrowthBook | 피처 플래그와 A/B 테스트를 관리하는 외부 서비스 | `src/services/analytics/growthbook.ts` | +| 피처 플래그 | Feature Flag | 런타임에서 기능을 켜고 끄는 제어 메커니즘 | `src/services/analytics/growthbook.ts` | +| 피처 게이트 | Feature Gate | GrowthBook의 불리언 피처 플래그. `checkStatsigFeatureGate_CACHED_MAY_BE_STALE()` | `src/services/analytics/growthbook.ts` | +| 동적 설정 | Dynamic Config | GrowthBook에서 런타임에 제공되는 구조화된 설정값 | `src/services/analytics/growthbook.ts` | +| 번들 피처 | Bundle Feature | 번들 타임에 평가되는 `feature()` 플래그 (Bun 번들러 전용) | `src/types/permissions.ts` | + +--- + +## 런타임 & 빌드 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| Bun | Bun | Claude Code의 JavaScript 런타임 및 번들러 | `bun.lock`, `package.json` | +| `bun:bundle` | bun:bundle | Bun 번들러가 제공하는 특수 모듈. `feature()` 함수로 번들 타임 조건 분기 | `src/main.tsx:21` | +| Commander.js | Commander.js | CLI 인자 파싱에 사용하는 라이브러리 (`@commander-js/extra-typings`) | `src/main.tsx:22` | +| Zod | Zod | 스키마 검증 라이브러리. 도구 입력 스키마와 설정 파일 검증에 사용 | `src/Tool.ts` | + +--- + +## 데이터 & 타입 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| DeepImmutable | DeepImmutable | 타입 레벨에서 중첩 객체까지 불변성을 강제하는 유틸리티 타입 | `src/utils/` | +| 마커 타입 | Marker Type | 실제 값을 담지 않고 개발자 의도를 타입 시스템에 문서화하는 `never` 기반 타입 | `src/services/analytics/index.ts:19` | +| PII 태깅 | PII Tagging | 개인 식별 정보를 `_PROTO_*` 키로 표시하여 특권 스토리지로만 라우팅하는 패턴 | `src/services/analytics/index.ts:27` | + +--- + +## 브릿지 & 통합 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 브릿지 | Bridge | Claude Code와 외부 환경(Chrome, 원격 세션)을 연결하는 통신 레이어 | `src/bridge/` | +| 훅 | Hook | 특정 이벤트 시점에 실행되는 사용자 정의 스크립트 (Pre/PostToolUse 등) | `src/types/hooks.ts` | +| 워크트리 | Worktree | Git worktree를 활용한 병렬 개발 환경 관리 기능 | `src/tools/EnterWorktreeTool/` | +| 업스트림 프록시 | Upstream Proxy | 기업 네트워크에서 Claude API 요청을 중계하는 프록시 설정 | `src/upstreamproxy/` | + +--- + +## 분석 & 모니터링 + +| 한국어 | English | 정의 | 소스코드 위치 | +|--------|---------|------|--------------| +| 분석 싱크 | Analytics Sink | 이벤트를 실제 백엔드로 전달하는 라우팅 레이어 | `src/services/analytics/sink.ts` | +| 이벤트 샘플링 | Event Sampling | 고빈도 이벤트의 일부만 기록하여 스토리지 비용을 절감하는 기법 | `src/services/analytics/firstPartyEventLogger.ts` | +| 사용자 버킷 | User Bucket | 사용자 ID 해시 기반 0~29 정수. 고유 사용자 수를 PII 없이 근사하는 데 사용 | `src/services/analytics/datadog.ts` | +| 킬스위치 | Killswitch | 런타임에서 특정 분석 싱크를 즉시 비활성화하는 메커니즘 | `src/services/analytics/sinkKillswitch.ts` | +| 텔레메트리 | Telemetry | 앱 동작에 관한 사용 데이터를 자동 수집·전송하는 시스템 전체 | `src/services/analytics/` | diff --git a/docs/ko/level-1-overview/architecture.md b/docs/ko/level-1-overview/architecture.md new file mode 100644 index 0000000..b5bbc84 --- /dev/null +++ b/docs/ko/level-1-overview/architecture.md @@ -0,0 +1,209 @@ +# 아키텍처 개요 + +> **레벨**: 입문 (Level 1) | **대상**: Claude Code CLI의 전체 구조를 처음 파악하려는 개발자 + +--- + +## 1. 기술 스택 + +| 계층 | 기술 | +|------|------| +| **Runtime** (런타임) | [Bun](https://bun.sh) — Node.js 호환 고성능 JS 런타임 | +| **Language** (언어) | TypeScript strict 모드 (`tsconfig` `strict: true`) | +| **UI** | [React](https://react.dev) + [Ink](https://github.com/vadimdemedes/ink) — 터미널 UI를 React 컴포넌트로 렌더링 | +| **Schema** (스키마 검증) | [Zod](https://zod.dev) v4 — 런타임 타입 검증 | +| **CLI** | [Commander.js](https://github.com/tj/commander.js) (`@commander-js/extra-typings`) | +| **Protocols** (프로토콜) | [MCP SDK](https://modelcontextprotocol.io) (Model Context Protocol), LSP (Language Server Protocol) | +| **Search** (검색) | [ripgrep](https://github.com/BurntSushi/ripgrep) — 고속 파일 내용 검색 | +| **Auth** (인증) | OAuth 2.0, JWT, macOS Keychain | +| **Telemetry** (원격 측정) | [OpenTelemetry](https://opentelemetry.io) + gRPC | +| **Feature Flags** (기능 플래그) | [GrowthBook](https://www.growthbook.io) + `bun:bundle` (`feature()`) | + +--- + +## 2. 고수준 아키텍처 + +아래 다이어그램은 사용자 입력이 처리되는 6개의 핵심 계층을 보여준다. + +```mermaid +graph TD + A["CLI Entry
main.tsx"] --> B["Command Parser
commands.ts"] + A --> C["QueryEngine
QueryEngine.ts"] + B --> C + C --> D["Tool System
Tool.ts / tools.ts"] + C --> E["Agent Loop
query.ts"] + D --> E + E --> F["Ink UI
components/"] + F --> A + + style A fill:#4A90D9,color:#fff + style B fill:#7B68EE,color:#fff + style C fill:#50C878,color:#fff + style D fill:#FF7F50,color:#fff + style E fill:#FFD700,color:#000 + style F fill:#DDA0DD,color:#000 +``` + +| 계층 | 역할 | +|------|------| +| CLI Entry | 프로세스 진입점, 사이드이펙트 초기화, 옵션 파싱 | +| Command Parser | 슬래시 커맨드(`/commit`, `/doctor` 등) 등록 및 라우팅 | +| QueryEngine | 세션 상태, 모델 설정, 권한 컨텍스트를 통합 관리 | +| Tool System | 각 Tool의 입력 스키마 정의 및 실행 위임 | +| Agent Loop | Claude API 호출 → 도구 실행 → 결과 반환 반복 루프 | +| Ink UI | React 컴포넌트로 터미널에 실시간 렌더링 | + +--- + +## 3. 디렉토리 ↔ 레이어 매핑 + +`src/` 하위 35개 서브디렉토리와 아키텍처 계층의 대응 관계는 다음과 같다. + +| 디렉토리 | 아키텍처 계층 | 설명 | +|----------|--------------|------| +| `entrypoints/` | CLI Entry | SDK 진입점, MCP 진입점, 초기화(`init.ts`) | +| `cli/` | CLI Entry | CLI 전용 유틸리티 | +| `bootstrap/` | CLI Entry | 세션 전역 상태(`state.ts`) | +| `commands/` | Command Parser | 슬래시 커맨드 구현체 모음 | +| `QueryEngine.ts` | QueryEngine | 쿼리 설정 타입 및 실행 엔진 | +| `query/` | QueryEngine | API 호출 및 스트리밍 처리 | +| `tools/` | Tool System | 개별 Tool 구현체 디렉토리 | +| `Tool.ts` / `tools.ts` | Tool System | Tool 타입 정의 및 레지스트리 | +| `components/` | Ink UI | React/Ink 터미널 UI 컴포넌트 | +| `ink/` | Ink UI | Ink 렌더링 헬퍼 및 터미널 I/O | +| `screens/` | Ink UI | 전체 화면 단위 뷰 | +| `hooks/` | Ink UI / QueryEngine | React 훅 및 권한 훅 | +| `state/` | QueryEngine | AppState 스토어, 상태 변환 | +| `context/` | QueryEngine | 세션 컨텍스트(알림, 통계 등) | +| `services/` | 공통 서비스 | API 클라이언트, MCP, LSP, 분석, 플러그인 | +| `utils/` | 공통 유틸 | 인증, 설정, 파일, 모델, 권한 등 | +| `types/` | 공통 타입 | 공유 TypeScript 인터페이스 | +| `schemas/` | 공통 타입 | Zod 스키마 정의 | +| `constants/` | 공통 상수 | 제품 상수, OAuth 설정 등 | +| `skills/` | Tool System | 번들 스킬 구현 | +| `plugins/` | 공통 서비스 | 번들 플러그인 시스템 | +| `coordinator/` | Agent Loop | 다중 에이전트 코디네이터 | +| `assistant/` | Agent Loop | KAIROS 어시스턴트 모드 | +| `tasks/` | Agent Loop | 백그라운드 태스크 관리 | +| `server/` | 네트워크 | Direct Connect 세션 서버 | +| `remote/` | 네트워크 | 원격 세션 관리 | +| `bridge/` | 네트워크 | REPL 브릿지 (원격 제어) | +| `memdir/` | QueryEngine | 메모리(CLAUDE.md) 로딩 | +| `migrations/` | CLI Entry | 설정 마이그레이션 스크립트 | +| `keybindings/` | Ink UI | 키보드 단축키 처리 | +| `vim/` | Ink UI | Vim 에디터 모드 통합 | +| `voice/` | CLI Entry | 음성 입력 모드 | +| `outputStyles/` | Ink UI | 출력 포맷 스타일 | +| `native-ts/` | 공통 유틸 | 네이티브 바인딩 | +| `moreright/` | 공통 유틸 | 확장 유틸리티 | +| `upstreamproxy/` | 네트워크 | 업스트림 프록시 설정 | +| `buddy/` | Agent Loop | 어드바이저(Buddy) 에이전트 | + +--- + +## 4. 부트스트랩 과정 + +`main.tsx`는 다음 순서로 Claude Code를 초기화한다. + +### 4-1. 단계 요약 + +1. **사이드이펙트 임포트** — 파일 최상단에서 즉시 실행되는 세 가지 병렬 초기화: + - `profileCheckpoint('main_tsx_entry')` — 스타트업 프로파일러에 진입 시각 기록 + - `startMdmRawRead()` — MDM(Mobile Device Management) 서브프로세스(`plutil`/`reg query`)를 백그라운드에서 실행 + - `startKeychainPrefetch()` — macOS Keychain에서 OAuth 토큰과 레거시 API 키를 병렬로 선읽기(약 65ms 절감) + +2. **Feature Flag 게이팅** — `bun:bundle`의 `feature()` 함수로 빌드 시점에 사용하지 않는 코드를 제거 (Dead Code Elimination): + ```typescript + const coordinatorModeModule = feature('COORDINATOR_MODE') + ? require('./coordinator/coordinatorMode.js') + : null + const assistantModule = feature('KAIROS') + ? require('./assistant/index.js') + : null + ``` + +3. **지연 `require()`** — 순환 의존성(circular dependency)을 끊기 위해 무거운 모듈을 함수 내부에서 동적 로드: + ```typescript + const getTeammateUtils = () => require('./utils/teammate.js') + ``` + +4. **Commander.js CLI 파싱** — `@commander-js/extra-typings`로 타입 안전한 CLI 옵션 파싱 + +5. **GrowthBook 초기화** — `initializeGrowthBook()`으로 Feature Flag 원격 구성 로드 + +6. **스킬 및 플러그인 등록**: + - `initBundledSkills()` — 번들 스킬 레지스트리에 등록 + - `initBuiltinPlugins()` — 내장 플러그인 초기화 + +7. **`launchRepl()`** — 인터랙티브 REPL(Read-Eval-Print Loop) 시작 + +### 4-2. 부트스트랩 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + participant OS as OS / Shell + participant Main as main.tsx + participant MDM as MDM Subprocess + participant KC as macOS Keychain + participant GB as GrowthBook + participant REPL as launchRepl() + + OS->>Main: 프로세스 시작 + Main->>Main: profileCheckpoint('main_tsx_entry') + par 병렬 프리페치 + Main->>MDM: startMdmRawRead() + Main->>KC: startKeychainPrefetch() + end + Main->>Main: feature() 플래그로 모듈 조건부 require() + Main->>Main: Commander.js CLI 옵션 파싱 + Main->>GB: initializeGrowthBook() + GB-->>Main: Feature Flag 구성 반환 + Main->>Main: initBundledSkills() + Main->>Main: initBuiltinPlugins() + Main->>REPL: launchRepl() + REPL-->>OS: 인터랙티브 프롬프트 표시 +``` + +--- + +## 5. 핵심 설계 원칙 + +### 병렬 Prefetch (병렬 선읽기) + +MDM 설정, Keychain 인증 정보, API 응답을 프로세스 시작과 동시에 병렬로 초기화한다. 이를 통해 최초 프롬프트가 나타나기까지의 지연 시간(약 65ms 이상)을 절감한다. + +### Dead Code Elimination (빌드 시 코드 제거) + +`bun:bundle`의 `feature()` 함수는 빌드 타임에 평가된다. 비활성화된 플래그(`COORDINATOR_MODE`, `KAIROS`, `VOICE_MODE` 등)에 해당하는 코드 경로는 최종 번들에서 완전히 제거된다. 이는 번들 크기와 런타임 메모리를 줄인다. + +### 지연 로딩 (Lazy Loading) + +`teammate.ts → AppStateStore → main.tsx`와 같은 순환 의존성을 끊기 위해, 무거운 모듈은 최상위 `import` 대신 함수 호출 시점의 `require()`로 동적 로드한다. React/Ink처럼 무거운 UI 모듈도 실제 사용 직전까지 로드를 미룬다. + +### 레지스트리 패턴 (Registry Pattern) + +Tool과 Command는 중앙 레지스트리에 등록된다. + +- **Tool 레지스트리** (`tools.ts`): `BashTool`, `FileReadTool`, `FileEditTool`, `GrepTool`, `WebSearchTool` 등 모든 Tool 인스턴스를 배열로 관리. `getTools()` 함수가 현재 권한·플래그 상태에 맞는 Tool 목록을 반환한다. +- **Command 레지스트리** (`commands.ts`): `/commit`, `/doctor`, `/mcp`, `/resume` 등 슬래시 커맨드를 `getCommands()` 함수로 노출한다. `filterCommandsForRemoteMode()`로 원격 세션에서 허용되지 않는 커맨드를 필터링한다. + +`QueryEngineConfig` 타입이 두 레지스트리를 통합하여 엔진에 주입한다: + +```typescript +export type QueryEngineConfig = { + cwd: string + tools: Tools + commands: Command[] + mcpClients: MCPServerConnection[] + agents: AgentDefinition[] + canUseTool: CanUseToolFn + // ... +} +``` + +--- + +## 내비게이션 + +- **다음**: [요청 생명주기](request-lifecycle.md) +- **상위**: [목차](../README.md) diff --git a/docs/ko/level-1-overview/key-concepts.md b/docs/ko/level-1-overview/key-concepts.md new file mode 100644 index 0000000..480a8e0 --- /dev/null +++ b/docs/ko/level-1-overview/key-concepts.md @@ -0,0 +1,204 @@ +# 핵심 개념 (Key Concepts) + +## 개요 + +Claude Code 코드베이스를 이해하려면 상호 긴밀하게 연결된 15개의 핵심 개념을 파악해야 한다. 이 개념들은 크게 세 계층으로 분류된다: LLM 호출과 도구 실행을 조율하는 **엔진 계층** (QueryEngine, Tool, Agent, Context Window, Token Budget), 외부 시스템과 통합하는 **프로토콜 계층** (MCP, LSP, Bridge), 그리고 동작을 확장하고 제어하는 **확장 계층** (Permission Mode, Skill, Plugin, Hook, Slash Command, Worktree, Ink Component). 각 개념의 소스코드 위치와 상호 의존 관계를 함께 파악하면 전체 아키텍처를 빠르게 내면화할 수 있다. + +--- + +## 개념 목록 + +### 1. Tool (도구) + +- **정의**: LLM이 호출할 수 있는 실행 단위로, 입력 스키마(JSON Schema), 실행 로직(`call` 메서드), 권한 검사(`isEnabled`, `isReadOnly`)를 하나의 객체로 캡슐화한다. `src/Tool.ts`의 `ToolUseContext` 타입이 실행 시점에 필요한 모든 런타임 정보(현재 메시지 목록, 파일 상태 캐시, 앱 상태 접근자 등)를 Tool에 주입한다. +- **소스코드 위치**: `src/Tool.ts` (인터페이스 및 타입), `src/tools/` (40+ 개별 구현체) +- **관련 문서**: [Level 2 — Tool 심층 분석](../level-2-tools/tool-interface.md) +- **핵심 포인트**: + - `ToolPermissionContext`가 `DeepImmutable`로 감싸져 있어 실행 중 권한 규칙이 변경되지 않음을 타입 수준에서 보장한다. + - `src/tools.ts`에서 `assembleToolPool()`로 조립되며, `USER_TYPE === 'ant'` 환경 변수와 feature flag에 따라 일부 Tool이 조건부로 포함된다 (예: `REPLTool`, `SleepTool`). + +--- + +### 2. Agent (에이전트) + +- **정의**: 독립적인 메시지 컨텍스트와 시스템 프롬프트를 가지고 비동기 또는 동기로 실행되는 서브프로세스 단위다. 메인 스레드의 `AgentTool`이 `runAgent()`를 호출해 서브에이전트를 스폰하며, 서브에이전트는 별도의 `AgentId`와 자체 `ToolUseContext`를 부여받아 격리된 환경에서 작동한다. +- **소스코드 위치**: `src/tools/AgentTool/AgentTool.tsx`, `src/tools/AgentTool/runAgent.ts`, `src/tools/AgentTool/forkSubagent.ts` +- **관련 문서**: [Level 2 — Agent 심층 분석](../level-2-agents/agent-lifecycle.md) +- **핵심 포인트**: + - `isolation: 'worktree'` 파라미터를 전달하면 에이전트가 격리된 git worktree 내에서 실행된다 (`createAgentWorktree()` 호출). + - `isCoordinatorMode()` 플래그에 따라 코디네이터 모드로 전환되며, 이 경우 에이전트는 팀원(Teammate) 서브에이전트들에게 작업을 위임한다. + +--- + +### 3. QueryEngine (질의 엔진) + +- **정의**: Claude LLM API 호출과 Tool 실행 루프 전체를 관리하는 핵심 엔진이다. 사용자 요청을 받아 메시지를 조립하고, API를 호출하고, 응답에서 Tool 호출을 추출해 실행한 뒤, 그 결과를 다시 컨텍스트에 추가하는 반복 루프를 구동한다. +- **소스코드 위치**: `src/QueryEngine.ts` +- **관련 문서**: [Level 2 — QueryEngine 심층 분석](../level-2-engine/query-engine.md) +- **핵심 포인트**: + - Context Window 관리, Token Budget 추적, Tool 실행 권한 검사, Hook 실행이 모두 QueryEngine 루프 안에서 조율된다. + - `refreshTools` 콜백을 통해 MCP 서버가 실행 도중 추가로 연결되어도 Tool 풀을 동적으로 갱신할 수 있다. + +--- + +### 4. MCP (Model Context Protocol) + +- **정의**: 외부 서버에서 제공하는 도구(Tool)와 리소스(Resource)를 Claude Code에 연결하는 표준 프로토콜이다. `@modelcontextprotocol/sdk`를 기반으로 구현되며, MCP 서버는 stdio 또는 SSE 채널을 통해 통신한다. +- **소스코드 위치**: `src/services/mcp/` (특히 `client.ts`, `types.ts`, `MCPConnectionManager.tsx`) +- **관련 문서**: [Level 2 — MCP 심층 분석](../level-2-integrations/mcp.md) +- **핵심 포인트**: + - `channelPermissions.ts`와 `channelAllowlist.ts`가 MCP 채널별 권한과 허용 목록을 별도로 관리해, 신뢰하지 않는 외부 서버의 Tool 호출을 제한할 수 있다. + - `useManageMCPConnections.ts` 훅이 React 컴포넌트 생명주기와 MCP 연결 수명을 동기화한다. + +--- + +### 5. LSP (Language Server Protocol) + +- **정의**: 코드 인텔리전스(정의 이동, 진단, 심볼 검색 등)를 제공하는 언어 서버와의 통신 프로토콜이다. Claude Code는 LSP 클라이언트로서 언어 서버를 관리하고, LLM이 LSPTool을 통해 코드 지식을 쿼리할 수 있게 한다. +- **소스코드 위치**: `src/services/lsp/` (특히 `LSPClient.ts`, `LSPServerManager.ts`, `LSPDiagnosticRegistry.ts`) +- **관련 문서**: [Level 2 — LSP 심층 분석](../level-2-integrations/lsp.md) +- **핵심 포인트**: + - `LSPDiagnosticRegistry.ts`가 각 파일의 진단 결과를 캐싱해, LLM이 파일 저장 없이도 최신 타입 에러를 조회할 수 있다. + - `passiveFeedback.ts`는 에이전트가 파일을 편집할 때마다 LSP 진단 결과를 수동적으로 수집해 피드백으로 활용한다. + +--- + +### 6. Permission Mode (권한 모드) + +- **정의**: Tool 실행 요청을 자동 승인할지, 사용자 확인을 요구할지, 또는 거부할지를 제어하는 보안 모델이다. `PermissionMode` 타입이 `'default'`, `'acceptEdits'`, `'bypassPermissions'`, `'plan'` 등의 모드를 정의하며, `ToolPermissionContext`가 이를 각 Tool 실행 시점에 전달한다. +- **소스코드 위치**: `src/types/permissions.ts`, `src/Tool.ts` (`ToolPermissionContext` 타입), `src/hooks/useCanUseTool.tsx` +- **관련 문서**: [Level 2 — Permission 시스템](../level-2-security/permissions.md) +- **핵심 포인트**: + - `alwaysAllowRules`, `alwaysDenyRules`, `alwaysAskRules`가 소스별(`ToolPermissionRulesBySource`)로 분리되어, 설정 파일·CLI 인수·사용자 세션 등 규칙의 출처를 추적할 수 있다. + - `shouldAvoidPermissionPrompts` 플래그가 `true`이면 UI를 표시할 수 없는 백그라운드 에이전트에서 자동으로 권한 요청을 거부한다. + +--- + +### 7. Slash Command (슬래시 커맨드) + +- **정의**: `/`로 시작하는 사용자 입력 명령어로, 마크다운 파일(`.claude/commands/*.md`) 또는 코드 내 `Command` 객체로 정의된다. `loadSkillsDir.ts`에서 frontmatter를 파싱해 명령어 인수 치환, 실행 모델 지정, 허용 도구 목록 등을 구성한다. +- **소스코드 위치**: `src/skills/loadSkillsDir.ts`, `src/types/command.ts`, `src/utils/markdownConfigLoader.ts` +- **관련 문서**: [Level 2 — Slash Command](../level-2-ux/slash-commands.md) +- **핵심 포인트**: + - frontmatter의 `allowed-tools`, `model`, `effort` 필드를 통해 각 명령어가 사용할 수 있는 Tool, 모델, 처리 강도를 개별 지정할 수 있다. + - `$ARGUMENTS` 치환(`substituteArguments()`) 메커니즘으로 사용자가 입력한 텍스트를 프롬프트 템플릿에 동적으로 삽입한다. + +--- + +### 8. Ink Component (Ink 컴포넌트) + +- **정의**: React 기반의 CLI UI 프레임워크인 [Ink](https://github.com/vadimdemedes/ink)를 사용해 터미널에 렌더링되는 UI 컴포넌트다. 각 Tool은 `renderToolUseMessage()` 같은 함수를 통해 실행 상태를 JSX로 반환하고, QueryEngine이 이를 `setToolJSX()` 콜백으로 화면에 전달한다. +- **소스코드 위치**: `src/tools/AgentTool/UI.tsx`, `src/components/` (전체 UI 컴포넌트), `src/Tool.ts` (`SetToolJSXFn` 타입) +- **관련 문서**: [Level 2 — UI 렌더링 시스템](../level-2-ux/ink-components.md) +- **핵심 포인트**: + - `SetToolJSXFn`의 `shouldHidePromptInput` 플래그로 Tool 실행 중 사용자 입력창을 숨기거나 표시할 수 있다. + - React Compiler(`react/compiler-runtime`)가 적용되어 있어 상태 변경에 따른 리렌더링이 자동으로 최적화된다. + +--- + +### 9. Context Window (컨텍스트 윈도우) + +- **정의**: LLM API 호출 시 전송되는 대화 이력의 범위다. 메시지 목록(`Message[]`)이 누적됨에 따라 토큰 한계에 근접하면 QueryEngine이 오래된 내용을 요약(compact)하거나 Tool 결과를 압축하는 전략을 실행한다. +- **소스코드 위치**: `src/utils/context.ts`, `src/utils/analyzeContext.ts`, `src/Tool.ts` (`CompactProgressEvent` 타입, `onCompactProgress` 콜백) +- **관련 문서**: [Level 2 — Context 관리](../level-2-engine/context-management.md) +- **핵심 포인트**: + - `CompactProgressEvent`의 `pre_compact`, `post_compact`, `session_start` 훅 타입이 컴팩션 생명주기의 세 지점에 외부 훅을 삽입할 수 있음을 보여준다. + - `contentReplacementState`(`ContentReplacementState`)가 Tool 결과 예산을 추적하고, 예산 초과 시 내용을 참조 링크로 대체해 컨텍스트 크기를 줄인다. + +--- + +### 10. Token Budget (토큰 예산) + +- **정의**: API 호출에서 소비되는 토큰 수를 추적하고 비용 상한(`maxBudgetUsd`)을 적용하는 메커니즘이다. 에이전트별 토큰 카운터가 진행 중인 작업 추적기(`createProgressTracker()`)와 연동되어 총 사용량을 실시간으로 집계한다. +- **소스코드 위치**: `src/utils/tokenBudget.ts`, `src/utils/tokens.ts`, `src/Tool.ts` (`maxBudgetUsd` 옵션) +- **관련 문서**: [Level 2 — Token Budget](../level-2-engine/token-budget.md) +- **핵심 포인트**: + - `ToolUseContext.options.maxBudgetUsd`로 세션 단위 비용 상한을 설정할 수 있으며, 초과 시 새 API 호출을 차단한다. + - 서브에이전트는 `getTokenCountFromTracker()`로 자신의 토큰 사용량을 부모 에이전트에게 보고한다. + +--- + +### 11. Skill (스킬) + +- **정의**: 마크다운 파일로 정의된 재사용 가능한 워크플로우 단위로, Slash Command와 유사하지만 `skills/` 디렉토리에 묶여 배포 가능한 단위(Plugin의 구성 요소)로 존재한다. `loadSkillsDir.ts`가 frontmatter를 파싱해 `PromptCommand` 또는 MCP 기반 스킬로 변환한다. +- **소스코드 위치**: `src/skills/loadSkillsDir.ts`, `src/skills/bundledSkills.ts`, `src/skills/mcpSkillBuilders.ts` +- **관련 문서**: [Level 2 — Skill 시스템](../level-2-extensions/skills.md) +- **핵심 포인트**: + - `bundledSkills.ts`에 등록된 스킬은 빌드 시점에 번들에 포함되며, 마켓플레이스에서 설치한 외부 스킬과 런타임에 병합된다. + - `mcpSkillBuilders.ts`는 MCP 서버가 제공하는 프롬프트를 Skill 형식으로 변환해 동일한 슬래시 커맨드 UI로 노출한다. + +--- + +### 12. Plugin (플러그인) + +- **정의**: 여러 컴포넌트(스킬, 훅, MCP 서버, 슬래시 커맨드)를 하나의 배포 단위로 묶는 외부 확장 시스템이다. `{name}@builtin` 형식의 ID를 가진 빌트인 플러그인과 `{name}@{marketplace}` 형식의 마켓플레이스 플러그인으로 구분된다. +- **소스코드 위치**: `src/plugins/builtinPlugins.ts`, `src/types/plugin.ts`, `src/plugins/bundled/` +- **관련 문서**: [Level 2 — Plugin 시스템](../level-2-extensions/plugins.md) +- **핵심 포인트**: + - `registerBuiltinPlugin()`으로 등록된 빌트인 플러그인은 `/plugin` UI에서 사용자가 활성화/비활성화할 수 있으며, 설정이 사용자 설정 파일에 영속된다. + - 플러그인은 `isAvailable()` 함수를 통해 현재 환경에서 사용 가능 여부를 런타임에 판단하고, 불가능한 경우 목록에서 제외된다. + +--- + +### 13. Bridge (브릿지) + +- **정의**: VS Code, JetBrains 등 IDE와 Claude Code CLI 간의 양방향 통신 레이어다. IDE 확장이 브릿지 API 서버에 접속해 세션을 생성하고, CLI는 폴링 또는 웹소켓을 통해 IDE의 컨텍스트(현재 열린 파일, 선택 영역 등)를 수신하고 작업 결과를 IDE에 돌려준다. +- **소스코드 위치**: `src/bridge/bridgeMain.ts`, `src/bridge/bridgeApi.ts`, `src/bridge/bridgeMessaging.ts`, `src/bridge/types.ts` +- **관련 문서**: [Level 2 — Bridge 시스템](../level-2-integrations/bridge.md) +- **핵심 포인트**: + - `workSecret.ts`의 `registerWorker()`와 JWT 기반 인증(`jwtUtils.ts`)으로 브릿지 연결의 신뢰성을 검증한다. + - `capacityWake.ts`가 백그라운드 세션의 용량 이벤트를 처리해, IDE가 비활성 상태일 때도 에이전트 작업이 계속 진행될 수 있게 한다. + +--- + +### 14. Hook (훅) + +- **정의**: Tool 사용 전후, 세션 시작, 컴팩션 등 특정 생명주기 이벤트에 외부 로직을 삽입하는 이벤트 기반 확장 포인트다. 설정 파일의 `hooks` 섹션에 셸 커맨드 또는 MCP 서버를 지정하면, Claude Code가 해당 이벤트 발생 시 이를 실행한다. +- **소스코드 위치**: `src/hooks/useCanUseTool.tsx` (Tool 사용 권한 훅), `src/types/hooks.ts`, `src/hooks/toolPermission/` +- **관련 문서**: [Level 2 — Hook 시스템](../level-2-extensions/hooks.md) +- **핵심 포인트**: + - `useCanUseTool.tsx`에서 `awaitAutomatedChecksBeforeDialog` 플래그가 활성화되면, 사용자에게 권한 대화창을 표시하기 전에 분류기(classifier)와 훅 결과를 먼저 대기한다. + - `HookProgress` 타입으로 훅 실행 상태가 UI에 스트리밍되어, 사용자가 훅 실행 경과를 실시간으로 확인할 수 있다. + +--- + +### 15. Worktree (워크트리) + +- **정의**: 에이전트가 메인 저장소와 격리된 환경에서 작업할 수 있도록 `git worktree`를 기반으로 생성되는 임시 작업 디렉토리다. `AgentTool`이 `isolation: 'worktree'`로 호출되면 `createAgentWorktree()`가 새 브랜치와 워크트리를 생성하고, 에이전트 종료 시 `removeAgentWorktree()`로 정리한다. +- **소스코드 위치**: `src/utils/worktree.ts`, `src/tools/AgentTool/AgentTool.tsx` (생성/제거 로직), `src/tools/EnterWorktreeTool/`, `src/tools/ExitWorktreeTool/` +- **관련 문서**: [Level 2 — Worktree 격리](../level-2-agents/worktree-isolation.md) +- **핵심 포인트**: + - 포크(Fork) 서브에이전트와 워크트리가 함께 사용될 때, `buildWorktreeNotice()`가 자식 에이전트에게 경로 변환 안내를 시스템 프롬프트로 주입한다. + - `hasWorktreeChanges()`로 에이전트 작업 완료 후 실제 변경 사항이 있는지 확인하고, 변경이 있을 때만 결과를 메인 브랜치에 반영하는 흐름을 지원한다. + +--- + +## 개념 관계도 + +```mermaid +mindmap + root((QueryEngine)) + Tool + Permission Mode + LSP + Agent + Worktree + Slash Command + MCP + Context Window + Token Budget + UI 계층 + Ink Component + Bridge + 확장 계층 + Skill + Plugin + Hook +``` + +--- + +## Navigation + +- 이전: [요청 생명주기](request-lifecycle.md) +- 상위: [목차](../README.md) diff --git a/docs/ko/level-1-overview/request-lifecycle.md b/docs/ko/level-1-overview/request-lifecycle.md new file mode 100644 index 0000000..ace7baf --- /dev/null +++ b/docs/ko/level-1-overview/request-lifecycle.md @@ -0,0 +1,332 @@ +# 요청 생명주기 (Request Lifecycle) + +## 개요 + +이 문서는 사용자가 프롬프트를 입력하는 순간부터 최종 응답이 터미널에 렌더링되기까지, 하나의 요청이 Claude Code 내부 파이프라인 전체를 통과하는 경로를 단계별로 추적한다. 각 단계에서 어떤 모듈이 개입하고, 어떤 데이터가 변환되며, 어떤 조건 분기가 발생하는지를 소스 코드에 기반하여 정확하게 서술한다. + +--- + +## 전체 흐름 다이어그램 + +```mermaid +sequenceDiagram + actor User + participant CLI as CLI (main.tsx) + participant REPL as REPL (screens/REPL.tsx) + participant PUI as processUserInput() + participant CTX as Context Collection (context.ts) + participant QE as QueryEngine.submitMessage() + participant Q as query() / queryLoop() + participant API as Anthropic API + participant TE as Tool Execution (runTools) + participant PC as Permission Check (canUseTool) + participant INK as Ink / React Renderer + + User->>CLI: 터미널에 프롬프트 입력 + CLI->>CLI: Commander.js 옵션 파싱 + CLI->>CLI: MDM / Keychain / API 병렬 프리페치 + CLI->>REPL: launchRepl() 호출 + REPL->>PUI: processUserInput({ input, mode, context }) + PUI->>PUI: 슬래시 커맨드 / bash 모드 / 일반 텍스트 분기 + PUI->>PUI: UserPromptSubmit 훅 실행 + PUI-->>QE: messages[], shouldQuery 반환 + QE->>CTX: getSystemContext() — git 상태 수집 (memoized) + QE->>CTX: getUserContext() — CLAUDE.md 탐색, 현재 날짜 (memoized) + QE->>API: query() → queryLoop() → claude() API 호출 (스트리밍) + API-->>Q: 스트리밍 응답 (text / tool_use 블록) + Q->>Q: 텍스트 블록 → INK으로 실시간 렌더링 + Q->>Q: tool_use 블록 감지 + Q->>PC: canUseTool() — 권한 확인 + PC-->>Q: allow / deny / ask + Q->>TE: runTools() — 도구 실행 (BashTool, FileEditTool 등) + TE-->>Q: ToolResultBlockParam[] 반환 + Q->>API: 도구 결과 포함 재질의 (에이전트 루프) + API-->>Q: 다음 응답 스트림 + Q->>Q: tool_use 없음 → 루프 종료 + Q-->>INK: 최종 assistant 메시지 스트림 + INK->>User: 최종 응답 렌더링 +``` + +--- + +## 단계 1: CLI 부트스트랩 + +**관련 파일:** `src/main.tsx` + +Claude Code가 실행되면 `main.tsx`가 진입점 역할을 한다. 모듈 평가가 시작되는 즉시 세 가지 사이드 이펙트가 순차적으로 트리거되어, 이후 약 135ms의 무거운 모듈 임포트 시간 동안 백그라운드에서 I/O를 병렬로 수행한다. + +``` +profileCheckpoint('main_tsx_entry') // 기동 프로파일링 마커 +startMdmRawRead() // MDM 설정 읽기 (plutil/reg query 서브프로세스) +startKeychainPrefetch() // macOS Keychain: OAuth 토큰 + 레거시 API 키 병렬 읽기 +``` + +이 세 작업이 먼저 실행된 후 Commander.js가 CLI 옵션을 파싱한다. `--model`, `--tools`, `--add-dir`, `--print`, `--output-format` 등 수십 개의 옵션이 여기서 처리된다. 파싱이 완료되면 최종적으로 `launchRepl()`을 호출하여 인터랙티브 세션을 시작한다. + +**병렬 프리페치 항목:** +- MDM(Mobile Device Management) 원격 관리 설정 +- Keychain에서 OAuth 토큰 및 레거시 API 키 +- GrowthBook A/B 테스트 플래그 초기화 +- MCP(Model Context Protocol) 공식 레지스트리 URL 프리페치 + +--- + +## 단계 2: 사용자 입력 수신 + +**관련 파일:** `src/screens/REPL.tsx`, `src/utils/processUserInput/processUserInput.ts` + +REPL은 Ink(React 기반 터미널 UI 라이브러리)로 렌더링된 `PromptInput` 컴포넌트를 통해 사용자 입력을 수신한다. 사용자가 Enter를 누르면 `handlePromptSubmit()`이 호출되고, 이것이 `processUserInput()`으로 이어진다. + +`processUserInput()`은 원시 입력을 API 호출에 적합한 메시지 배열로 변환하는 게이트웨이다. 내부적으로 `processUserInputBase()`를 호출한 뒤 `UserPromptSubmit` 훅을 실행한다. + +**입력 분기 처리:** + +| 입력 유형 | 분기 경로 | +|---|---| +| `/` 로 시작하는 문자열 | `processSlashCommand()` — 슬래시 커맨드 처리 | +| `bash` 모드 | `processBashCommand()` — Bash 직접 실행 | +| 이미지 포함 (ContentBlockParam[]) | 이미지 리사이즈 및 base64 처리 후 일반 경로 | +| 일반 텍스트 | `processTextPrompt()` — UserMessage 생성 | + +**슬래시 커맨드 탐지:** +입력이 `/`로 시작하고 `skipSlashCommands`가 false이면 슬래시 커맨드로 처리된다. `parseSlashCommand()`로 커맨드 이름을 파싱한 뒤 `findCommand()`로 등록된 커맨드를 탐색한다. 원격 브리지(CCR) 클라이언트에서 온 입력은 `bridgeOrigin` 플래그와 `isBridgeSafeCommand()` 검사를 통해 안전한 커맨드만 허용한다. + +**반환값 (`ProcessUserInputBaseResult`):** +```typescript +{ + messages: (UserMessage | AssistantMessage | AttachmentMessage | SystemMessage)[], + shouldQuery: boolean, // false이면 LLM 호출 없이 즉시 반환 + allowedTools?: string[], + model?: string, + resultText?: string, +} +``` + +--- + +## 단계 3: 컨텍스트 수집 + +**관련 파일:** `src/context.ts` + +`QueryEngine.submitMessage()`는 API 호출 전에 시스템 프롬프트를 구성하기 위해 두 가지 컨텍스트 수집 함수를 호출한다. 두 함수 모두 `lodash-es/memoize`로 메모이제이션되어 동일 대화 내에서는 한 번만 실행된다. + +### `getSystemContext()` + +Git 저장소 정보를 수집하여 시스템 프롬프트에 포함시킨다. `getGitStatus()`를 내부적으로 호출하며, 다음 git 명령들을 병렬로 실행한다: + +``` +git --no-optional-locks status --short // 변경 파일 목록 +git --no-optional-locks log --oneline -n 5 // 최근 커밋 5개 +git config user.name // 사용자 이름 +getBranch() // 현재 브랜치 +getDefaultBranch() // 기본 브랜치 (PR 대상) +``` + +반환되는 컨텍스트 문자열 예시: +``` +Current branch: main +Main branch (you will usually use this for PRs): main +Git user: Jane Smith +Status: + M src/query.ts +Recent commits: +abc1234 Fix tool permission check +... +``` + +상태 문자열이 2,000자를 초과하면 잘라낸 뒤 BashTool 사용을 안내하는 메시지를 덧붙인다. 원격 CCR 환경(`CLAUDE_CODE_REMOTE` 환경변수) 또는 git 지시사항이 비활성화된 경우 이 단계를 건너뛴다. + +### `getUserContext()` + +사용자 정의 지시사항과 현재 날짜를 수집한다: + +- **CLAUDE.md 탐색**: 작업 디렉터리에서 상위 방향으로 `CLAUDE.md` 파일을 탐색(`getMemoryFiles()` → `filterInjectedMemoryFiles()` → `getClaudeMds()`). `CLAUDE_CODE_DISABLE_CLAUDE_MDS` 환경변수 또는 `--bare` 모드에서 비활성화. +- **현재 날짜**: `getLocalISODate()`로 ISO 형식 날짜 생성 후 `"Today's date is YYYY-MM-DD."` 형태로 포함. + +수집된 컨텍스트는 시스템 프롬프트의 일부로 `prependUserContext()` / `appendSystemContext()`를 통해 API 요청에 포함된다. + +--- + +## 단계 4: LLM 호출 (QueryEngine → query()) + +**관련 파일:** `src/QueryEngine.ts`, `src/query.ts` + +### QueryEngineConfig + +`QueryEngine`은 하나의 대화 세션에 대응하는 클래스다. 생성자에서 받는 `QueryEngineConfig`의 핵심 필드: + +```typescript +{ + cwd: string, // 현재 작업 디렉터리 + tools: Tools, // 사용 가능한 도구 목록 + commands: Command[], // 슬래시 커맨드 목록 + mcpClients: MCPServerConnection[], // MCP 서버 연결 + agents: AgentDefinition[], // 에이전트 정의 + canUseTool: CanUseToolFn, // 도구 권한 확인 함수 + getAppState: () => AppState, + setAppState: (f) => void, + maxTurns?: number, // 최대 에이전트 루프 횟수 + maxBudgetUsd?: number, // 예산 제한 +} +``` + +### submitMessage() → query() 흐름 + +`submitMessage()`는 다음 순서로 동작한다: + +1. `processUserInput()`으로 사용자 입력을 메시지 배열로 변환 +2. `fetchSystemPromptParts()`로 시스템 프롬프트 조립 (기본 프롬프트 + 사용자 컨텍스트 + 시스템 컨텍스트) +3. 세션 트랜스크립트 기록 (`recordTranscript()`) — API 응답 전에 선제적으로 저장 +4. `query()` 호출 — 실제 LLM 통신 루프 시작 + +`query()`는 `queryLoop()`에 위임하는 얇은 래퍼이며, 완료된 커맨드 UUID를 lifecycle 이벤트로 통지하는 역할만 추가한다. + +### queryLoop() 내부 + +`queryLoop()`는 비동기 제너레이터(`AsyncGenerator`)로 구현된 핵심 에이전트 루프다. 각 이터레이션에서: + +1. `normalizeMessagesForAPI()`로 메시지를 Anthropic API 형식으로 정규화 +2. `claude()` 함수를 통해 스트리밍 API 호출 +3. 스트림에서 수신되는 이벤트를 즉시 `yield`하여 렌더러에 전달 + +**스트리밍 응답 처리:** +- `text_delta` 이벤트 → 텍스트 청크를 누적하여 Ink 컴포넌트에 실시간 전달 +- `tool_use` 블록 → 도구 실행 경로로 분기 +- `message_stop` → 현재 이터레이션 종료, 루프 계속 여부 결정 + +**토큰 및 비용 추적:** +API 응답의 `usage` 필드를 `accumulateUsage()` / `updateUsage()`로 집계한다. `getTotalCost()`, `getModelUsage()`, `getTotalAPIDuration()`을 통해 누적 비용과 지연 시간을 추적한다. + +--- + +## 단계 5: Tool 실행 + +**관련 파일:** `src/query.ts`, `src/services/tools/toolOrchestration.ts`, `src/Tool.ts` + +LLM 응답에 `tool_use` 블록이 포함되면 도구 실행 경로로 진입한다. + +### 권한 확인 (ToolPermissionContext) + +도구 실행 전에 `canUseTool()`이 호출된다. `ToolPermissionContext`는 다음 필드로 권한 정책을 관리한다: + +```typescript +type ToolPermissionContext = { + mode: PermissionMode, // 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' + alwaysAllowRules: ToolPermissionRulesBySource, + alwaysDenyRules: ToolPermissionRulesBySource, + alwaysAskRules: ToolPermissionRulesBySource, + isBypassPermissionsModeAvailable: boolean, + shouldAvoidPermissionPrompts?: boolean, // 백그라운드 에이전트에서 true +} +``` + +`canUseTool()`의 반환값은 `{ behavior: 'allow' | 'deny' | 'ask' }`이며, `deny`나 `ask`(사용자가 거부)인 경우 `SDKPermissionDenial`로 기록된다. + +### 입력 스키마 검증 + +각 도구는 Zod 스키마(`inputSchema`)를 보유한다. 도구 실행 전에 LLM이 제공한 `tool_input`을 해당 스키마로 검증한다. 검증 실패 시 오류를 `tool_result` 블록으로 래핑하여 LLM에게 다시 전달한다. + +### 도구 실행 (runTools) + +`runTools()`는 `StreamingToolExecutor`를 통해 허가된 도구들을 실행한다. 실행 결과는 `ToolResultBlockParam[]`으로 수집된다. + +**기본 제공 도구 목록 (`src/tools.ts`):** + +| 도구 | 역할 | +|---|---| +| `BashTool` | 셸 커맨드 실행 | +| `FileReadTool` | 파일 읽기 | +| `FileEditTool` | 파일 편집 (diff 기반) | +| `FileWriteTool` | 파일 쓰기 | +| `GlobTool` | 파일 패턴 검색 | +| `GrepTool` | 내용 패턴 검색 | +| `WebFetchTool` | URL 콘텐츠 가져오기 | +| `WebSearchTool` | 웹 검색 | +| `AgentTool` | 서브에이전트 실행 | +| `TodoWriteTool` | TODO 목록 관리 | +| `SkillTool` | 슬래시 커맨드 스킬 실행 | +| `NotebookEditTool` | Jupyter 노트북 편집 | + +MCP 도구는 `mcpClients`를 통해 동적으로 추가된다. + +--- + +## 단계 6: 에이전트 루프 + +**관련 파일:** `src/query.ts` (`queryLoop()`), `src/QueryEngine.ts` (`submitMessage()`), `src/utils/fileHistory.ts` + +도구 실행 결과가 있으면 `queryLoop()`는 루프를 계속한다. 결과를 새로운 `UserMessage`의 `tool_result` 블록으로 추가하고 LLM에게 재질의한다. + +### 루프 종료 조건 + +| 조건 | 처리 | +|---|---| +| 응답에 tool_use 블록 없음 | 정상 종료 (`Terminal` 반환) | +| `maxTurns` 도달 | 강제 종료, 결과 메시지 반환 | +| `maxBudgetUsd` 초과 | 비용 초과 오류 반환 | +| 사용자 중단 (Ctrl+C) | `AbortController.abort()` 신호 전파 | +| LLM이 `stop_reason: "end_turn"` 반환 | 정상 종료 | + +### 파일 히스토리 스냅샷 + +`fileHistoryEnabled()` 조건이 만족되면 사용자 메시지마다 `fileHistoryMakeSnapshot()`을 호출하여 파일 상태 스냅샷을 기록한다. 이 스냅샷은 `/undo` 커맨드가 이전 상태로 복원할 때 사용된다. + +### 컨텍스트 압축 (Auto-compact) + +`calculateTokenWarningState()`로 토큰 사용량을 모니터링하며, 컨텍스트가 한계에 가까워지면 `autoCompact` 메커니즘이 활성화된다. `buildPostCompactMessages()`로 대화 히스토리를 압축하고 `compact_boundary` 시스템 메시지를 삽입한다. + +--- + +## 단계 7: 응답 렌더링 + +**관련 파일:** `src/screens/REPL.tsx`, Ink React 컴포넌트들 + +`query()`에서 `yield`된 메시지들은 Ink의 React 렌더링 파이프라인을 통해 터미널에 출력된다. + +**스트리밍 렌더링:** +`text_delta` 이벤트가 도착할 때마다 Ink 컴포넌트가 리렌더링되어 타이핑 효과처럼 텍스트가 점진적으로 표시된다. 마크다운 구문은 터미널 색상과 굵기로 변환된다. + +**도구 결과 인라인 표시:** +도구 실행 중에는 스피너와 함께 진행 상황이 표시된다. 실행 완료 후에는 결과(파일 변경 내용, 커맨드 출력 등)가 접을 수 있는 블록으로 렌더링된다. + +**비용 및 토큰 정보:** +각 응답 완료 후 `getTotalCost()`, `getModelUsage()`를 통해 집계된 토큰 사용량과 비용이 표시된다. + +**최종 `result` 메시지 (SDK 모드):** +SDK(`QueryEngine`) 경로에서는 모든 처리 완료 후 다음 형식의 최종 결과 메시지를 `yield`한다: + +```typescript +{ + type: 'result', + subtype: 'success', + duration_ms: number, + duration_api_ms: number, + num_turns: number, + result: string, // 최종 텍스트 응답 + total_cost_usd: number, + usage: NonNullableUsage, + permission_denials: SDKPermissionDenial[], +} +``` + +--- + +## 각 단계별 Level 2 참조 링크 + +| 단계 | 주제 | Level 2 문서 | +|---|---|---| +| 단계 1 | CLI 부트스트랩 및 기동 시퀀스 | [CLI 부트스트랩 상세](../level-2-components/cli-bootstrap.md) | +| 단계 2 | 입력 처리 및 슬래시 커맨드 | [입력 처리 상세](../level-2-components/input-processing.md) | +| 단계 3 | 컨텍스트 수집 및 시스템 프롬프트 | [컨텍스트 수집 상세](../level-2-components/context-collection.md) | +| 단계 4 | QueryEngine 및 LLM 통신 | [QueryEngine 상세](../level-2-components/query-engine.md) | +| 단계 5 | 도구 권한 및 실행 | [Tool 실행 상세](../level-2-components/tool-execution.md) | +| 단계 6 | 에이전트 루프 제어 | [에이전트 루프 상세](../level-2-components/agent-loop.md) | +| 단계 7 | Ink 렌더링 파이프라인 | [렌더링 상세](../level-2-components/rendering.md) | + +--- + +## Navigation + +- 이전: [아키텍처 개요](architecture.md) +- 다음: [핵심 개념](key-concepts.md) +- 상위: [목차](../README.md) diff --git a/docs/ko/level-2-systems/agent-coordinator.md b/docs/ko/level-2-systems/agent-coordinator.md new file mode 100644 index 0000000..959dd38 --- /dev/null +++ b/docs/ko/level-2-systems/agent-coordinator.md @@ -0,0 +1,561 @@ +# 에이전트 오케스트레이션: 멀티에이전트 시스템 분석 + +## 1. 개요 + +Claude Code의 멀티에이전트(multi-agent) 오케스트레이션(orchestration) 시스템은 단일 세션 안에서 여러 서브에이전트(subagent)를 동시에 생성하고 조율하는 기반 구조다. 이 시스템은 복잡한 작업을 독립적인 단위로 분해하고 병렬 실행하여 성능과 컨텍스트 격리(context isolation)를 동시에 달성한다. + +**핵심 구성 요소와 파일 위치:** + +| 구성 요소 | 경로 | +|-----------|------| +| Coordinator Mode (코디네이터 모드) | `src/coordinator/coordinatorMode.ts` | +| AgentTool (에이전트 생성 도구) | `src/tools/AgentTool/` | +| TeamCreateTool / TeamDeleteTool | `src/tools/TeamCreateTool/`, `src/tools/TeamDeleteTool/` | +| SendMessageTool (에이전트 간 통신) | `src/tools/SendMessageTool/` | +| Task 시스템 | `src/tasks/` | + +이 문서는 각 구성 요소의 구현 세부 사항과 상호작용 방식을 분석한다. + +--- + +## 2. 아키텍처 다이어그램 + +전체 시스템의 데이터 흐름은 다음과 같다. + +```mermaid +graph TD + User["사용자"] --> MainAgent["메인 에이전트\n(Coordinator Mode)"] + + MainAgent -->|"AgentTool(subagent_type, prompt)"| Spawn["에이전트 생성 경로"] + Spawn -->|"일반 서브에이전트"| LocalAgentTask["LocalAgentTask\n(로컬 비동기 실행)"] + Spawn -->|"isolation: worktree"| Worktree["Git Worktree\n(격리된 작업 복사본)"] + Spawn -->|"isolation: remote (ant-only)"| RemoteAgentTask["RemoteAgentTask\n(원격 CCR 실행)"] + + LocalAgentTask --> SubAgent["서브에이전트\n(독립 QueryEngine + 컨텍스트)"] + Worktree --> SubAgent + + SubAgent -->|"task-notification XML"| MainAgent + + MainAgent -->|"TeamCreateTool(team_name)"| TeamContext["팀 컨텍스트\n(TeamFile)"] + TeamContext -->|"AgentTool(subagent_type, team_name)"| Teammate["팀메이트(Teammate)\n(tmux/in-process)"] + Teammate -->|"SendMessageTool(to, message)"| Mailbox["메일박스(Mailbox)\n파일시스템 기반"] + Mailbox -->|"수신"| Teammate + Mailbox -->|"수신"| MainAgent + + MainAgent -->|"SendMessageTool(to: agentId)"| SubAgent + MainAgent -->|"TeamDeleteTool()"| Cleanup["팀 정리\n(디렉터리, 컬러, 태스크)"] + + SubAgent -.->|"ProgressTracker 업데이트"| TaskProgress["태스크 진행 상황\n(토큰 수, 도구 호출 수)"] + TaskProgress -.-> MainAgent + + style MainAgent fill:#2d6a9f,color:#fff + style SubAgent fill:#3a7d44,color:#fff + style LocalAgentTask fill:#7d5a3c,color:#fff + style RemoteAgentTask fill:#7d3a3a,color:#fff + style TeamContext fill:#5a3a7d,color:#fff +``` + +**핵심 설계 원칙:** 모든 서브에이전트는 자신만의 독립적인 `QueryEngine` 인스턴스와 메시지 컨텍스트를 가진다. 부모 에이전트는 서브에이전트의 내부 상태를 직접 접근할 수 없으며, 오직 `task-notification` XML 메시지를 통해 결과를 수신한다. + +--- + +## 3. Coordinator Mode (코디네이터 모드) + +### 3.1 기본 동작: `isCoordinatorMode()` + +```typescript +// src/coordinator/coordinatorMode.ts +export function isCoordinatorMode(): boolean { + if (feature('COORDINATOR_MODE')) { + return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) + } + return false +} +``` + +코디네이터 모드는 두 겹의 게이팅(gating)을 통해 활성화된다. + +1. **피처 플래그(feature flag) 게이트**: `feature('COORDINATOR_MODE')` — Bun 번들러의 `bun:bundle` 모듈이 제공하는 빌드 타임 상수다. 플래그가 `false`이면 번들러가 이 코드 경로 전체를 데드 코드(dead code)로 제거한다. +2. **환경 변수 게이트**: `CLAUDE_CODE_COORDINATOR_MODE` 환경 변수가 truthy 값으로 설정되어야 한다. + +이중 게이트 구조는 프로덕션 빌드에서 멀티에이전트 기능 코드를 완전히 제거할 수 있도록 한다. + +### 3.2 세션 재개 시 모드 일치: `matchSessionMode()` + +```typescript +export function matchSessionMode( + sessionMode: 'coordinator' | 'normal' | undefined, +): string | undefined +``` + +저장된 세션을 재개할 때 현재 환경의 모드와 세션에 기록된 모드가 다를 수 있다. 이 함수는 `process.env.CLAUDE_CODE_COORDINATOR_MODE`를 런타임에 직접 수정하여 불일치를 해소한다. `isCoordinatorMode()`가 환경 변수를 캐시 없이 매번 직접 읽기 때문에 이 방식이 작동한다. + +### 3.3 워커 도구 컨텍스트: `getCoordinatorUserContext()` + +코디네이터 모드가 활성화되면 메인 에이전트는 워커(worker) 에이전트들이 어떤 도구에 접근할 수 있는지 알아야 한다. `getCoordinatorUserContext()`는 이 정보를 `workerToolsContext` 키로 반환한다. + +```typescript +// 내부 도구 집합 (워커에게 노출되지 않음) +const INTERNAL_WORKER_TOOLS = new Set([ + TEAM_CREATE_TOOL_NAME, + TEAM_DELETE_TOOL_NAME, + SEND_MESSAGE_TOOL_NAME, + SYNTHETIC_OUTPUT_TOOL_NAME, +]) +``` + +`CLAUDE_CODE_SIMPLE` 환경 변수가 설정된 경우 워커 도구를 Bash, Read, Edit 세 가지로만 제한하는 단순 모드도 지원한다. + +### 3.4 코디네이터 시스템 프롬프트 + +`getCoordinatorSystemPrompt()`는 다음 단계로 구성된 멀티에이전트 워크플로를 코디네이터에게 지시하는 시스템 프롬프트를 생성한다. + +| 단계 | 담당 | 목적 | +|------|------|------| +| Research (조사) | 워커 (병렬) | 코드베이스 탐색, 문제 파악 | +| Synthesis (종합) | 코디네이터 | 발견 사항 읽기, 구현 명세 작성 | +| Implementation (구현) | 워커 | 변경 적용, 커밋 | +| Verification (검증) | 워커 | 변경 사항 테스트 | + +핵심 원칙은 **"절대로 이해를 위임하지 말라(Never delegate understanding)"**이다. 코디네이터는 워커의 결과를 직접 종합하여 다음 워커에게 구체적인 파일 경로, 줄 번호, 변경 내용을 포함한 지시를 작성해야 한다. + +### 3.5 Scratchpad (스크래치패드) 게이트 + +```typescript +function isScratchpadGateEnabled(): boolean { + return checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_scratch') +} +``` + +`tengu_scratch` 피처 게이트가 활성화되면 워커들이 퍼미션 프롬프트 없이 읽고 쓸 수 있는 공유 스크래치패드 디렉터리가 활성화된다. 스크래치패드는 워커 간 지식을 영속적으로 공유하는 메커니즘이다. + +주목할 점은 `isScratchpadGateEnabled()`가 `src/utils/permissions/filesystem.ts`의 `isScratchpadEnabled()`와 동일한 게이트를 검사하지만 함수를 재사용하지 않는다는 것이다. 이는 `filesystem.ts`를 가져오면 `filesystem → permissions → ... → coordinatorMode`의 순환 의존성(circular dependency)이 발생하기 때문이다. 이 설계 결정은 코드 주석에 명시되어 있다. + +--- + +## 4. AgentTool 분석 + +### 4.1 에이전트 생성 메커니즘 + +`AgentTool.tsx`는 새 에이전트를 생성하는 핵심 도구다. 입력 스키마(input schema)는 피처 플래그에 따라 동적으로 구성된다. + +```typescript +// 기본 입력 파라미터 +const baseInputSchema = lazySchema(() => z.object({ + description: z.string(), // 3-5 단어의 짧은 작업 설명 + prompt: z.string(), // 에이전트에게 전달할 실제 지시 + subagent_type: z.string().optional(), // 전문 에이전트 타입 + model: z.enum(['sonnet', 'opus', 'haiku']).optional(), + run_in_background: z.boolean().optional(), +})); +``` + +`KAIROS` 피처 플래그가 활성화된 경우 `cwd` 파라미터(작업 디렉터리 재정의)가 추가된다. `run_in_background`는 `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` 환경 변수 또는 Fork Subagent 기능이 활성화된 경우 스키마에서 제거된다. + +**자동 백그라운드 타임아웃:** + +```typescript +function getAutoBackgroundMs(): number { + if (isEnvTruthy(process.env.CLAUDE_AUTO_BACKGROUND_TASKS) || + getFeatureValue_CACHED_MAY_BE_STALE('tengu_auto_background_agents', false)) { + return 120_000; // 2분 후 자동으로 백그라운드로 전환 + } + return 0; +} +``` + +포그라운드(foreground)로 실행 중인 에이전트가 2분을 초과하면 자동으로 백그라운드로 전환된다. + +### 4.2 AgentDefinition 타입 계층 + +에이전트 정의는 세 가지 소스에서 로드된다. + +``` +AgentDefinition (유니언 타입) +├── BuiltInAgentDefinition (source: 'built-in') +│ └── getSystemPrompt(params) — toolUseContext 접근 가능 +├── CustomAgentDefinition (source: 'userSettings' | 'projectSettings' | ...) +│ └── getSystemPrompt() — 클로저(closure)로 마크다운 내용 캡처 +└── PluginAgentDefinition (source: 'plugin') + └── getSystemPrompt() — 플러그인 메타데이터 포함 +``` + +`getSystemPrompt`가 함수인 이유는 메모리(memory) 기능이 활성화된 경우 에이전트 메모리 프롬프트를 동적으로 결합해야 하기 때문이다. 정적 문자열이 아닌 클로저를 사용하여 이를 구현한다. + +**에이전트 우선순위 (덮어쓰기 순서):** + +``` +built-in < plugin < userSettings < projectSettings < flagSettings < policySettings +``` + +동일한 `agentType` 이름이 여러 소스에 존재하면 우선순위가 높은 소스가 낮은 소스를 덮어쓴다. 예를 들어 프로젝트 설정의 커스텀 에이전트는 빌트인 에이전트를 같은 이름으로 오버라이드할 수 있다. + +### 4.3 에이전트 로딩: `loadAgentsDir.ts` + +`getAgentDefinitionsWithOverrides(cwd)`는 `lodash-es/memoize`로 메모화(memoize)되어 동일한 작업 디렉터리에 대해 한 번만 파일시스템을 읽는다. + +마크다운 에이전트 파일 프론트매터(frontmatter) 구조: + +```yaml +--- +name: my-agent +description: 이 에이전트를 사용할 때 +model: sonnet +effort: high +permissionMode: default +isolation: worktree +memory: user +background: false +maxTurns: 50 +tools: [Bash, Read, Edit] +--- +에이전트 시스템 프롬프트 내용... +``` + +`parseAgentFromMarkdown()`은 다음을 수행한다. + +- `isolation: 'remote'`는 `USER_TYPE === 'ant'`(Anthropic 내부 빌드)에서만 허용한다. +- `memory` 필드가 설정되면 `FILE_WRITE_TOOL_NAME`, `FILE_EDIT_TOOL_NAME`, `FILE_READ_TOOL_NAME`을 도구 목록에 자동 주입한다. +- `HooksSchema`로 훅(hook)을 파싱한다. `HooksSchema`가 lazy인 이유는 `AppState → loadAgentsDir → settings/types`의 순환 의존성을 모듈 로드 시점에 끊기 위해서다. + +### 4.4 Fork Subagent (포크 서브에이전트) + +`forkSubagent.ts`는 부모 에이전트의 전체 대화 컨텍스트를 상속받는 자식 에이전트를 생성하는 실험적 기능을 구현한다. + +```typescript +export function isForkSubagentEnabled(): boolean { + if (feature('FORK_SUBAGENT')) { + if (isCoordinatorMode()) return false // 코디네이터와 상호 배타적 + if (getIsNonInteractiveSession()) return false + return true + } + return false +} +``` + +**포크 메커니즘의 핵심: 프롬프트 캐시 공유** + +모든 포크 자식은 바이트 동일(byte-identical)한 API 요청 접두사를 생성해야 캐시 히트(cache hit)가 가능하다. `buildForkedMessages()`는 이를 위해 다음 구조를 만든다. + +``` +[...부모 히스토리, assistant(모든_tool_use 블록), user(플레이스홀더_results..., 자식별_지시문)] +``` + +모든 `tool_result` 블록에 동일한 플레이스홀더 텍스트 `"Fork started — processing in background"`를 사용하고, 오직 마지막 텍스트 블록만 자식별로 다르게 함으로써 캐시 히트율을 극대화한다. + +**포크 자식 규칙 (buildChildMessage에서 강제):** + +포크 자식 메시지는 `` 태그로 시작하며 다음을 강제한다. +- 서브에이전트를 재귀적으로 생성하지 말 것 +- 도구 호출 사이에 텍스트를 출력하지 말 것 +- 응답은 반드시 "Scope:"로 시작할 것 +- 파일을 수정한 경우 커밋 후 해시를 보고할 것 + +재귀 포크 방지는 `isInForkChild()`가 대화 히스토리에서 `` 태그를 탐지하는 방식으로 구현된다. + +### 4.5 에이전트 실행: `runAgent.ts` + +`runAgent.ts`는 서브에이전트의 전체 실행 수명주기(lifecycle)를 관리한다. + +**에이전트 전용 MCP 서버 초기화:** + +에이전트 정의의 프론트매터에 `mcpServers`가 지정된 경우, `initializeAgentMcpServers()`가 부모의 MCP 클라이언트에 추가로 에이전트 전용 서버를 연결한다. 에이전트 종료 시 정리 함수가 호출되어 연결을 해제한다. + +**파일 상태 캐시 상속:** + +서브에이전트는 부모의 파일 상태 캐시를 `cloneFileStateCache()`로 복제하여 시작한다. 이를 통해 부모가 이미 읽은 파일 정보를 서브에이전트가 재활용할 수 있다. + +**도구 풀(tool pool) 구성:** + +`resolveAgentTools()`는 에이전트 정의의 `tools`(허용 목록) 및 `disallowedTools`(거부 목록)을 부모의 도구 풀에 적용하여 서브에이전트의 최종 도구 집합을 결정한다. + +### 4.6 에이전트 색상 관리 + +멀티에이전트 UI에서 각 에이전트를 시각적으로 구별하기 위해 `agentColorManager.ts`가 에이전트 타입별 색상을 관리한다. 에이전트 정의의 `color` 필드나 자동 할당을 통해 색상이 지정된다. + +--- + +## 5. 팀 시스템 + +### 5.1 TeamCreateTool + +`TeamCreateTool`은 에이전트 스웜(swarm) 기능이 활성화된 경우(`isAgentSwarmsEnabled()`)에만 사용 가능하다. + +```typescript +// 팀 생성 입력 파라미터 +z.strictObject({ + team_name: z.string(), // 팀 이름 + description: z.string().optional(), + agent_type: z.string().optional(), // 팀 리더의 역할 타입 +}) +``` + +**팀 생성 시 수행 작업:** + +1. **팀 파일 생성**: `TeamFile` 구조를 파일시스템에 기록한다. 동일 이름의 팀이 이미 존재하면 새로운 고유 이름을 자동 생성한다. +2. **태스크 디렉터리 초기화**: `resetTaskList()`와 `ensureTasksDir()`로 팀 전용 태스크 목록 디렉터리를 생성한다. 팀마다 태스크 번호가 1부터 시작한다. +3. **리더 팀 이름 등록**: `setLeaderTeamName()`을 호출하여 `getTaskListId()`가 세션 ID 대신 팀 이름을 반환하도록 한다. +4. **AppState 업데이트**: `teamContext`를 설정하여 현재 세션이 팀 컨텍스트를 인식하게 한다. +5. **세션 정리 등록**: `registerTeamForSessionCleanup()`으로 세션 종료 시 팀 디렉터리가 자동 정리되도록 한다. + +**리더(leader)와 팀메이트(teammate) 구분:** + +팀 리더는 `CLAUDE_CODE_AGENT_ID` 환경 변수를 설정하지 않는다. 이를 통해 `isTeammate()`가 리더에 대해 `false`를 반환하고, 인박스(inbox) 폴링(polling)을 비롯한 팀메이트 전용 동작이 리더에게는 적용되지 않는다. + +### 5.2 TeamDeleteTool + +팀 삭제는 입력 파라미터가 없는 단순한 정리 작업이다. + +**삭제 전 안전 검사:** + +```typescript +const activeMembers = nonLeadMembers.filter(m => m.isActive !== false) +if (activeMembers.length > 0) { + // 활성 멤버가 있으면 삭제 거부 +} +``` + +`isActive !== false`로 판단하는 이유는 `isActive`가 정의되지 않은 멤버(undefined)를 활성 상태로 간주하기 때문이다. `false`로 명시적으로 설정된 경우에만 유휴/종료 상태로 판단한다. + +**정리 순서:** + +1. 팀 파일 및 관련 디렉터리 삭제 (`cleanupTeamDirectories`) +2. 세션 정리 목록에서 제거 (`unregisterTeamForSessionCleanup`) +3. 에이전트 색상 할당 초기화 (`clearTeammateColors`) +4. 리더 팀 이름 초기화 (`clearLeaderTeamName`) +5. AppState에서 `teamContext` 및 `inbox` 제거 + +### 5.3 SendMessageTool (에이전트 간 통신) + +`SendMessageTool`은 여러 통신 경로를 단일 인터페이스로 통합한다. + +**메시지 라우팅 결정 트리:** + +``` +SendMessageTool(to, message) +├── to == "bridge:" → 원격 제어 세션 (사용자 승인 필요) +├── to == "uds:" → 로컬 UDS 소켓 전송 +├── to == "*" → 팀 전체 브로드캐스트 +├── to == agentId (로컬 태스크) → queuePendingMessage() 또는 resumeAgentBackground() +└── to == teammateName → writeToMailbox() (파일시스템 기반) +``` + +**인박스 기반 통신의 특성:** + +`writeToMailbox()`는 파일시스템의 팀 디렉터리에 메시지를 기록한다. 수신자는 자신의 인박스를 주기적으로 폴링하여 메시지를 처리한다. 이 비동기 방식은 tmux 세션이나 별도 프로세스로 실행되는 팀메이트와의 통신에 적합하다. + +**로컬 서브에이전트 메시지 큐잉:** + +로컬 에이전트 태스크(`LocalAgentTask`)가 실행 중인 경우 메시지를 즉시 전달하는 것이 아니라 `queuePendingMessage()`로 큐에 추가한다. 에이전트는 다음 도구 호출 라운드에서 큐의 메시지를 처리한다. + +**정지된 에이전트 자동 재개:** + +에이전트가 정지 상태(stopped)인 경우 `SendMessageTool`이 자동으로 `resumeAgentBackground()`를 호출하여 에이전트를 재개한다. 에이전트가 태스크 레지스트리에서 제거된 경우에도 디스크의 트랜스크립트(transcript)에서 복원할 수 있다. + +**구조화 메시지(structured message) 타입:** + +- `shutdown_request`: 팀메이트에게 종료 요청 전송 +- `shutdown_response` (approve/reject): 종료 요청에 대한 응답 +- `plan_approval_response`: 플랜 모드(plan mode)에서 팀 리더의 승인/거부 + +--- + +## 6. 태스크 시스템 + +### 6.1 태스크 타입 계층 + +`src/tasks/types.ts`에 정의된 `TaskState` 유니언 타입은 모든 태스크 타입을 포함한다. + +```typescript +export type TaskState = + | LocalShellTaskState // 로컬 셸 태스크 (Bash 실행) + | LocalAgentTaskState // 로컬 에이전트 태스크 (AgentTool 생성) + | RemoteAgentTaskState // 원격 에이전트 태스크 (CCR) + | InProcessTeammateTaskState // 인-프로세스 팀메이트 + | LocalWorkflowTaskState // 로컬 워크플로 태스크 + | MonitorMcpTaskState // MCP 모니터 태스크 + | DreamTaskState // Dream 태스크 +``` + +### 6.2 LocalAgentTask: 로컬 에이전트 실행 + +`LocalAgentTask`는 `AgentTool`로 생성된 서브에이전트의 비동기 실행을 관리한다. + +**진행 상황 추적 (`ProgressTracker`):** + +```typescript +export type ProgressTracker = { + toolUseCount: number; + latestInputTokens: number; // 누적 입력 (API가 매 턴 누적값 반환) + cumulativeOutputTokens: number; // 매 턴 출력 토큰의 합산 + recentActivities: ToolActivity[]; +}; +``` + +입력 토큰과 출력 토큰을 별도로 추적하는 이유는 Claude API의 특성 때문이다. `input_tokens`는 매 요청마다 이전 컨텍스트를 포함한 누적값을 반환하므로 최신값을 유지하고, `output_tokens`는 턴별 값이므로 누산해야 한다. + +**활동 설명 해석기 (`ActivityDescriptionResolver`):** + +도구 이름과 입력을 받아 "src/foo.ts 읽기"와 같은 사람이 읽을 수 있는 활동 설명을 반환하는 함수다. 도구의 `getActivityDescription()` 메서드를 호출하여 생성하며, 최근 5개의 활동을 유지한다. + +**태스크 완료 알림:** + +에이전트 실행이 완료되면 다음 구조의 XML을 생성하여 부모에게 알린다. + +```xml + + {agentId} + completed|failed|killed + {상태 요약} + {에이전트의 최종 텍스트 응답} + + N + N + N + + +``` + +### 6.3 RemoteAgentTask: 원격 실행 + +`RemoteAgentTask`는 원격 CCR(Claude Code Remote) 환경에서 실행되는 에이전트를 관리한다. + +```typescript +const REMOTE_TASK_TYPES = [ + 'remote-agent', // 일반 원격 에이전트 + 'ultraplan', // 대형 계획 수립 + 'ultrareview', // 코드 리뷰 + 'autofix-pr', // PR 자동 수정 + 'background-pr', // 백그라운드 PR 처리 +] as const; +``` + +원격 태스크는 `pollRemoteSessionEvents()`로 원격 세션의 이벤트를 폴링하며, 완료 시 `archiveRemoteSession()`을 호출한다. `RemoteTaskCompletionChecker` 콜백을 등록하여 태스크 타입별 완료 조건을 외부에서 주입할 수 있다. + +### 6.4 백그라운드 태스크 판별 + +```typescript +export function isBackgroundTask(task: TaskState): task is BackgroundTaskState { + if (task.status !== 'running' && task.status !== 'pending') { + return false + } + if ('isBackgrounded' in task && task.isBackgrounded === false) { + return false // 포그라운드 태스크는 백그라운드 표시기에 나타나지 않음 + } + return true +} +``` + +`isBackgrounded === false`(명시적 false)와 `isBackgrounded === undefined`(기본값)를 구분하는 것이 중요하다. undefined인 태스크는 백그라운드로 간주한다. + +--- + +## 7. 주요 설계 결정 + +### 7.1 피처 플래그를 통한 코디네이터 모드 격리 + +멀티에이전트 기능은 `feature('COORDINATOR_MODE')` 빌드 타임 플래그로 전체 코드 경로를 조건부 컴파일한다. 이는 단순히 런타임 기능 토글이 아닌 **데드 코드 제거(dead code elimination)**를 위한 선택이다. + +`AgentTool.tsx`의 팀메이트 관련 코드도 동일한 패턴을 사용한다. + +```typescript +// 타입 정의는 TypeScript 컴파일 시 제거되므로 허용 +type TeammateSpawnedOutput = { ... } // 타입은 괜찮음 + +// 런타임 상수는 인라인 게이트 블록 내부에서만 정의 +// "Multi-agent type constants are defined inline inside gated blocks +// to enable dead code elimination" +``` + +이 접근법은 멀티에이전트 기능이 비활성화된 빌드에서 번들 크기와 공격 표면(attack surface)을 최소화한다. + +### 7.2 순환 의존성 방지를 위한 Lazy require() + +`AgentTool.tsx`에서 Proactive 모듈을 동적으로 로드하는 패턴이 사용된다. + +```typescript +const proactiveModule = feature('PROACTIVE') || feature('KAIROS') + ? require('../../proactive/index.js') as typeof import('../../proactive/index.js') + : null; +``` + +정적 `import` 대신 런타임 `require()`를 사용하는 것은 모듈 로드 시점의 순환 의존성을 끊기 위해서다. `HooksSchema`의 lazy 선언, scratchpad 게이트의 중복 구현도 같은 이유에서 비롯된다. + +### 7.3 에이전트 격리: 컨텍스트 독립성 + +각 서브에이전트는 다음을 독립적으로 소유한다. + +- 자체 `QueryEngine` 인스턴스 +- 부모로부터 복제된 파일 상태 캐시 +- 에이전트 정의에 명시된 도구 집합 +- (선택적) 격리된 Git Worktree + +이 격리는 서브에이전트가 부모 컨텍스트를 오염시키거나 간섭하는 것을 방지한다. 부모와 자식 간의 유일한 통신 채널은 `task-notification` XML 메시지다. + +### 7.4 Worktree 통합 + +`isolation: 'worktree'`가 지정된 에이전트는 `createAgentWorktree()`로 임시 Git Worktree를 생성한다. 에이전트 종료 시: + +- 변경 사항이 없으면 `removeAgentWorktree()`로 즉시 정리 +- 변경 사항이 있으면 Worktree 경로와 브랜치를 결과에 포함하여 반환 + +이를 통해 병렬로 실행되는 에이전트들이 서로의 파일 변경에 간섭하지 않는다. + +### 7.5 ONE_SHOT_BUILTIN_AGENT_TYPES 최적화 + +```typescript +export const ONE_SHOT_BUILTIN_AGENT_TYPES: ReadonlySet = new Set([ + 'Explore', + 'Plan', +]) +``` + +`Explore`와 `Plan` 에이전트는 한 번 실행하고 결과를 보고하는 단발성 에이전트다. 이 에이전트들의 결과에는 `agentId`/`SendMessage`/`usage` 트레일러를 생략한다. 주석에 따르면 이를 통해 에이전트당 약 135자를 절약하며, 주당 3,400만 회 실행되는 Explore 에이전트 규모에서 상당한 토큰 절약이 가능하다. + +### 7.6 에이전트 목록의 어태치먼트 메시지 전환 + +```typescript +export function shouldInjectAgentListInMessages(): boolean { + return getFeatureValue_CACHED_MAY_BE_STALE('tengu_agent_list_attach', false) +} +``` + +에이전트 목록이 도구 설명(tool description) 안에 인라인으로 포함되면 MCP 연결, 플러그인 리로드, 권한 모드 변경 시마다 도구 스키마가 바뀌어 전체 프롬프트 캐시가 무효화된다. `tengu_agent_list_attach` 게이트가 활성화되면 에이전트 목록을 별도의 어태치먼트 메시지로 분리하여 도구 스키마를 정적으로 유지한다. 이는 플릿 전체 `cache_creation_tokens`의 10.2%를 차지하는 문제를 해결한다. + +--- + +## 8. 정리: 오케스트레이션 흐름 요약 + +완전한 멀티에이전트 작업 흐름은 다음과 같다. + +1. **진입**: 사용자 요청이 코디네이터 모드의 메인 에이전트에게 전달된다. +2. **연구 단계**: 메인 에이전트가 여러 `AgentTool` 호출을 단일 메시지에 포함하여 병렬 워커를 생성한다. +3. **비동기 실행**: 각 워커는 `LocalAgentTask`로 등록되어 독립적으로 실행된다. +4. **결과 수신**: 워커 완료 시 `task-notification` XML이 사용자 롤 메시지로 메인 에이전트에게 전달된다. +5. **종합**: 메인 에이전트가 결과를 분석하고 구체적인 구현 명세를 작성한다. +6. **계속**: `SendMessageTool(to: agentId)`로 기존 워커를 재개하거나 새 워커를 생성한다. +7. **팀 관리**: 필요시 `TeamCreateTool`로 영속적인 팀 컨텍스트를 생성하고, 작업 완료 후 `TeamDeleteTool`로 정리한다. + +--- + +## 참고 자료 + +- `src/coordinator/coordinatorMode.ts` — 코디네이터 모드 활성화, 시스템 프롬프트, 세션 재개 로직 +- `src/tools/AgentTool/AgentTool.tsx` — 에이전트 생성, 백그라운드 타임아웃, 출력 스키마 +- `src/tools/AgentTool/loadAgentsDir.ts` — `AgentDefinition` 타입 계층, 마크다운/JSON 파싱 +- `src/tools/AgentTool/forkSubagent.ts` — 포크 메커니즘, 캐시 공유, 재귀 방지 +- `src/tools/AgentTool/prompt.ts` — 에이전트 도구 프롬프트 생성, 어태치먼트 전환 +- `src/tools/AgentTool/runAgent.ts` — 에이전트 수명주기, MCP 서버, 도구 풀 구성 +- `src/tools/SendMessageTool/SendMessageTool.ts` — 에이전트 간 메시지 라우팅 +- `src/tools/TeamCreateTool/TeamCreateTool.ts` — 팀 생성, 태스크 디렉터리 초기화 +- `src/tools/TeamDeleteTool/TeamDeleteTool.ts` — 팀 정리, 안전 검사 +- `src/tasks/LocalAgentTask/LocalAgentTask.tsx` — 로컬 비동기 에이전트 태스크 관리 +- `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx` — 원격 CCR 에이전트 태스크 +- `src/tasks/types.ts` — `TaskState` 유니언 타입 정의 + +--- + +## Navigation + +- 이전: [권한 시스템](permission-system.md) +- 상위: [목차](../README.md) diff --git a/docs/ko/level-2-systems/command-system.md b/docs/ko/level-2-systems/command-system.md new file mode 100644 index 0000000..7ec37ec --- /dev/null +++ b/docs/ko/level-2-systems/command-system.md @@ -0,0 +1,459 @@ +# 커맨드 시스템: 슬래시 커맨드 분석 + +## 1. 개요 + +커맨드 시스템은 사용자가 `/commit`, `/mcp`, `/review` 처럼 슬래시(`/`)로 시작하는 명령어를 입력할 때 이를 해석하고 실행하는 계층이다. 단순한 UI 단축키가 아니라 Claude Code의 모든 사용자 대면 기능을 조율하는 중앙 디스패처 역할을 수행한다. + +**역할 요약:** +- **등록**: 내장(built-in) 커맨드, 스킬(skill) 디렉터리, 플러그인, MCP 서버, 워크플로 스크립트를 단일 커맨드 풀로 통합 +- **필터링**: 인증 공급자 가용성(availability), feature flag(`bun:bundle`의 `feature()`), 환경 변수 조건을 거쳐 현재 세션에서 사용 가능한 커맨드 목록을 결정 +- **실행**: 커맨드 타입(`prompt` / `local` / `local-jsx`)에 따라 서로 다른 실행 경로로 위임 +- **캐시 관리**: `lodash-es/memoize`를 사용하여 디스크 I/O가 수반되는 동적 로딩을 메모이제이션 + +**핵심 파일:** + +| 파일 | 역할 | +|------|------| +| `src/commands.ts` | 레지스트리, 필터링, 공개 API 전체 | +| `src/types/command.ts` | `Command` 타입 계층 정의 | +| `src/commands/*/index.ts` | 개별 커맨드 구현체 (100개 이상) | + +--- + +## 2. 아키텍처 다이어그램 + +```mermaid +graph TD + User["사용자 입력\n(/command args)"] --> Resolver["findCommand()\n이름·별칭(alias) 탐색"] + Resolver --> Avail["meetsAvailabilityRequirement()\n인증 공급자 확인"] + Avail --> Enabled["isCommandEnabled()\nfeature flag / isEnabled()"] + Enabled --> Dispatch{커맨드 타입} + + Dispatch -- "type: 'prompt'" --> Prompt["getPromptForCommand()\n프롬프트 확장 → LLM 전달"] + Dispatch -- "type: 'local'" --> Local["load() → call()\n동기적 로컬 실행"] + Dispatch -- "type: 'local-jsx'" --> JSX["load() → call()\nInk JSX 렌더링"] + + subgraph "loadAllCommands() — memoized by cwd" + Bundled["getBundledSkills()"] + BuiltinPlugin["getBuiltinPluginSkillCommands()"] + SkillDir["getSkillDirCommands(cwd)"] + Workflow["getWorkflowCommands(cwd)"] + PluginCmd["getPluginCommands()"] + PluginSkill["getPluginSkills()"] + COMMANDS["COMMANDS()\n내장 커맨드 배열"] + end + + Bundled --> Pool["커맨드 풀\n(우선순위 순 병합)"] + BuiltinPlugin --> Pool + SkillDir --> Pool + Workflow --> Pool + PluginCmd --> Pool + PluginSkill --> Pool + COMMANDS --> Pool + + Pool --> Dynamic["getDynamicSkills()\n파일 작업 중 발견된 스킬"] + Dynamic --> getCommands["getCommands(cwd)\n최종 필터링된 커맨드 목록"] + getCommands --> Resolver + + subgraph "조건부 로딩 (feature flag)" + F1["feature('PROACTIVE') → proactive"] + F2["feature('KAIROS') → brief, assistant"] + F3["feature('BRIDGE_MODE') → bridge"] + F4["feature('VOICE_MODE') → voice"] + F5["feature('WORKFLOW_SCRIPTS') → workflows"] + F6["USER_TYPE=ant → agentsPlatform"] + end + + F1 & F2 & F3 & F4 & F5 & F6 --> COMMANDS +``` + +--- + +## 3. Command 타입 분석 + +`src/types/command.ts`에 정의된 타입 계층은 모든 커맨드가 공유하는 `CommandBase`와 실행 방식을 결정하는 세 가지 유니언 멤버로 구성된다. + +### 3.1 CommandBase — 공통 메타데이터 + +```typescript +type CommandBase = { + name: string // 슬래시 뒤에 오는 식별자 (/name) + description: string // 자동완성·도움말에 표시되는 설명 + aliases?: string[] // 대체 이름 (예: config의 'settings') + availability?: CommandAvailability[] // 'claude-ai' | 'console' (인증 게이팅) + isEnabled?: () => boolean // feature flag / 환경 조건 + isHidden?: boolean // 자동완성 목록에서 숨김 + argumentHint?: string // 인수 힌트 (회색 미리보기 텍스트) + whenToUse?: string // 모델 호출 시나리오 설명 (스킬 전용) + disableModelInvocation?: boolean // 모델이 직접 호출하지 못하도록 차단 + loadedFrom?: 'commands_DEPRECATED' | 'skills' | 'plugin' | 'managed' | 'bundled' | 'mcp' + source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled' + kind?: 'workflow' // 워크플로 배지 표시 + immediate?: boolean // 큐를 우회하여 즉시 실행 + isSensitive?: boolean // 인수를 대화 히스토리에서 삭제 +} +``` + +### 3.2 세 가지 실행 타입 + +| 타입 | 설명 | 실행 방식 | 대표 예시 | +|------|------|-----------|-----------| +| `prompt` | 프롬프트 템플릿을 LLM에 전달 | `getPromptForCommand()` → ContentBlockParam[] → 모델 쿼리 | `/commit`, `/review`, `/security-review` | +| `local` | 순수 로컬 함수 실행 | `load()` → `call()` → `LocalCommandResult` | `/compact`, `/cost`, `/clear` | +| `local-jsx` | Ink 기반 인터랙티브 UI 렌더링 | `load()` → `call()` → `React.ReactNode` | `/mcp`, `/config`, `/session`, `/plan` | + +#### PromptCommand 추가 필드 + +```typescript +type PromptCommand = { + progressMessage: string // 실행 중 표시할 메시지 + contentLength: number // 토큰 추정용 콘텐츠 길이 (동적이면 0) + allowedTools?: string[] // 이 커맨드 실행 중 허용되는 도구 목록 + model?: string // 특정 모델 지정 (없으면 세션 기본값 사용) + context?: 'inline' | 'fork' // 'fork': 별도 서브에이전트로 실행 + agent?: string // fork 컨텍스트에서 사용할 에이전트 타입 + paths?: string[] // 이 glob 패턴에 해당하는 파일 접근 후에만 표시 + hooks?: HooksSettings // 스킬 호출 시 등록할 훅 +} +``` + +#### LocalCommand 추가 필드 + +```typescript +type LocalCommand = { + supportsNonInteractive: boolean // CI/비대화형 모드 지원 여부 + load: () => Promise +} +``` + +#### LocalJSXCommand 추가 필드 + +```typescript +type LocalJSXCommand = { + load: () => Promise + // load()는 항상 지연 임포트(dynamic import)로 구현 + // → 무거운 UI 의존성을 커맨드 호출 시점까지 번들에서 분리 +} +``` + +--- + +## 4. 커맨드 레지스트리 + +### 4.1 직접 import (정적 로딩) + +`commands.ts` 상단에서 약 55개 커맨드를 ES 모듈 정적 임포트로 등록한다. 이 커맨드들은 번들 시점에 포함되며 런타임 조건 없이 항상 사용 가능하다. + +```typescript +import commit from './commands/commit.js' +import compact from './commands/compact/index.js' +import mcp from './commands/mcp/index.js' +// ... (약 55개) +``` + +각 커맨드 파일(예: `src/commands/mcp/index.ts`)은 커맨드 객체를 정의하고 무거운 구현체는 `load: () => import('./mcp.js')` 형태로 지연 임포트한다. 결과적으로 **메타데이터는 즉시**, **실제 로직은 필요 시** 로드된다. + +### 4.2 조건부 require (feature flag 게이팅) + +Bun 번들러의 `feature()` 함수와 CommonJS `require()`를 조합하여 특정 기능이 활성화된 경우에만 커맨드를 포함시킨다. `feature()`는 빌드 시 데드 코드 제거(dead code elimination)를 가능하게 한다. + +```typescript +// 단일 플래그 +const voiceCommand = feature('VOICE_MODE') + ? require('./commands/voice/index.js').default + : null + +// 복합 플래그 (OR) +const proactive = feature('PROACTIVE') || feature('KAIROS') + ? require('./commands/proactive.js').default + : null + +// 복합 플래그 (AND) +const remoteControlServerCommand = + feature('DAEMON') && feature('BRIDGE_MODE') + ? require('./commands/remoteControlServer/index.js').default + : null +``` + +null이 된 커맨드는 스프레드 연산자(`...(voiceCommand ? [voiceCommand] : [])`)로 `COMMANDS()` 배열에서 자동 제외된다. + +### 4.3 사용자 타입 게이팅 + +`USER_TYPE === 'ant'`인 경우에만 `INTERNAL_ONLY_COMMANDS`가 `COMMANDS()`에 포함된다. 이 배열에는 `backfillSessions`, `bughunter`, `commit`, `goodClaude`, `autofixPr` 등 Anthropic 내부 개발자 전용 커맨드 약 20개가 포함된다. + +```typescript +...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO + ? INTERNAL_ONLY_COMMANDS + : []), +``` + +### 4.4 가용성(Availability) 게이팅 + +`CommandAvailability` 타입으로 인증 공급자별 접근을 제한한다. + +| 값 | 대상 | +|----|------| +| `'claude-ai'` | claude.ai OAuth 구독자 (Pro/Max/Team/Enterprise) | +| `'console'` | Anthropic Console API 키 사용자 (api.anthropic.com 직접 접근) | + +`meetsAvailabilityRequirement()`는 매 `getCommands()` 호출 시 재평가된다(메모이제이션 없음). `/login` 이후 인증 상태가 변경되어도 즉시 반영된다. + +### 4.5 동적 로딩 소스 우선순위 + +`loadAllCommands()`는 다음 순서로 커맨드를 병합한다: + +``` +bundledSkills → builtinPluginSkills → skillDirCommands + → workflowCommands → pluginCommands → pluginSkills → COMMANDS() +``` + +나중에 추가된 커맨드가 이름 충돌 시 덮어쓰지 않도록 중복 제거는 `getCommands()`의 Set 기반 필터링으로 처리된다. + +### 4.6 지연 로딩 특례: /insights + +113KB에 달하는 `insights.ts` 모듈은 정적 임포트 없이 `commands.ts` 내부에서 직접 인라인 `prompt` 타입 커맨드로 정의되어 있다. + +```typescript +const usageReport: Command = { + type: 'prompt', + name: 'insights', + // ... + async getPromptForCommand(args, context) { + const real = (await import('./commands/insights.js')).default + return real.getPromptForCommand(args, context) + }, +} +``` + +이 패턴은 `getPromptForCommand()` 호출 시점에만 무거운 모듈을 로드하는 지연 심(lazy shim)이다. + +--- + +## 5. 커맨드 실행 흐름 + +### 5.1 커맨드 탐색 + +```typescript +function findCommand(commandName: string, commands: Command[]): Command | undefined { + return commands.find( + _ => + _.name === commandName || + getCommandName(_) === commandName || // userFacingName() 우선 + _.aliases?.includes(commandName), + ) +} +``` + +`getCommandName()`은 `cmd.userFacingName?.()`을 먼저 시도하여 플러그인이 접두사를 제거한 이름을 표시할 수 있도록 한다. 탐색 실패 시 `getCommand()`가 사용 가능한 모든 커맨드 이름을 나열한 `ReferenceError`를 던진다. + +### 5.2 prompt 타입 실행 흐름 + +``` +사용자 입력 /commit + → findCommand('commit') → commit 커맨드 객체 + → getPromptForCommand(args, context) 호출 + → executeShellCommandsInPrompt() 로 !`git status` 등 셸 명령 미리 실행 + → ContentBlockParam[] 반환 + → 반환된 프롬프트가 현재 대화에 삽입되어 LLM에 전달 + → LLM이 allowedTools 범위 내에서 도구 호출 수행 +``` + +`allowedTools` 필드는 해당 커맨드 실행 중에 사용 가능한 도구를 제한한다. `/commit`의 경우 `['Bash(git add:*)', 'Bash(git status:*)', 'Bash(git commit:*)']`로 git 관련 작업만 허용한다. + +### 5.3 local 타입 실행 흐름 + +``` +사용자 입력 /compact [instructions] + → findCommand('compact') → compact 커맨드 객체 + → compact.load() → import('./compact.js') (지연 임포트) + → module.call(args, context) 호출 + → LocalCommandResult 반환 + → { type: 'text', value: string } + → { type: 'compact', compactionResult, displayText? } + → { type: 'skip' } +``` + +`supportsNonInteractive: true`인 경우 `--print` 플래그 등 비대화형 모드에서도 실행 가능하다. + +### 5.4 local-jsx 타입 실행 흐름 + +``` +사용자 입력 /mcp enable server-name + → findCommand('mcp') → mcp 커맨드 객체 + → immediate: true → 큐 대기 없이 즉시 실행 + → mcp.load() → import('./mcp.js') (지연 임포트) + → module.call(onDone, context, args) 호출 + → React.ReactNode 반환 → Ink 렌더러가 터미널에 출력 + → 사용자 인터랙션 완료 후 onDone() 호출 + → display: 'skip' | 'system' | 'user' + → shouldQuery: true → 완료 후 LLM에 메시지 전달 +``` + +### 5.5 캐시 무효화 + +| 함수 | 용도 | +|------|------| +| `clearCommandMemoizationCaches()` | 커맨드 목록 캐시만 클리어 (스킬 캐시 유지) | +| `clearCommandsCache()` | 전체 캐시 클리어 (플러그인, 스킬 포함) | + +동적 스킬이 추가되면 `clearCommandMemoizationCaches()`를 호출하여 `loadAllCommands`와 `getSkillToolCommands`의 memoize 캐시를 무효화한다. `skillSearch/localSearch.ts`의 스킬 인덱스는 별도 메모이제이션 계층이므로 `clearSkillIndexCache?.()` 호출이 추가로 필요하다. + +--- + +## 6. 주요 커맨드 카테고리 + +### 6.1 Git / 코드 관리 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/commit` | prompt | git add/status/commit을 LLM이 수행 | +| `/commit-push-pr` | prompt | commit + push + PR 생성 원스텝 | +| `/review` | prompt | `gh pr` 기반 PR 코드 리뷰 | +| `/ultrareview` | local-jsx | 원격 bughunter 경로를 통한 심층 리뷰 | +| `/security-review` | prompt | 보안 취약점 집중 리뷰 | +| `/diff` | local-jsx | 현재 변경 사항 시각화 | +| `/branch` | local-jsx | 브랜치 관리 | +| `/autofix-pr` | prompt | PR 자동 수정 (내부 전용) | + +### 6.2 컨텍스트 / 메모리 관리 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/compact` | local | 대화 히스토리 압축, 요약 보존 | +| `/context` | local-jsx | 컨텍스트 창 관리 | +| `/memory` | local-jsx | 프로젝트/사용자 메모리 편집 | +| `/clear` | local | 대화 초기화 | +| `/summary` | local | 대화 요약 생성 | +| `/rewind` | local-jsx | 이전 대화 상태로 복귀 | +| `/files` | local | 추적 중인 파일 목록 | + +### 6.3 설정 / 환경 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/config` (= `/settings`) | local-jsx | 설정 패널 열기 | +| `/model` | local-jsx | 사용 모델 변경 | +| `/theme` | local-jsx | 터미널 테마 변경 | +| `/vim` | local | Vim 모드 토글 | +| `/keybindings` | local-jsx | 키 바인딩 관리 | +| `/effort` | local-jsx | 모델 사고 노력(effort) 조정 | +| `/output-style` | local-jsx | 출력 스타일 변경 | +| `/sandbox-toggle` | local-jsx | 샌드박스 모드 토글 | +| `/env` | local-jsx | 환경 변수 관리 | +| `/remote-env` | local-jsx | 원격 환경 변수 관리 | + +### 6.4 MCP / 플러그인 / 스킬 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/mcp` | local-jsx | MCP 서버 활성화/비활성화 | +| `/plugin` | local-jsx | 플러그인 관리 | +| `/reload-plugins` | local | 플러그인 캐시 재로드 | +| `/skills` | local-jsx | 사용 가능한 스킬 목록 조회 | +| `/hooks` | local-jsx | 훅 설정 관리 | +| `/permissions` | local-jsx | 도구 권한 관리 | + +### 6.5 세션 / 이력 관리 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/session` (= `/remote`) | local-jsx | 원격 세션 URL/QR 코드 표시 | +| `/resume` | local-jsx | 이전 세션 재개 | +| `/tasks` | local-jsx | 백그라운드 태스크 관리 | +| `/agents` | local-jsx | 에이전트 목록 조회 | +| `/export` | local-jsx | 대화 내보내기 | +| `/tag` | local-jsx | 세션 태그 관리 | + +### 6.6 AI 기능 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/plan` | local-jsx | 플랜 모드 활성화/세션 플랜 조회 | +| `/thinkback` | local-jsx | 사고 과정 재검토 | +| `/thinkback-play` | local-jsx | 사고 재생 | +| `/fast` | local-jsx | 빠른 응답 모드 | +| `/passes` | local-jsx | 멀티 패스 실행 설정 | +| `/advisor` | prompt | 코드 개선 조언 | +| `/bughunter` | local-jsx | 버그 탐색 (내부 전용) | + +### 6.7 설치 / 셋업 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/init` | prompt | 프로젝트 CLAUDE.md 초기화 | +| `/doctor` | local-jsx | 설치 상태 진단 | +| `/ide` | local-jsx | IDE 익스텐션 설치 | +| `/terminal-setup` | local-jsx | 터미널 환경 설정 | +| `/install-github-app` | local-jsx | GitHub 앱 설치 | +| `/install-slack-app` | local-jsx | Slack 앱 설치 | +| `/upgrade` | local-jsx | Claude Code 업그레이드 | +| `/desktop` | local-jsx | 데스크탑 앱 설정 | +| `/mobile` | local-jsx | 모바일 QR 코드 | +| `/chrome` | local-jsx | Chrome 익스텐션 설정 | + +### 6.8 인증 / 요금 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/login` | local-jsx | 인증 (3P 서비스 비사용 시만 표시) | +| `/logout` | local | 로그아웃 | +| `/cost` | local | 세션 비용 조회 | +| `/usage` | local-jsx | 사용량 정보 | +| `/extra-usage` | local-jsx | 추가 사용량 구매 | +| `/rate-limit-options` | local-jsx | 요금 한도 설정 | +| `/insights` | prompt | 세션 분석 리포트 (지연 로드) | + +### 6.9 유틸리티 / 기타 + +| 커맨드 | 타입 | 설명 | +|--------|------|------| +| `/help` | local-jsx | 도움말 | +| `/exit` | local | 종료 | +| `/status` | local-jsx | 시스템 상태 | +| `/statusline` | local | 상태 표시줄 토글 | +| `/copy` | local | 마지막 메시지 복사 | +| `/feedback` | local-jsx | 피드백 전송 | +| `/color` | local-jsx | 에이전트 색상 변경 | +| `/stickers` | local-jsx | 스티커 | +| `/btw` | local-jsx | 빠른 노트 | +| `/rename` | local-jsx | 세션 이름 변경 | +| `/stats` | local-jsx | 통계 조회 | +| `/release-notes` | local-jsx | 변경 이력 | +| `/privacy-settings` | local-jsx | 개인정보 설정 | +| `/add-dir` | local-jsx | 작업 디렉터리 추가 | + +--- + +## 7. 주요 설계 결정 + +### 7.1 타입 유니언 방식의 실행 다형성 + +커맨드 실행은 상속 계층이 아닌 구조적 타입 유니언(`prompt | local | local-jsx`)으로 구현된다. 각 타입은 서로 다른 함수 시그니처(`getPromptForCommand` / `call`)를 가지며, 호출 측에서 `cmd.type` 판별식으로 분기한다. 이 방식은 타입 안전성을 유지하면서도 새로운 실행 타입을 추가할 때 기존 커맨드를 수정하지 않아도 된다는 장점이 있다. + +### 7.2 메모이제이션과 신선도의 균형 + +`loadAllCommands(cwd)`는 `lodash-es/memoize`로 메모이제이션되어 디스크 I/O 비용을 절감한다. 반면 `meetsAvailabilityRequirement()`는 의도적으로 메모이제이션하지 않아 `/login` 이후 인증 상태 변경이 즉시 반영된다. 캐시 계층(memoize) 위에 신선한 필터링 계층을 두는 이중 구조가 핵심이다. + +### 7.3 bun:bundle feature()를 활용한 데드 코드 제거 + +`feature('VOICE_MODE')` 같은 호출은 Bun 번들러가 빌드 시 평가하여 비활성 브랜치를 번들에서 완전히 제거한다. 외부 배포본에서 `INTERNAL_ONLY_COMMANDS`에 해당하는 코드가 포함되지 않는 것도 같은 원리다. `/* eslint-disable @typescript-eslint/no-require-imports */` 주석과 함께 CommonJS `require()`를 사용하는 이유가 여기에 있다: ES 모듈 정적 임포트는 번들러가 조건부 제거를 적용할 수 없기 때문이다. + +### 7.4 원격 모드와 브리지 모드의 이중 안전 목록 + +`REMOTE_SAFE_COMMANDS`(18개)와 `BRIDGE_SAFE_COMMANDS`(6개)는 두 가지 다른 원격 실행 컨텍스트를 각각 제어한다. `REMOTE_SAFE_COMMANDS`는 CCR 초기화 전 TUI 사전 필터링에 사용되고, `BRIDGE_SAFE_COMMANDS`는 iOS/웹 클라이언트에서 들어오는 슬래시 커맨드의 안전 실행에 사용된다. `local-jsx` 타입은 Ink UI를 렌더링하므로 브리지에서 원천 차단된다(`isBridgeSafeCommand()`). + +### 7.5 스킬과 커맨드의 통합 레지스트리 + +스킬(skill)은 `type: 'prompt'`인 커맨드의 하위 집합으로, `loadedFrom` 필드(`'skills'`, `'bundled'`, `'plugin'`)와 `disableModelInvocation` 유무로 구분된다. `getSkillToolCommands()`는 모델이 직접 호출할 수 있는 커맨드를, `getSlashCommandToolSkills()`는 슬래시 커맨드 형태로 사용자가 호출하는 스킬을 필터링한다. 이 통합 구조 덕분에 동일한 커맨드 객체가 자동완성 UI와 모델 SkillTool 양쪽에서 재사용된다. + +### 7.6 formatDescriptionWithSource()의 UI/모델 분리 + +커맨드 설명은 사용자 대면 UI(자동완성, 도움말)와 모델 대면 프롬프트(SkillTool)에서 다르게 표시된다. UI에서는 `formatDescriptionWithSource()`가 출처 주석(`(plugin)`, `(bundled)`, `(workflow)`)을 추가하여 사용자가 커맨드 출처를 인식할 수 있게 한다. 모델 프롬프트에서는 `cmd.description`을 직접 사용하여 불필요한 메타 정보 없이 깔끔한 설명을 전달한다. + +--- + +## Navigation + +- 이전: [Tool 시스템: 도구 레지스트리 & 실행 파이프라인](./tool-system.md) +- 다음: [권한 시스템: 도구 실행 승인 흐름](./permission-system.md) +- 상위: [Level 2 시스템 분석 목록](../index.md) diff --git a/docs/ko/level-2-systems/permission-system.md b/docs/ko/level-2-systems/permission-system.md new file mode 100644 index 0000000..30b93ce --- /dev/null +++ b/docs/ko/level-2-systems/permission-system.md @@ -0,0 +1,365 @@ +# 권한 시스템: Permission Model 분석 + +## 1. 개요 + +Permission System (권한 시스템)은 Claude Code에서 Tool (도구)을 실행하기 전 사용자의 승인을 관리하는 보안 레이어다. 단순한 yes/no 프롬프트가 아니라, 다계층 규칙 기반(rule-based) 평가 파이프라인으로 설계되어 있으며, 현재 모드(mode)·규칙(rule)·분류기(classifier)·훅(hook)이 순차적으로 적용된다. + +**핵심 위치** + +| 관심사 | 경로 | +|---|---| +| 타입 정의 | `src/types/permissions.ts` | +| Context 타입 | `src/Tool.ts` (`ToolPermissionContext`) | +| 모드 정의 | `src/utils/permissions/PermissionMode.ts` | +| 권한 체크 로직 | `src/utils/permissions/permissions.ts` | +| 초기화 및 모드 전환 | `src/utils/permissions/permissionSetup.ts` | +| 거부 추적 | `src/utils/permissions/denialTracking.ts` | +| Auto 모드 분류기 | `src/utils/permissions/yoloClassifier.ts` | +| 모드 순환 | `src/utils/permissions/getNextPermissionMode.ts` | + +--- + +## 2. 아키텍처 다이어그램 + +```mermaid +flowchart TD + A[Tool 실행 요청] --> B{Permission Mode 확인} + + B -->|bypassPermissions| Z[즉시 허용] + B -->|plan| P[쓰기 도구 차단 / 읽기만 허용] + B -->|default / acceptEdits / dontAsk / auto| C[규칙 매칭 단계] + + C --> D{alwaysAllowRules 매칭?} + D -->|일치| Z + D -->|불일치| E{alwaysDenyRules 매칭?} + + E -->|일치| DENY[거부 — PermissionDenyDecision] + E -->|불일치| F{alwaysAskRules 매칭?} + + F -->|일치| ASK + F -->|불일치| G{Tool.checkPermissions 호출} + + G -->|allow| Z + G -->|deny| DENY + G -->|ask| ASK + + ASK --> H{shouldAvoidPermissionPrompts?} + H -->|true — 헤드리스 에이전트| HK[PermissionRequest Hook 실행] + HK -->|Hook 결정 있음| HOOKRESULT[Hook 결과 반환] + HK -->|Hook 결정 없음| DENY + H -->|false| I{auto 모드?} + + I -->|yes| J[YOLO Classifier 실행] + J -->|shouldBlock=false| Z + J -->|shouldBlock=true| ASK2[사용자 프롬프트 표시] + + I -->|no| ASK2 + + ASK2 --> K[사용자 결정] + K -->|허용| Z + K -->|거부| L[거부 기록 — DenialTracking] + L --> M{Denial 한계 도달?} + M -->|yes| N[프롬프트 폴백으로 전환] + M -->|no| DENY + + Z --> EXEC[Tool 실행] +``` + +--- + +## 3. Permission Mode 타입 + +Permission Mode (권한 모드)는 시스템 전체의 기본 동작 방침을 결정한다. 외부(External)에서 사용자가 설정할 수 있는 모드와 내부(Internal) 전용 모드로 구분된다. + +### 3.1 External Permission Mode + +`src/types/permissions.ts`에서 `EXTERNAL_PERMISSION_MODES`로 선언되며, 사용자 설정(`settings.json`의 `defaultMode`)과 CLI 플래그(`--permission-mode`)에서 사용할 수 있다. + +| 모드 | 심볼 | 색상 키 | 설명 | +|---|---|---|---| +| `default` | (없음) | `text` | 표준 동작. Tool별 `checkPermissions` 로직에 따라 ask/allow 결정 | +| `plan` | ⏸ | `planMode` | 읽기 전용 Plan Mode. 파일 쓰기·명령 실행 등 상태 변경 Tool 차단 | +| `acceptEdits` | ⏵⏵ | `autoAccept` | 파일 편집 자동 수락. Bash 실행은 여전히 확인 요구 | +| `bypassPermissions` | ⏵⏵ | `error` | 모든 권한 체크 우회. `isBypassPermissionsModeAvailable`이 `true`일 때만 활성화 가능 | +| `dontAsk` | ⏵⏵ | `error` | 권한 프롬프트 없이 거부 대신 자동 허용. `bypassPermissions`와 유사하나 의미론적으로 구분 | + +### 3.2 Internal Permission Mode + +`auto` 모드는 `TRANSCRIPT_CLASSIFIER` feature flag가 활성화된 ant(내부) 빌드에서만 사용 가능하다. 외부 공개 모드 목록(`EXTERNAL_PERMISSION_MODES`)에는 포함되지 않으며, `toExternalPermissionMode()`를 통해 `'default'`로 매핑된다. + +`bubble` 모드는 타입 정의에만 존재하며 현재 UI 순환에 노출되지 않는다. + +### 3.3 모드 순환 (Shift+Tab) + +`getNextPermissionMode()` 함수가 Shift+Tab 입력 시 다음 모드를 결정한다. + +``` +일반 사용자: default → acceptEdits → plan → [bypassPermissions →] [auto →] default +ant 내부: default → [bypassPermissions →] [auto →] default +``` + +`bypassPermissions`는 `isBypassPermissionsModeAvailable`이 `true`일 때만 순환에 포함되고, `auto`는 `isAutoModeAvailable`과 런타임 feature gate가 모두 활성화되어야 포함된다. + +--- + +## 4. ToolPermissionContext 분석 + +`ToolPermissionContext`는 `src/Tool.ts`에서 `DeepImmutable<{...}>`로 정의된다. `DeepImmutable` (깊은 불변 타입) 래퍼가 적용되어 런타임 중 권한 컨텍스트가 외부에서 변조되는 것을 타입 수준에서 방지한다. + +```typescript +export type ToolPermissionContext = DeepImmutable<{ + mode: PermissionMode + additionalWorkingDirectories: Map + alwaysAllowRules: ToolPermissionRulesBySource + alwaysDenyRules: ToolPermissionRulesBySource + alwaysAskRules: ToolPermissionRulesBySource + isBypassPermissionsModeAvailable: boolean + isAutoModeAvailable?: boolean + strippedDangerousRules?: ToolPermissionRulesBySource + shouldAvoidPermissionPrompts?: boolean + awaitAutomatedChecksBeforeDialog?: boolean + prePlanMode?: PermissionMode +}> +``` + +### 4.1 각 필드 설명 + +**`mode: PermissionMode`** +현재 활성 모드. 권한 평가의 첫 번째 분기점이다. + +**`additionalWorkingDirectories: Map`** +기본 작업 디렉터리 외에 파일 접근을 허용할 추가 경로 목록. 각 항목은 `path`와 `source`(어떤 설정에서 추가되었는지)를 포함한다. + +**`alwaysAllowRules / alwaysDenyRules / alwaysAskRules: ToolPermissionRulesBySource`** +세 가지 규칙 목록. 모두 `ToolPermissionRulesBySource` 타입으로, 규칙의 출처(source)별로 문자열 배열을 저장한다. + +```typescript +type ToolPermissionRulesBySource = { + [T in PermissionRuleSource]?: string[] +} +``` + +출처(`PermissionRuleSource`)는 `userSettings`, `projectSettings`, `localSettings`, `flagSettings`, `policySettings`, `cliArg`, `command`, `session` 중 하나다. 이 출처 정보는 `createPermissionRequestMessage()`에서 사용자에게 "어느 설정 파일의 규칙이 이 요청을 차단했는지" 설명할 때 사용된다. + +**`isBypassPermissionsModeAvailable: boolean`** +`bypassPermissions` 모드로 전환 가능한지 여부. 보안 민감 환경에서 `false`로 설정해 모드 접근 자체를 차단한다. + +**`isAutoModeAvailable?: boolean`** +Auto 모드 게이트(Statsig feature gate 등)를 통과했는지 캐시된 값. 런타임에 live check(`isAutoModeGateEnabled()`)와 함께 `canCycleToAuto()`에서 이중 검증된다. + +**`strippedDangerousRules?: ToolPermissionRulesBySource`** +Auto 모드 진입 시 제거된 위험 규칙의 원본 보관소. `isDangerousBashPermission()` 등이 탐지한 규칙들이 이 필드에 저장되며, 모드 이탈 시 복원에 사용된다. + +**`shouldAvoidPermissionPrompts?: boolean`** +`true`이면 사용자 인터페이스를 띄울 수 없는 헤드리스(headless) 에이전트 환경임을 의미한다. 이 경우 ask 결정은 프롬프트 대신 `PermissionRequest` 훅을 거쳐 자동 거부된다. + +**`awaitAutomatedChecksBeforeDialog?: boolean`** +`true`이면 사용자 다이얼로그 표시 전에 분류기·훅 등 자동화 체크가 완료될 때까지 대기한다. 코디네이터(coordinator) 워커 환경에서 활성화된다. + +**`prePlanMode?: PermissionMode`** +모델이 자체적으로 Plan Mode에 진입하기 전의 원래 모드를 저장한다. Plan Mode 이탈 시 이 값으로 복원한다. + +### 4.2 빈 컨텍스트 초기값 + +`getEmptyToolPermissionContext()`는 다음 기본값을 반환한다. + +```typescript +{ + mode: 'default', + additionalWorkingDirectories: new Map(), + alwaysAllowRules: {}, + alwaysDenyRules: {}, + alwaysAskRules: {}, + isBypassPermissionsModeAvailable: false, +} +``` + +--- + +## 5. 권한 체크 흐름 + +### 5.1 규칙 매칭 단계 + +`permissions.ts`의 `getAllowRules()`, `getDenyRules()`, `getAskRules()`는 각각 `PERMISSION_RULE_SOURCES` 순서(`userSettings` → `projectSettings` → `localSettings` → `flagSettings` → `policySettings` → `cliArg` → `command` → `session`)로 규칙을 평탄화(flatMap)하여 반환한다. + +**Tool 수준 매칭 (`toolMatchesRule`)** + +규칙에 `ruleContent`가 없으면 Tool 전체에 대한 매칭이다. MCP (Model Context Protocol) 도구는 `mcp__serverName__toolName` 형식으로 식별되며, 서버 수준 규칙(`mcp__server1`)은 해당 서버의 모든 도구에 적용된다. + +**콘텐츠 수준 매칭 (`getRuleByContentsForTool`)** + +`ruleContent`가 있는 경우 도구별 커스텀 매칭 로직(`Tool.checkPermissions()`)이 사용된다. 예를 들어 BashTool은 `Bash(git commit:*)` 형식으로 특정 명령 패턴에만 규칙을 적용한다. + +### 5.2 복합 명령 처리 + +Bash 등 복합 명령을 실행하는 도구는 `subcommandResults` 타입의 `PermissionDecisionReason`을 반환할 수 있다. 하나의 명령 라인에 여러 서브커맨드가 있을 때, 각각을 독립적으로 평가한 결과를 맵으로 반환한다. 어느 하나라도 `ask` 또는 `passthrough`이면 전체가 사용자 프롬프트로 에스컬레이션된다. + +### 5.3 PermissionDecision 타입 계층 + +``` +PermissionResult +├── PermissionDecision +│ ├── PermissionAllowDecision — behavior: 'allow' +│ ├── PermissionAskDecision — behavior: 'ask', message, suggestions?, pendingClassifierCheck? +│ └── PermissionDenyDecision — behavior: 'deny', message, decisionReason (필수) +└── passthrough — 상위 처리기로 위임 +``` + +`PermissionAskDecision`의 `suggestions` 필드에는 사용자가 규칙을 영구 저장할 수 있는 `PermissionUpdate` 목록이 포함될 수 있다. 예를 들어 "이 명령을 항상 허용"을 클릭하면 `{type: 'addRules', destination: 'projectSettings', ...}` 형태의 업데이트가 실행된다. + +--- + +## 6. 자동 모드 (Auto Mode) + +### 6.1 게이트 접근 검증 + +Auto 모드는 다중 게이트 구조로 보호된다. + +1. **Feature flag**: `TRANSCRIPT_CLASSIFIER` 빌드 플래그가 활성화된 빌드에서만 컴파일됨 +2. **사용자 타입**: `process.env.USER_TYPE === 'ant'`인 내부 사용자 전용 +3. **Statsig gate**: 런타임 feature gate 통과 필요 (`isAutoModeGateEnabled()`) +4. **모델 지원**: `modelSupportsAutoMode()`로 현재 메인 루프 모델이 auto 모드를 지원하는지 확인 +5. **보안 제한 게이트**: `checkSecurityRestrictionGate()` + +`verifyAutoModeGateAccess()` 함수가 세션 시작 시 위 조건들을 확인하고 결과를 `isAutoModeAvailable`에 캐시한다. + +### 6.2 YOLO Classifier + +`yoloClassifier.ts`의 YOLO (You Only Live Once) Classifier (분류기)는 Auto 모드에서 사용자 프롬프트 없이 도구 실행 여부를 자동 판단하는 LLM 기반 분류기다. + +`YoloClassifierResult` 타입은 다음 주요 필드를 포함한다. + +| 필드 | 설명 | +|---|---| +| `shouldBlock` | `true`이면 실행 차단 (사용자 프롬프트로 에스컬레이션) | +| `reason` | 판단 근거 | +| `unavailable` | API 오류 등으로 분류기 자체가 불가용한 경우 | +| `transcriptTooLong` | 컨텍스트 창 초과 — 동일 입력은 재시도해도 동일 결과이므로 즉시 폴백 | +| `model` | 분류에 사용된 모델 ID | +| `stage` | 2단계 XML 분류기에서 최종 결정을 낸 단계 (`'fast'` or `'thinking'`) | + +분류기는 세션의 대화 기록(transcript)을 컨텍스트로 사용하며, `getLastClassifierRequests()`로 이전 요청을 캐싱하여 반복 오버헤드를 줄인다. + +### 6.3 위험 권한 제거 (Dangerous Permission Stripping) + +Auto 모드 진입 시 `isDangerousBashPermission()` 함수가 `alwaysAllowRules`를 스캔해 분류기를 우회할 수 있는 위험 패턴을 제거한다. + +**위험으로 판단하는 패턴:** + +``` +Bash — 도구 전체 허용 (모든 명령 실행 가능) +Bash(*) — 와일드카드 전체 허용 +Bash(python:*) — 스크립트 인터프리터 prefix 허용 +Bash(python*) — 인터프리터 와일드카드 +Bash(node -*) — 플래그 포함 패턴 +``` + +제거된 규칙은 `strippedDangerousRules`에 보존되며, Auto 모드 이탈 시 복원된다. 동일 로직이 `isDangerousPowerShellPermission()` 함수로 PowerShell에도 적용된다. + +### 6.4 Classifier Fail-Closed + +분류기가 오류로 불가용 상태가 된 경우, 30분 새로고침 주기(`CLASSIFIER_FAIL_CLOSED_REFRESH_MS`)가 적용된다. 분류기 불가용 시 `buildClassifierUnavailableMessage()`가 메시지를 생성하고 ask 결정으로 폴백한다. + +--- + +## 7. 거부 추적 (Denial Tracking) + +`denialTracking.ts`는 분류기 또는 자동화된 체크가 반복적으로 거부할 때 사용자 프롬프트 폴백으로 전환하는 안전망이다. + +### 7.1 상태 구조 + +```typescript +type DenialTrackingState = { + consecutiveDenials: number // 연속 거부 횟수 + totalDenials: number // 총 거부 횟수 +} + +const DENIAL_LIMITS = { + maxConsecutive: 3, // 연속 3회 거부 시 폴백 + maxTotal: 20, // 총 20회 거부 시 폴백 +} +``` + +### 7.2 상태 전환 + +- `recordDenial(state)`: 거부 발생 시 두 카운터 모두 증가 (순수 함수, 새 객체 반환) +- `recordSuccess(state)`: 성공 시 `consecutiveDenials`를 0으로 리셋 (총 거부 수는 유지) +- `shouldFallbackToPrompting(state)`: 두 한계 중 하나라도 도달하면 `true` 반환 + +이 설계는 분류기가 특정 도구 요청을 지속적으로 잘못 판단하거나 과도하게 차단할 때 시스템이 무한 거부 루프에 빠지는 것을 방지한다. + +--- + +## 8. 주요 설계 결정 + +### 8.1 DeepImmutable을 통한 불변성 보장 + +`ToolPermissionContext`에 `DeepImmutable` 래퍼를 적용하는 이유는 권한 컨텍스트가 여러 계층의 도구 실행 코드에 전달될 때 의도치 않은 변경을 타입 수준에서 차단하기 위해서다. TypeScript의 표준 `readonly`는 최상위 속성만 보호하지만, `DeepImmutable`은 중첩 구조까지 재귀적으로 읽기 전용으로 만든다. + +`src/types/permissions.ts`에는 import cycle (순환 임포트) 문제를 피하기 위해 단순화된 `readonly` 버전의 `ToolPermissionContext`가 별도로 정의되어 있다. + +### 8.2 출처 추적 규칙 시스템 + +규칙을 단순 문자열 배열로 저장하지 않고 `ToolPermissionRulesBySource` 구조를 사용해 출처를 보존한다. 이를 통해: + +- 사용자에게 "어느 설정 파일의 어떤 규칙이 이 요청을 제어하는지" 명시적으로 안내할 수 있다 +- 프로젝트 설정과 사용자 설정의 규칙이 충돌할 때 우선순위를 관리할 수 있다 +- 규칙 삭제 시 올바른 파일에서 제거할 수 있다 (`deletePermissionRuleFromSettings`) + +### 8.3 Plan Mode 복원 메커니즘 + +모델이 대화 중 자체적으로 Plan Mode를 요청할 수 있다 (`prePlanMode` 필드). 이 경우 진입 전 모드를 `prePlanMode`에 저장하고 Plan Mode 이탈 시 복원한다. 이는 사용자가 수동으로 설정한 모드(`acceptEdits` 등)가 모델의 일시적 Plan Mode 진입으로 인해 유실되는 것을 방지한다. + +### 8.4 헤드리스 에이전트를 위한 안전한 폴백 + +`shouldAvoidPermissionPrompts`가 `true`인 환경(백그라운드 에이전트, CI 파이프라인 등)에서는 ask 결정이 사용자 UI를 띄우는 대신 `executePermissionRequestHooks()`를 거친다. 훅이 명시적 결정을 내리지 않으면 자동 거부(`AUTO_REJECT_MESSAGE`)로 처리된다. 이는 UI가 없는 환경에서 시스템이 무한 대기하거나 의도치 않게 위험한 작업을 승인하는 것을 방지하는 설계다. + +--- + +## 9. 주요 타입 요약 + +```typescript +// 권한 행동 — 규칙이 취하는 조치 +type PermissionBehavior = 'allow' | 'deny' | 'ask' + +// 규칙 값 — 어떤 도구의 어떤 명령에 적용하는지 +type PermissionRuleValue = { + toolName: string + ruleContent?: string // e.g. "git commit:*" +} + +// 규칙 — 행동 + 값 + 출처 +type PermissionRule = { + source: PermissionRuleSource + ruleBehavior: PermissionBehavior + ruleValue: PermissionRuleValue +} + +// 업데이트 — 규칙 추가/제거/변경 명령 +type PermissionUpdate = + | { type: 'addRules'; destination: PermissionUpdateDestination; rules: PermissionRuleValue[]; behavior: PermissionBehavior } + | { type: 'removeRules'; ... } + | { type: 'replaceRules'; ... } + | { type: 'setMode'; destination: ...; mode: ExternalPermissionMode } + | { type: 'addDirectories'; ... } + | { type: 'removeDirectories'; ... } + +// 결정 이유 — 왜 이 결정이 내려졌는지 +type PermissionDecisionReason = + | { type: 'rule'; rule: PermissionRule } + | { type: 'mode'; mode: PermissionMode } + | { type: 'hook'; hookName: string; reason?: string } + | { type: 'classifier'; classifier: string; reason: string } + | { type: 'subcommandResults'; reasons: Map } + | { type: 'workingDir'; reason: string } + | { type: 'safetyCheck'; reason: string; classifierApprovable: boolean } + | ... +``` + +--- + +## Navigation + +- 이전: [Tool 시스템](tool-system.md) +- 다음: [에이전트 오케스트레이션](agent-coordinator.md) +- 상위: [목차](../README.md) diff --git a/docs/ko/level-2-systems/query-engine.md b/docs/ko/level-2-systems/query-engine.md new file mode 100644 index 0000000..a20ee5d --- /dev/null +++ b/docs/ko/level-2-systems/query-engine.md @@ -0,0 +1,402 @@ +# QueryEngine: LLM API 엔진 분석 + +## 1. 개요 + +`QueryEngine`은 Claude Code의 핵심 실행 엔진으로, LLM(Large Language Model, 대형 언어 모델) API 호출, 에이전트 루프(agent loop) 관리, 스트리밍(streaming) 응답 처리를 단일 클래스에서 조율한다. SDK(Software Development Kit) 헤드리스 경로와 REPL(Read-Eval-Print Loop) 인터랙티브 경로 양쪽에서 하나의 대화 세션을 대표하는 객체로 인스턴스화된다. + +- **역할**: LLM API 호출, 에이전트 루프 관리, 스트리밍 응답 처리의 중앙 엔진 +- **위치**: `src/QueryEngine.ts` (1,295 lines) +- **설계 원칙**: 대화 하나 당 인스턴스 하나. `submitMessage()` 호출마다 새 턴(turn)이 시작되며, 메시지 히스토리·파일 캐시·사용량 집계는 턴 간에 유지된다. + +### 의존 관계 다이어그램 + +```mermaid +graph TD + QE["QueryEngine\n(src/QueryEngine.ts)"] + ASK["ask()\n편의 래퍼"] + QUERY["query()\n(src/query.ts)"] + CLAUDE["services/api/claude.ts\nAnthropics API 클라이언트"] + TOOLS["Tool 시스템\n(src/Tool.ts)"] + COST["cost-tracker.ts\n비용·토큰 집계"] + COMPACT["services/compact/\n컨텍스트 압축"] + FSC["FileStateCache\n파일 상태 캐시"] + SYSPROMPT["utils/queryContext.ts\nfetchSystemPromptParts()"] + TRANSCRIPT["utils/sessionStorage.ts\n트랜스크립트 기록"] + PERM["hooks/useCanUseTool.ts\n권한 관리"] + + ASK -->|"new QueryEngine()"| QE + QE -->|"submitMessage() → for await"| QUERY + QUERY -->|"deps.callModel()"| CLAUDE + QUERY -->|"runTools()"| TOOLS + QUERY -->|"autocompact / snip"| COMPACT + QE -->|"fetchSystemPromptParts()"| SYSPROMPT + QE -->|"getTotalCost() / getModelUsage()"| COST + QE -->|"cloneFileStateCache()"| FSC + QE -->|"recordTranscript()"| TRANSCRIPT + QE -->|"wrappedCanUseTool()"| PERM +``` + +--- + +## 2. 아키텍처 다이어그램 + +아래 다이어그램은 하나의 `submitMessage()` 호출이 완료될 때까지 데이터가 흐르는 경로를 보여준다. + +```mermaid +sequenceDiagram + participant Caller as SDK Caller + participant QE as QueryEngine + participant PUI as processUserInput() + participant QF as query() / queryLoop() + participant CM as callModel() (claude.ts) + participant Tools as Tool 실행 + participant CT as cost-tracker + + Caller->>QE: submitMessage(prompt) + QE->>QE: fetchSystemPromptParts() — 시스템 프롬프트 조립 + QE->>PUI: processUserInput(prompt) + PUI-->>QE: messagesFromUserInput, shouldQuery + QE->>QE: recordTranscript() — 사용자 메시지 즉시 저장 + QE->>QF: for await query({ messages, systemPrompt, ... }) + + loop 에이전트 루프 (tool_use → 재질의) + QF->>QF: applyToolResultBudget() — 도구 결과 크기 제한 + QF->>QF: autocompact / snip — 필요 시 컨텍스트 압축 + QF->>CM: callModel(messages, tools, ...) + CM-->>QF: stream_event / AssistantMessage + QF-->>QE: yield AssistantMessage / StreamEvent + QE->>CT: accumulateUsage() + alt stop_reason == tool_use + QF->>Tools: runTools() + Tools-->>QF: UserMessage (tool_result) + end + end + + QF-->>QE: result (Terminal) + QE-->>Caller: yield SDKResultMessage +``` + +### 서브시스템별 역할 요약 + +| 모듈 | 역할 | +|---|---| +| `src/QueryEngine.ts` | 세션 상태 유지, 턴 오케스트레이션, SDK 메시지 변환 | +| `src/query.ts` | API 호출 루프, 압축 트리거, 도구 실행 조율 | +| `src/services/api/claude.ts` | Anthropic 베타 API 클라이언트, 스트리밍 파싱 | +| `src/cost-tracker.ts` | 모델별 토큰·비용 누적 집계 | +| `src/services/compact/` | 자동 압축, 스닙(snip), 반응형 압축 | +| `src/utils/fileStateCache.ts` | 파일 읽기 캐시, 언두(undo) 스냅샷 | +| `src/utils/queryContext.ts` | 시스템 프롬프트 구성 요소 조회 | + +--- + +## 3. 핵심 타입/인터페이스 + +### `QueryEngineConfig` + +`QueryEngine` 생성자에 전달되는 구성 객체. 대화 생명주기 전체에서 불변으로 유지되는 값과 변경 가능한 콜백의 두 범주로 나뉜다. + +```typescript +export type QueryEngineConfig = { + // -- 실행 환경 + cwd: string // 현재 작업 디렉터리 + tools: Tools // 사용 가능한 도구 목록 + commands: Command[] // 슬래시 커맨드 목록 + mcpClients: MCPServerConnection[] // MCP(Model Context Protocol) 클라이언트 + agents: AgentDefinition[] // 에이전트 정의 목록 + + // -- 권한·상태 콜백 + canUseTool: CanUseToolFn // 도구 사용 가능 여부 판단 함수 + getAppState: () => AppState // 현재 앱 상태 읽기 + setAppState: (f: ...) => void // 앱 상태 업데이트 + + // -- 초기 상태 + initialMessages?: Message[] // 재개 시 이전 메시지 히스토리 + readFileCache: FileStateCache // 파일 읽기 캐시 (언두용) + + // -- 시스템 프롬프트 제어 + customSystemPrompt?: string // 기본 프롬프트 전체 교체 + appendSystemPrompt?: string // 기본 프롬프트에 추가 + + // -- 모델 선택 + userSpecifiedModel?: string // 사용자 지정 모델 (예: claude-opus-4) + fallbackModel?: string // API 오류 시 폴백 모델 + + // -- 실행 제한 + thinkingConfig?: ThinkingConfig // extended thinking 설정 + maxTurns?: number // 최대 에이전트 루프 턴 수 + maxBudgetUsd?: number // USD 비용 상한선 + taskBudget?: { total: number } // 태스크 토큰 예산 (beta API) + + // -- 출력 형식 + jsonSchema?: Record // 구조화 출력 스키마 + verbose?: boolean // 상세 로그 활성화 + + // -- SDK 전용 옵션 + replayUserMessages?: boolean // 사용자 메시지 재전송 여부 + includePartialMessages?: boolean // 스트림 이벤트 포함 여부 + handleElicitation?: ... // MCP URL elicitation 핸들러 + setSDKStatus?: (s: SDKStatus) => void // SDK 상태 콜백 + abortController?: AbortController // 외부에서 주입 가능한 취소 컨트롤러 + orphanedPermission?: OrphanedPermission // 고아(orphaned) 권한 처리 + snipReplay?: (msg, store) => ... // HISTORY_SNIP 기능 콜백 (주입식) +} +``` + +**주요 설계 결정**: `customSystemPrompt`가 존재하면 기본 시스템 프롬프트를 완전히 대체하고, `appendSystemPrompt`는 기본 프롬프트 뒤에 추가된다. SDK 호출자가 커스텀 프롬프트와 `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE`를 함께 설정했을 때는 메모리 메카닉스 프롬프트가 자동 주입된다. + +### `QueryEngine` 클래스 내부 상태 + +```typescript +class QueryEngine { + private config: QueryEngineConfig + private mutableMessages: Message[] // 턴 간 누적 메시지 히스토리 + private abortController: AbortController // 인터럽트 신호 + private permissionDenials: SDKPermissionDenial[] // 권한 거부 추적 + private totalUsage: NonNullableUsage // 누적 토큰 사용량 + private hasHandledOrphanedPermission: boolean + private discoveredSkillNames: Set // 턴-스코프 스킬 발견 추적 + private loadedNestedMemoryPaths: Set + private readFileState: FileStateCache // 언두 가능한 파일 상태 +} +``` + +--- + +## 4. 실행 흐름 + +### 4.1 설정 조립 (Config Assembly) + +`submitMessage()` 진입 시 `config`에서 구조 분해로 파라미터를 추출한다. `discoveredSkillNames`는 각 턴 시작 시 초기화되어 탐색된 스킬이 다음 턴으로 누적되지 않도록 한다. `setCwd(cwd)`를 호출해 작업 디렉터리를 전역 셸 상태와 동기화한다. + +### 4.2 시스템 프롬프트 구성 (`fetchSystemPromptParts`) + +```typescript +const { + defaultSystemPrompt, + userContext: baseUserContext, + systemContext, +} = await fetchSystemPromptParts({ + tools, + mainLoopModel: initialMainLoopModel, + additionalWorkingDirectories, + mcpClients, + customSystemPrompt: customPrompt, +}) + +const systemPrompt = asSystemPrompt([ + ...(customPrompt !== undefined ? [customPrompt] : defaultSystemPrompt), + ...(memoryMechanicsPrompt ? [memoryMechanicsPrompt] : []), + ...(appendSystemPrompt ? [appendSystemPrompt] : []), +]) +``` + +`fetchSystemPromptParts()`는 코디네이터 모드(`COORDINATOR_MODE`) 기능 플래그가 활성화된 경우 `getCoordinatorUserContext()`를 통해 `userContext`를 보강한다. 이 함수는 dead-code elimination(데드코드 제거)을 위해 `feature()` 조건부 `require()`로 지연 임포트된다. + +### 4.3 메시지 정규화 + +`processUserInput()`이 원시 프롬프트(문자열 또는 `ContentBlockParam[]`)를 처리해 내부 `Message` 타입으로 변환한다. 슬래시 커맨드가 포함된 경우 이 단계에서 실행된다. 처리 후 `messagesFromUserInput`이 `this.mutableMessages`에 추가된다. + +처리 결과인 `shouldQuery`가 `false`이면(로컬 커맨드만 있는 경우) API 호출 없이 즉시 결과를 반환한다. + +### 4.4 사전 트랜스크립트 기록 + +```typescript +// API 응답 전에 사용자 메시지를 즉시 저장 +if (persistSession && messagesFromUserInput.length > 0) { + const transcriptPromise = recordTranscript(messages) + if (isBareMode()) { + void transcriptPromise // 베어 모드에서는 비동기로 처리 + } else { + await transcriptPromise // 그 외에는 동기 대기 + } +} +``` + +프로세스가 API 응답 전에 종료되더라도 `--resume`이 가능하도록 사용자 메시지를 먼저 기록한다. 베어 모드(bare mode, `--bare` 플래그)에서는 약 4~30ms의 지연을 피하기 위해 fire-and-forget으로 처리한다. + +### 4.5 API 호출 (`query()` 함수) + +```typescript +for await (const message of query({ + messages, + systemPrompt, + userContext, + systemContext, + canUseTool: wrappedCanUseTool, + toolUseContext: processUserInputContext, + fallbackModel, + querySource: 'sdk', + maxTurns, + taskBudget, +})) { ... } +``` + +`query()`는 `src/query.ts`에 정의된 AsyncGenerator 함수로, 내부 `queryLoop()`를 호출한다. 루프 내에서 `deps.callModel()`(실제로는 `claude.ts`의 Anthropic 베타 API 클라이언트)을 통해 스트리밍 응답을 수신한다. + +### 4.6 스트리밍 응답 처리 + +`query()`가 yield하는 메시지 타입별 처리: + +| 메시지 타입 | 처리 방식 | +|---|---| +| `assistant` | `mutableMessages`에 추가, `normalizeMessage()` 후 yield | +| `user` | `mutableMessages`에 추가, turnCount 증가, yield | +| `stream_event` | `message_start`/`message_delta`/`message_stop` 이벤트로 토큰 누적 | +| `progress` | `mutableMessages`와 트랜스크립트에 인라인 기록 | +| `attachment` | 구조화 출력, max_turns_reached, queued_command 처리 | +| `system` | 압축 경계(compact_boundary), API 오류 재시도, snip 경계 처리 | +| `tool_use_summary` | SDK로 도구 사용 요약 전달 | +| `tombstone` | 고아 메시지 제거 신호 (스킵) | + +### 4.7 도구 사용 감지 및 실행 + +`query.ts`의 `queryLoop()` 내에서 `StreamingToolExecutor` 또는 `runTools()`를 통해 도구를 실행한다. 도구 결과는 `UserMessage`(tool_result)로 변환되어 다음 API 호출 메시지에 포함된다. + +`wrappedCanUseTool`은 `canUseTool`을 래핑하여 권한이 거부된 경우 `this.permissionDenials`에 기록한다. 이 정보는 최종 `SDKResultMessage`의 `permission_denials` 필드로 노출된다. + +### 4.8 재질의 루프 (에이전트 루프) + +`queryLoop()`는 `needsFollowUp`(tool_use 블록 존재 여부)이 `true`인 한 반복한다. 각 반복에서: + +1. `applyToolResultBudget()` — 도구 결과 크기를 예산 내로 제한 +2. `snipModule.snipCompactIfNeeded()` — HISTORY_SNIP 플래그 활성 시 스닙 적용 +3. 마이크로컴팩트(microcompact) 적용 +4. `autocompact` 판정 및 실행 +5. `callModel()` 호출 → 스트리밍 수신 +6. 도구 사용 블록 감지 → 도구 실행 → tool_result 생성 +7. `maxTurns` 초과 시 `max_turns_reached` attachment yield 후 반환 + +### 4.9 비용 추적 및 토큰 집계 + +```typescript +// stream_event 핸들러 내 +if (message.event.type === 'message_start') { + currentMessageUsage = updateUsage(EMPTY_USAGE, message.event.message.usage) +} +if (message.event.type === 'message_delta') { + currentMessageUsage = updateUsage(currentMessageUsage, message.event.usage) +} +if (message.event.type === 'message_stop') { + this.totalUsage = accumulateUsage(this.totalUsage, currentMessageUsage) +} +``` + +`cost-tracker.ts`의 `addToTotalSessionCost()`는 모델별로 토큰(입력/출력/캐시 읽기/캐시 생성)과 USD 비용을 누적 집계한다. 어드바이저(advisor) 모델이 있는 경우 재귀 호출로 어드바이저 사용량도 합산한다. 세션 비용은 `saveCurrentSessionCosts()`를 통해 프로젝트 설정에 저장되며 재개 시 `restoreCostStateForSession()`으로 복원된다. + +### 4.10 파일 히스토리 스냅샷 + +```typescript +if (fileHistoryEnabled() && persistSession) { + messagesFromUserInput + .filter(messageSelector().selectableUserMessagesFilter) + .forEach(message => { + void fileHistoryMakeSnapshot(...) + }) +} +``` + +사용자 메시지마다 파일 히스토리 스냅샷을 생성해 언두(undo) 기능을 지원한다. `readFileState` (`FileStateCache`)는 엔진 인스턴스 수명 동안 유지되며, `ask()` 래퍼가 완료 후 `setReadFileCache(engine.getReadFileState())`를 호출해 상위 컨텍스트로 전파한다. + +### 4.11 압축/컴팩션 트리거 + +`query.ts`의 `queryLoop()` 내에서 매 API 호출 전에 압축 조건을 확인한다: + +- **자동 압축 (autocompact)**: 토큰이 임계값을 초과하면 `deps.autocompact()`를 호출해 대화를 요약본으로 압축 +- **스닙 (HISTORY_SNIP)**: 특정 메시지 패턴을 감지해 히스토리 일부를 잘라냄 +- **반응형 압축 (REACTIVE_COMPACT)**: API에서 `prompt_too_long` 오류 수신 시 사후 압축 시도 +- **컨텍스트 콜랩스 (CONTEXT_COLLAPSE)**: 컨텍스트 창 초과 전에 단계적 축소 + +압축 완료 후 `compact_boundary` 시스템 메시지가 yield되고, `QueryEngine`은 이를 받아 `mutableMessages`에서 압축 전 메시지를 GC(garbage collection, 가비지 컬렉션) 해제한다. + +--- + +## 5. 주요 설계 결정 + +### 5.1 메모이즈드 컨텍스트 (`getSystemContext`, `getUserContext`) + +`fetchSystemPromptParts()`는 `getSystemContext()`와 `getUserContext()`를 내부적으로 사용한다. 이 함수들은 비용이 높은 파일 시스템 접근(CLAUDE.md, 플러그인, 메모리 디렉터리 등)을 한 번만 수행하고 결과를 메모이즈(memoize)한다. `submitMessage()` 내에서 매 턴마다 호출하더라도 성능에 영향을 주지 않는다. + +### 5.2 Dead Code Elimination (`bun:bundle` 기능 플래그) + +```typescript +// 예시: COORDINATOR_MODE 플래그 +const getCoordinatorUserContext = feature('COORDINATOR_MODE') + ? require('./coordinator/coordinatorMode.js').getCoordinatorUserContext + : () => ({}) +``` + +`feature()` 함수는 `bun:bundle`의 트리 쉐이킹(tree-shaking) 경계 역할을 한다. 플래그가 비활성화된 빌드에서는 해당 모듈 전체가 번들에서 제외된다. `QueryEngine.ts`와 `query.ts` 모두 이 패턴을 사용해 `HISTORY_SNIP`, `COORDINATOR_MODE`, `REACTIVE_COMPACT`, `CONTEXT_COLLAPSE` 등의 실험적 기능을 격리한다. + +### 5.3 순환 의존성 방지를 위한 지연 임포트 (Lazy Imports) + +```typescript +// React/ink를 풀지 않기 위해 지연 임포트 +const messageSelector = + (): typeof import('src/components/MessageSelector.js') => + require('src/components/MessageSelector.js') +``` + +`MessageSelector.tsx`는 React/ink를 의존하므로 최상위 임포트 시 테스트 셔드(shard)의 모듈 초기화 순서를 깨뜨릴 수 있다. `require()` 지연 임포트로 실제 사용 시점까지 로딩을 미룬다. `snipReplay` 콜백을 설정으로 주입하는 것도 같은 이유다: 기능 플래그 문자열이 `QueryEngine.ts`에 직접 포함되면 제외 문자열 검사를 통과하지 못한다. + +### 5.4 `FileStateCache`와 언두 기능 + +`FileStateCache`는 파일 경로를 키로 파일 내용의 과거 상태를 저장한다. `ask()` 편의 래퍼는 엔진 생성 시 `cloneFileStateCache(getReadFileCache())`로 독립적인 캐시 복사본을 만들고, 완료 후 `setReadFileCache(engine.getReadFileState())`로 변경 사항을 상위에 전파한다. 이 구조가 도구 실행이 변경한 파일을 다음 턴에서 정확히 읽을 수 있게 하고, 언두 기능의 기반이 된다. + +### 5.5 도구 결과 예산 및 콘텐츠 교체 + +`applyToolResultBudget()`은 누적된 도구 결과의 총 크기가 한 메시지당 예산을 초과하면 오래된 결과를 플레이스홀더로 교체한다. 교체 기록은 `recordContentReplacement()`를 통해 세션 스토리지에 저장되어 재개 시 복원이 가능하다. 마이크로컴팩트(cached microcompact)는 `tool_use_id` 기준으로만 동작하므로 콘텐츠 교체와 독립적으로 합성된다. + +### 5.6 고아 권한 처리 (Orphaned Permission) + +SDK 호출에서 이전 세션의 미결 권한 요청이 있을 경우, `submitMessage()` 첫 호출 시 단 한 번 `handleOrphanedPermission()`이 실행된다. `hasHandledOrphanedPermission` 플래그로 중복 처리를 방지한다. + +### 5.7 베어 모드와 트랜스크립트 전략 + +`isBareMode()`가 true이면 사용자 메시지 트랜스크립트 기록을 fire-and-forget으로 처리한다. 스크립트 호출은 `--resume` 후 재개가 필요 없으므로 약 4~30ms의 디스크 I/O 지연을 절약한다. 비대화형(non-interactive)이지만 코워크(cowork) 환경에서는 `CLAUDE_CODE_EAGER_FLUSH` 또는 `CLAUDE_CODE_IS_COWORK` 환경 변수로 `flushSessionStorage()`를 강제로 동기 호출한다. + +--- + +## 6. `ask()` 편의 래퍼 + +```typescript +export async function* ask({ prompt, cwd, tools, ... }) { + const engine = new QueryEngine({ ... }) + try { + yield* engine.submitMessage(prompt, { uuid: promptUuid, isMeta }) + } finally { + setReadFileCache(engine.getReadFileState()) + } +} +``` + +`ask()`는 단발성(one-shot) 사용을 위한 편의 래퍼다. 내부적으로 `QueryEngine`을 생성하고 `submitMessage()`를 위임한다. `finally` 블록에서 파일 상태 캐시를 항상 상위로 전파한다. `HISTORY_SNIP` 기능이 활성화된 경우 `snipReplay` 콜백을 주입해 기능 플래그 문자열이 `QueryEngine.ts`에 포함되지 않도록 한다. + +--- + +## 7. 결과 유형 (SDKResultMessage 서브타입) + +| 서브타입 | 원인 | +|---|---| +| `success` | 정상 완료 (`end_turn` 또는 tool_result 후 응답) | +| `error_max_turns` | `maxTurns` 초과 | +| `error_max_budget_usd` | `maxBudgetUsd` 초과 | +| `error_max_structured_output_retries` | 구조화 출력 재시도 한도 초과 | +| `error_during_execution` | 비정상 종료 (API 오류, 예기치 않은 stop_reason 등) | + +`error_during_execution`에는 진단 접두사(`[ede_diagnostic]`)와 해당 턴 내에서 발생한 인메모리(in-memory) 에러 로그가 `errors[]`에 포함된다. 워터마크(watermark) 기반으로 이전 턴의 에러가 포함되지 않도록 한다. + +--- + +## 8. 관련 문서 + +- **상위 개요**: [요청 생명주기](../level-1-overview/request-lifecycle.md) — `ask()`가 어떻게 호출되는지 +- **하위 상세**: Tool 시스템 — 도구 실행 메커니즘 +- **하위 상세**: 압축 시스템 — `services/compact/` 세부 분석 + +--- + +## Navigation + +- 상위: [목차](../README.md) +- 다음: [Tool 시스템](tool-system.md) diff --git a/docs/ko/level-2-systems/tool-system.md b/docs/ko/level-2-systems/tool-system.md new file mode 100644 index 0000000..9f0eae1 --- /dev/null +++ b/docs/ko/level-2-systems/tool-system.md @@ -0,0 +1,633 @@ +# Tool 시스템: 도구 레지스트리 & 실행 파이프라인 + +## 1. 개요 + +Tool 시스템은 Claude Code의 핵심 확장 메커니즘이다. LLM(Large Language Model, 대규모 언어 모델)이 텍스트 생성 외에 실제 작업을 수행할 수 있도록 40개 이상의 도구를 등록, 검증, 실행하는 파이프라인 전체를 관할한다. + +**역할 요약:** +- **등록**: 빌트인(built-in, 내장) 도구와 MCP(Model Context Protocol) 도구를 단일 풀(pool)로 조합 +- **검증**: Zod v4 스키마 기반 입력 유효성 검사 및 권한 확인 +- **실행**: `ToolUseContext`를 의존성 주입(dependency injection) 컨테이너로 활용하여 각 도구에 실행 환경 전달 +- **결과 처리**: 결과 포맷팅, 대용량 결과의 디스크 저장, UI 렌더링 + +**파일 구조:** + +| 파일 | 역할 | +|------|------| +| `src/Tool.ts` | 핵심 타입 및 인터페이스 정의 | +| `src/tools.ts` | 도구 레지스트리 및 조합 로직 | +| `src/tools/*/` | 개별 도구 구현체 (40개 이상) | + +--- + +## 2. 아키텍처 다이어그램 + +```mermaid +graph TD + A[LLM tool_use 블록 반환] --> B[toolMatchesName으로 도구 탐색] + B --> C{도구 발견?} + C -- 아니오 --> D[에러 반환] + C -- 예 --> E[Zod 스키마 입력 검증] + E --> F{validateInput 통과?} + F -- 실패 --> G[ValidationResult 에러 반환] + F -- 통과 --> H[checkPermissions 호출] + H --> I{권한 결과} + I -- deny --> J[거부 메시지 반환] + I -- ask --> K[사용자에게 확인 요청] + I -- allow --> L[tool.call 실행] + K -- 승인 --> L + K -- 거부 --> J + L --> M[ToolResult 반환] + M --> N{결과 크기 > maxResultSizeChars?} + N -- 예 --> O[디스크에 저장, 프리뷰 반환] + N -- 아니오 --> P[mapToolResultToToolResultBlockParam] + O --> P + P --> Q[렌더링 및 메시지 히스토리 추가] + + subgraph "도구 레지스트리 (tools.ts)" + R[getAllBaseTools] --> S[getTools] + S --> T[filterToolsByDenyRules] + T --> U[assembleToolPool] + U --> V[MCP 도구 병합] + end + + subgraph "개별 도구 구현체 (src/tools/*/)" + W[BashTool] + X[FileReadTool] + Y[AgentTool] + Z[기타 40+ 도구] + end +``` + +--- + +## 3. 핵심 타입 및 인터페이스 + +`src/Tool.ts`는 792줄 규모의 타입 전용 파일로, 런타임 코드를 최소화하고 타입 시스템을 통해 도구 계약(contract)을 강제한다. + +### 3.1 `ToolInputJSONSchema` + +```typescript +export type ToolInputJSONSchema = { + [x: string]: unknown + type: 'object' + properties?: { + [x: string]: unknown + } +} +``` + +MCP 도구처럼 Zod 스키마 대신 JSON Schema를 직접 사용하는 도구를 위한 타입이다. 빌트인 도구는 `inputSchema: Input`(Zod 기반)을 사용하고, MCP 도구는 `inputJSONSchema`를 사용하는 두 경로가 공존한다. + +### 3.2 `ToolPermissionContext` + +```typescript +export type ToolPermissionContext = DeepImmutable<{ + mode: PermissionMode + additionalWorkingDirectories: Map + alwaysAllowRules: ToolPermissionRulesBySource + alwaysDenyRules: ToolPermissionRulesBySource + alwaysAskRules: ToolPermissionRulesBySource + isBypassPermissionsModeAvailable: boolean + isAutoModeAvailable?: boolean + strippedDangerousRules?: ToolPermissionRulesBySource + shouldAvoidPermissionPrompts?: boolean + awaitAutomatedChecksBeforeDialog?: boolean + prePlanMode?: PermissionMode +}> +``` + +`DeepImmutable`로 감싸져 있어 런타임 중 권한 컨텍스트가 변경되지 않음을 타입 수준에서 보장한다. `alwaysAllowRules`, `alwaysDenyRules`, `alwaysAskRules`는 각각 소스(source)별로 분류된 규칙 맵으로, `.claude/settings.json`의 훅(hook) 설정이 여기에 반영된다. + +### 3.3 `ToolUseContext` + +전체 시스템에서 가장 복잡한 타입이다. 모든 `tool.call()`에 전달되는 의존성 주입 컨테이너 역할을 한다. + +**주요 필드 분류:** + +| 카테고리 | 필드 | 설명 | +|----------|------|------| +| 설정 | `options.tools`, `options.commands`, `options.mainLoopModel` | 런타임 설정 | +| 상태 관리 | `getAppState()`, `setAppState()` | React 상태 접근자 | +| 세션 인프라 | `setAppStateForTasks` | 서브에이전트에서도 루트 스토어에 접근 가능 | +| UI 콜백 | `setToolJSX`, `addNotification`, `appendSystemMessage` | UI 업데이트 함수 | +| 중단 제어 | `abortController` | 도구 실행 취소 신호 | +| 파일 캐시 | `readFileState: FileStateCache` | LRU 캐시 기반 파일 상태 | +| 컨텍스트 추적 | `queryTracking`, `toolDecisions` | 체인 추적 및 결정 기록 | +| 에이전트 식별 | `agentId`, `agentType` | 서브에이전트 구분 | +| 권한 관련 | `localDenialTracking`, `requireCanUseTool` | 비동기 서브에이전트용 거부 추적 | +| 프롬프트 캐시 | `renderedSystemPrompt`, `contentReplacementState` | 캐시 공유 최적화 | + +`setAppStateForTasks`는 일반 `setAppState`와 다르게 비동기 서브에이전트에서도 루트 AppState에 접근할 수 있도록 설계된 점이 특징이다. 일반 `setAppState`는 비동기 에이전트에서 no-op(무작동)으로 작동하여 격리를 유지하지만, 세션 스코프 인프라(예: 백그라운드 태스크 등록)는 루트까지 전파되어야 하기 때문이다. + +### 3.4 `ValidationResult` + +```typescript +export type ValidationResult = + | { result: true } + | { result: false; message: string; errorCode: number } +``` + +도구 실행 전 사전 검증(pre-flight validation) 결과 타입이다. 실패 시 `message`가 LLM에게 반환되어 모델이 입력을 수정할 수 있도록 안내한다. + +### 3.5 `Tool` 인터페이스 + +```typescript +export type Tool< + Input extends AnyObject = AnyObject, + Output = unknown, + P extends ToolProgressData = ToolProgressData, +> = { + name: string + aliases?: string[] + inputSchema: Input + inputJSONSchema?: ToolInputJSONSchema + maxResultSizeChars: number + call(args, context, canUseTool, parentMessage, onProgress?): Promise> + description(input, options): Promise + checkPermissions(input, context): Promise + validateInput?(input, context): Promise + isConcurrencySafe(input): boolean + isReadOnly(input): boolean + isDestructive?(input): boolean + // ... UI 렌더링 메서드 다수 +} +``` + +제네릭 파라미터 3개가 입력 스키마 타입(`Input`), 출력 타입(`Output`), 진행 상태 타입(`P`)을 각각 고정한다. UI 렌더링 관련 메서드(`renderToolResultMessage`, `renderToolUseMessage` 등)가 도구 인터페이스에 직접 포함된 점이 이 설계의 특징으로, 도구 로직과 렌더링 로직이 같은 모듈 내에 공존한다. + +### 3.6 `buildTool` 팩토리 함수 + +```typescript +export function buildTool(def: D): BuiltTool { + return { + ...TOOL_DEFAULTS, + userFacingName: () => def.name, + ...def, + } as BuiltTool +} +``` + +모든 도구는 `buildTool`을 통해 생성되며, 다음 기본값이 주입된다: + +| 메서드 | 기본값 | 설계 원칙 | +|--------|--------|-----------| +| `isEnabled` | `() => true` | 명시적 비활성화 없이는 활성 | +| `isConcurrencySafe` | `() => false` | 실패 안전(fail-closed): 기본적으로 동시 실행 불가 | +| `isReadOnly` | `() => false` | 기본적으로 쓰기 작업으로 간주 | +| `isDestructive` | `() => false` | 기본적으로 비파괴적 | +| `checkPermissions` | `allow` 반환 | 일반 권한 시스템에 위임 | +| `toAutoClassifierInput` | `() => ''` | 보안 분류기에서 제외 | +| `userFacingName` | `() => def.name` | 도구명을 그대로 표시 | + +`BuiltTool` 타입은 타입 수준 스프레드(spread)를 통해 런타임 `{...TOOL_DEFAULTS, ...def}` 의미론을 정확히 반영한다. + +### 3.7 `Tools` 타입 + +```typescript +export type Tools = readonly Tool[] +``` + +`Tool[]` 대신 이 타입을 사용함으로써 코드베이스 전체에서 도구 집합이 조합, 전달, 필터링되는 지점을 타입 시스템으로 추적할 수 있다. + +--- + +## 4. Tool 레지스트리 (`tools.ts`) + +`src/tools.ts`는 도구 등록 방식을 세 가지 패턴으로 분류한다. + +### 4.1 정적 임포트 (Static Import) + +핵심 도구들은 ES 모듈 정적 임포트로 등록된다: + +```typescript +import { AgentTool } from './tools/AgentTool/AgentTool.js' +import { BashTool } from './tools/BashTool/BashTool.js' +import { FileEditTool } from './tools/FileEditTool/FileEditTool.js' +// ... +``` + +번들러(bundler)가 빌드 시점에 포함 여부를 결정할 수 있어 트리 셰이킹(tree shaking)이 적용된다. + +### 4.2 조건부 `require()` — 기능 플래그 기반 + +`bun:bundle`의 `feature()` 함수를 활용한 dead code elimination(데드 코드 제거) 패턴이다: + +```typescript +import { feature } from 'bun:bundle' + +const SleepTool = feature('PROACTIVE') || feature('KAIROS') + ? require('./tools/SleepTool/SleepTool.js').SleepTool + : null + +const cronTools = feature('AGENT_TRIGGERS') + ? [ + require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool, + // ... + ] + : [] +``` + +`feature()` 플래그가 `false`인 경우, 번들러는 해당 `require()` 브랜치 전체를 빌드 산출물에서 제거한다. 이는 프로덕션 번들 크기를 최소화하는 동시에 기능별 점진적 배포를 가능하게 한다. + +### 4.3 조건부 `require()` — 환경 변수 기반 + +`USER_TYPE` 환경 변수로 Ant(내부 Anthropic) 전용 도구를 분리한다: + +```typescript +const REPLTool = + process.env.USER_TYPE === 'ant' + ? require('./tools/REPLTool/REPLTool.js').REPLTool + : null +``` + +이 패턴을 사용하는 도구: `REPLTool`, `SuggestBackgroundPRTool`, `ConfigTool`, `TungstenTool`. + +### 4.4 지연 `require()` — 순환 의존성 해결 + +`TeamCreateTool`, `TeamDeleteTool`, `SendMessageTool`은 순환 의존성(circular dependency)을 해결하기 위해 지연 로딩(lazy loading)된다: + +```typescript +// Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts +const getTeamCreateTool = () => + require('./tools/TeamCreateTool/TeamCreateTool.js') + .TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool +``` + +이 팀 도구들은 `tools.ts`를 임포트하는 모듈 체인에 속하므로, 정적 임포트로 처리하면 모듈 초기화 시점에 순환이 발생한다. 지연 함수로 감싸면 실제 호출 시점까지 로딩을 미룰 수 있다. + +### 4.5 `getAllBaseTools()` 함수 + +전체 도구 목록의 단일 진실 공급원(single source of truth)이다: + +```typescript +export function getAllBaseTools(): Tools { + return [ + AgentTool, + TaskOutputTool, + BashTool, + ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]), + // ... + ...(isTodoV2Enabled() + ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool] + : []), + // ... + ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []), + ] +} +``` + +주석에 따르면 이 목록은 Statsig 시스템 프롬프트 캐싱 설정(`claude_code_global_system_caching`)과 동기화를 유지해야 한다. 도구 순서가 바뀌면 프롬프트 캐시 키가 무효화될 수 있기 때문이다. + +### 4.6 `getTools()` 함수 + +권한 컨텍스트를 적용하여 실제 사용 가능한 도구 목록을 반환한다: + +```typescript +export const getTools = (permissionContext: ToolPermissionContext): Tools => { + // 1. CLAUDE_CODE_SIMPLE 환경 변수: Bash/Read/Edit만 반환 + if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { ... } + + // 2. 특수 도구 제외 (ListMcpResources 등 별도 경로로 추가) + const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name)) + + // 3. Deny 규칙으로 도구 필터링 + let allowedTools = filterToolsByDenyRules(tools, permissionContext) + + // 4. REPL 모드: 원시 도구 숨김 (REPL 내부 VM에서만 접근 가능) + if (isReplModeEnabled()) { ... } + + // 5. isEnabled() 검사 + return allowedTools.filter((_, i) => isEnabled[i]) +} +``` + +### 4.7 `assembleToolPool()` 함수 + +빌트인 도구와 MCP 도구를 하나의 풀로 결합하는 최종 조합 함수다: + +```typescript +export function assembleToolPool( + permissionContext: ToolPermissionContext, + mcpTools: Tools, +): Tools { + const builtInTools = getTools(permissionContext) + const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext) + + // 빌트인 도구를 앞쪽 연속 블록으로 유지하여 프롬프트 캐시 안정성 확보 + const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name) + return uniqBy( + [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)), + 'name', + ) +} +``` + +정렬 시 빌트인 도구와 MCP 도구를 별도 파티션으로 유지하는 이유는 프롬프트 캐시 안정성(prompt cache stability) 때문이다. 서버 측 캐시 정책이 빌트인 도구 블록 끝에 캐시 브레이크포인트(cache breakpoint)를 두므로, 두 파티션을 평탄하게 섞으면 MCP 도구가 빌트인 목록 사이에 끼어들어 캐시 키가 무효화된다. + +--- + +## 5. Tool 실행 파이프라인 + +LLM이 `tool_use` 블록을 반환한 시점부터 결과가 대화 히스토리에 기록되기까지의 전체 흐름이다. + +### 단계 1: 도구 탐색 + +```typescript +export function toolMatchesName( + tool: { name: string; aliases?: string[] }, + name: string, +): boolean { + return tool.name === name || (tool.aliases?.includes(name) ?? false) +} + +export function findToolByName(tools: Tools, name: string): Tool | undefined { + return tools.find(t => toolMatchesName(t, name)) +} +``` + +`aliases` 필드는 도구명이 변경될 때 하위 호환성(backward compatibility)을 유지하기 위한 장치다. 구 도구명으로 호출된 LLM 요청도 새 도구로 라우팅된다. + +### 단계 2: 입력 검증 + +Zod v4 스키마로 입력을 파싱한다. 파싱 실패 시 구조화된 에러 메시지가 LLM에 반환되어 모델이 입력을 수정하도록 유도한다. + +옵셔널 메서드 `validateInput`이 구현된 경우, Zod 파싱 통과 후 추가적인 도구별 검증이 실행된다: + +```typescript +validateInput?( + input: z.infer, + context: ToolUseContext, +): Promise +``` + +### 단계 3: 권한 확인 + +```typescript +checkPermissions( + input: z.infer, + context: ToolUseContext, +): Promise +``` + +`PermissionResult`의 `behavior` 필드에 따라 실행 경로가 분기된다: + +| behavior | 동작 | +|----------|------| +| `allow` | 즉시 실행 | +| `deny` | 거부 메시지 반환, LLM에 전달 | +| `ask` | 사용자에게 확인 UI 표시 | + +`buildTool`의 기본 구현은 `allow`를 반환하여 일반 권한 시스템(`permissions.ts`)에 위임한다. 도구별 특수 로직이 필요한 경우에만 이 메서드를 오버라이드한다. + +### 단계 4: 실행 + +```typescript +call( + args: z.infer, + context: ToolUseContext, + canUseTool: CanUseToolFn, + parentMessage: AssistantMessage, + onProgress?: ToolCallProgress

, +): Promise> +``` + +`ToolUseContext`는 도구가 필요로 하는 모든 의존성(앱 상태, 파일 캐시, 중단 컨트롤러, UI 콜백 등)을 담은 컨테이너로 전달된다. `onProgress` 콜백을 통해 실행 중 진행 상태를 스트리밍할 수 있다. + +### 단계 5: 결과 처리 + +```typescript +export type ToolResult = { + data: T + newMessages?: (UserMessage | AssistantMessage | AttachmentMessage | SystemMessage)[] + contextModifier?: (context: ToolUseContext) => ToolUseContext + mcpMeta?: { _meta?: Record; structuredContent?: Record } +} +``` + +`contextModifier`는 동시 실행에 안전하지 않은 도구(`isConcurrencySafe = false`)에서만 사용 가능하며, 도구 결과에 따라 `ToolUseContext`를 변경할 수 있다. 예를 들어 `EnterPlanModeTool`은 이를 통해 권한 모드를 전환한다. + +### 단계 6: 결과 직렬화 및 저장 + +```typescript +maxResultSizeChars: number +mapToolResultToToolResultBlockParam( + content: Output, + toolUseID: string, +): ToolResultBlockParam +``` + +결과 크기가 `maxResultSizeChars`를 초과하면 디스크에 저장하고 LLM에는 파일 경로와 프리뷰만 전달한다. `FileReadTool`의 경우 이 값이 `Infinity`로 설정되어 있는데, 파일 내용을 디스크에 다시 저장하면 `Read → 파일 → Read` 순환이 발생하고 이미 자체적으로 크기 제한을 적용하기 때문이다. + +### 단계 7: 에러 처리 + +도구는 에러 UI를 커스터마이즈할 수 있다: + +```typescript +renderToolUseErrorMessage?( + result: ToolResultBlockParam['content'], + options: { ... }, +): React.ReactNode +``` + +이 메서드가 없으면 ``가 사용된다. 파일 탐색 도구처럼 "파일을 찾을 수 없습니다" 같은 도구별 에러 메시지가 필요한 경우에만 구현한다. + +--- + +## 6. Tool 카테고리 분석 + +`src/tools/` 디렉토리의 모든 도구를 카테고리별로 분류한다. + +### 파일 조작 + +| 도구 | 설명 | 읽기 전용 | +|------|------|-----------| +| `FileReadTool` | 파일 읽기, 이미지/PDF/노트북 지원 | O | +| `FileEditTool` | 파일 일부 편집 (문자열 대체) | X | +| `FileWriteTool` | 파일 생성 및 전체 덮어쓰기 | X | +| `GlobTool` | 파일 패턴 탐색 | O | +| `GrepTool` | 파일 내용 패턴 검색 (ripgrep 래핑) | O | +| `NotebookEditTool` | Jupyter 노트북 셀 편집 | X | + +`GlobTool`과 `GrepTool`은 `hasEmbeddedSearchTools()`가 `true`인 경우(Ant 내부 빌드) 제외된다. Ant 빌드에는 bfs/ugrep이 Bun 바이너리에 내장되어 있고, Bash 내에서 `find`/`grep` 명령이 이 빠른 도구로 앨리어스(alias)되기 때문이다. + +### 실행 + +| 도구 | 설명 | +|------|------| +| `BashTool` | 셸 명령 실행 (샌드박스 지원) | +| `REPLTool` | 격리된 VM 환경에서 코드 실행 (Ant 전용) | +| `PowerShellTool` | Windows PowerShell 명령 실행 | + +`BashTool`은 파이프라인 구성 명령을 분석하여 검색/읽기/목록 조회 명령인지 판단하고 UI에서 접을 수 있게(collapsible) 표시한다. 이를 위해 `BASH_SEARCH_COMMANDS`, `BASH_READ_COMMANDS`, `BASH_LIST_COMMANDS`, `BASH_SEMANTIC_NEUTRAL_COMMANDS` 집합을 사용한다. + +### 에이전트 + +| 도구 | 설명 | +|------|------| +| `AgentTool` | 서브에이전트 생성 및 실행 | +| `SendMessageTool` | 팀 내 에이전트 간 메시지 전송 | +| `TeamCreateTool` | 에이전트 팀 생성 | +| `TeamDeleteTool` | 에이전트 팀 해체 | + +`AgentTool`은 시스템에서 가장 복잡한 도구로, 로컬/원격 에이전트 실행, 워크트리(worktree) 생성, 포크(fork) 서브에이전트, 코디네이터 모드 등 다수의 실행 경로를 포함한다. `AgentTool`의 임포트 목록만 80줄에 달한다. + +### 태스크 관리 + +| 도구 | 설명 | +|------|------| +| `TaskCreateTool` | 새 태스크 생성 | +| `TaskGetTool` | 태스크 상태 조회 | +| `TaskListTool` | 태스크 목록 조회 | +| `TaskUpdateTool` | 태스크 업데이트 | +| `TaskOutputTool` | 태스크 출력 스트리밍 | +| `TaskStopTool` | 태스크 중단 | + +태스크 관련 도구 6개는 `isTodoV2Enabled()` 플래그로 일괄 활성화/비활성화된다. + +### MCP / LSP + +| 도구 | 설명 | +|------|------| +| `MCPTool` | MCP 서버 도구 래퍼 (동적) | +| `LSPTool` | Language Server Protocol 통합 | +| `McpAuthTool` | MCP 인증 처리 | +| `ListMcpResourcesTool` | MCP 서버 리소스 목록 | +| `ReadMcpResourceTool` | MCP 리소스 내용 읽기 | + +`ListMcpResourcesTool`과 `ReadMcpResourceTool`은 `getTools()`에서 `specialTools` 집합으로 분리되어 별도 경로로 추가된다. `LSPTool`은 `ENABLE_LSP_TOOL` 환경 변수로 명시적으로 활성화해야 한다. + +### 워크플로우 제어 + +| 도구 | 설명 | +|------|------| +| `EnterPlanModeTool` | Plan 모드 진입 (쓰기 작업 차단) | +| `ExitPlanModeV2Tool` | Plan 모드 종료 | +| `EnterWorktreeTool` | Git 워크트리 모드 진입 | +| `ExitWorktreeTool` | Git 워크트리 모드 종료 | +| `SkillTool` | 스킬 파일 실행 | +| `ToolSearchTool` | 지연 로드된 도구 검색 | + +`EnterWorktreeTool`과 `ExitWorktreeTool`은 `isWorktreeModeEnabled()` 조건으로 등록된다. + +### 사용자 상호작용 + +| 도구 | 설명 | +|------|------| +| `AskUserQuestionTool` | 사용자에게 질문 및 응답 수집 | +| `BriefTool` | 간략한 상태 업데이트 표시 | +| `ConfigTool` | 설정 읽기/쓰기 (Ant 전용) | +| `TodoWriteTool` | 할 일 목록 업데이트 | +| `SleepTool` | 실행 일시 중지 (PROACTIVE/KAIROS) | + +`TodoWriteTool`은 `renderToolResultMessage`를 구현하지 않는 도구의 예시다. 이 도구는 결과를 투명 패널(todo panel)로 업데이트하므로 대화 트랜스크립트(transcript)에 별도 결과를 출력하지 않는다. + +### 웹 접근 + +| 도구 | 설명 | +|------|------| +| `WebFetchTool` | URL에서 HTML/텍스트 가져오기 | +| `WebSearchTool` | 웹 검색 실행 | + +### 기타 (실험적/내부) + +| 도구 | 설명 | 조건 | +|------|------|------| +| `SyntheticOutputTool` | 합성 출력 생성 | 항상 특수 처리 | +| `RemoteTriggerTool` | 원격 트리거 실행 | `AGENT_TRIGGERS_REMOTE` 플래그 | +| `ScheduleCronTool` 계열 | 크론 작업 관리 | `AGENT_TRIGGERS` 플래그 | +| `SleepTool` | 에이전트 슬립 | `PROACTIVE` 또는 `KAIROS` 플래그 | +| `TungstenTool` | 내부 도구 | Ant 전용 | +| `TestingPermissionTool` | 권한 테스트 | `NODE_ENV === 'test'` | + +--- + +## 7. 주요 설계 결정 + +### 7.1 Zod v4 기반 스키마 검증 + +빌드 시점과 런타임 모두에서 타입 안전성을 보장하기 위해 Zod v4를 채택했다. `z.infer`으로 입력 타입을 자동 추론하므로 별도의 타입 정의가 불필요하다. 또한 Zod 스키마는 Anthropic API가 요구하는 JSON Schema로 직렬화되어 LLM에 도구 명세로 전달된다. + +`lazySchema` 패턴이 다수의 도구에서 사용된다: + +```typescript +// FileReadTool.tsx에서 +import { lazySchema } from '../../utils/lazySchema.js' +``` + +이는 스키마 생성 비용이 높거나 순환 의존성이 있는 경우, 실제 사용 시점까지 스키마 평가를 지연시키는 패턴이다. + +### 7.2 조건부 도구 로딩과 번들 크기 최적화 + +`bun:bundle`의 `feature()` 함수는 Bun의 번들러가 이해하는 특수 API다. `feature('FLAG_NAME')`이 `false`로 평가되는 빌드에서는 해당 `require()` 브랜치가 번들에서 완전히 제거된다. 이를 통해: + +1. 프로덕션 빌드 크기 최소화 +2. 미완성 기능의 코드가 배포되지 않음 보장 +3. A/B 테스트 및 단계적 기능 롤아웃 지원 + +런타임 기능 플래그(`isEnvTruthy`, `process.env.USER_TYPE`)와 빌드 타임 플래그(`feature()`)를 구분하여 사용하는 점이 특징이다. + +### 7.3 `ToolUseContext`의 의존성 주입 패턴 + +모든 도구가 단일 `context: ToolUseContext` 인수를 통해 필요한 의존성에 접근한다. 이 패턴의 장점: + +- **테스트 용이성**: 컨텍스트 객체를 모킹(mocking)하여 도구를 격리 테스트 가능 +- **확장성**: 새 의존성을 추가할 때 함수 시그니처를 변경하지 않고 컨텍스트에 필드만 추가 +- **서브에이전트 격리**: `setAppState`가 서브에이전트에서 no-op으로 작동하도록 컨텍스트를 교체하여 격리 달성 + +단점으로는 컨텍스트 타입이 매우 커진다는 점이 있다 — `ToolUseContext`는 약 50개의 필드를 포함한다. + +### 7.4 프롬프트 캐시 안정성 설계 + +`assembleToolPool`이 빌트인/MCP 도구를 별도 파티션으로 정렬하는 이유는 Anthropic API의 프롬프트 캐싱 메커니즘과 직접 연관된다. 서버 측 캐시 정책은 빌트인 도구 목록 끝에 캐시 브레이크포인트를 배치하는데, 도구 순서가 변경되면 이 브레이크포인트 이후의 모든 토큰이 캐시 미스(cache miss)가 된다. 이 설계를 통해 MCP 서버 연결/해제가 빈번해도 빌트인 도구 캐시는 유지된다. + +### 7.5 `shouldDefer` / `alwaysLoad` — 지연 도구 로딩 + +도구 수가 많아지면 LLM에 전달되는 프롬프트 크기가 크게 증가한다. `shouldDefer: true`인 도구는 초기 프롬프트에서 스키마가 생략되고(`defer_loading: true` 플래그로 API에 전달), `ToolSearchTool`을 통해 필요 시 온디맨드(on-demand)로 스키마를 가져올 수 있다: + +```typescript +readonly shouldDefer?: boolean +readonly alwaysLoad?: boolean // 지연 도구 중에서도 항상 초기 로드 +``` + +이를 통해 40개 이상의 도구를 보유하면서도 초기 컨텍스트 토큰 소비를 최소화한다. + +--- + +## 8. 실제 도구 구현 패턴 + +### BashTool 구현 패턴 + +```typescript +// BashTool.tsx (단순화) +export const BashTool = buildTool({ + name: BASH_TOOL_NAME, + // 1. Zod 스키마 정의 + inputSchema: lazySchema(() => z.object({ + command: z.string(), + timeout: z.number().optional(), + })), + // 2. 핵심 실행 로직 + async call(args, context, canUseTool, parentMessage, onProgress) { + // ... 실행 로직 + }, + // 3. 검색/읽기 명령 판별 + isSearchOrReadCommand(input) { + return isSearchOrReadBashCommand(input.command) + }, + // 4. 읽기 전용 여부 (기본값 false 사용) + isReadOnly: () => false, +}) +``` + +### FileReadTool 구현 패턴 + +`FileReadTool.tsx`는 빌드 시점에 수십 개의 유틸리티를 임포트하는 복잡한 도구다. PDF, 이미지, Jupyter 노트북, 일반 텍스트 등 다양한 파일 형식을 처리하며, `maxResultSizeChars: Infinity`를 통해 결과가 디스크에 저장되지 않도록 보장한다. + +--- + +## 내비게이션 + +- 이전: [QueryEngine](query-engine.md) +- 다음: [권한 시스템](permission-system.md) +- 상위: [목차](../README.md) diff --git a/docs/ko/level-2-systems/ui-ink-components.md b/docs/ko/level-2-systems/ui-ink-components.md new file mode 100644 index 0000000..62cd86e --- /dev/null +++ b/docs/ko/level-2-systems/ui-ink-components.md @@ -0,0 +1,396 @@ +# UI 시스템: React/Ink CLI 인터페이스 분석 + +> **Level 2 — 시스템 심층 분석** +> 관련 문서: [쿼리 엔진](query-engine.md) · [도구 시스템](tool-system.md) · [권한 시스템](permission-system.md) · [에이전트 코디네이터](agent-coordinator.md) + +--- + +## 1. 개요 + +Claude Code의 사용자 인터페이스는 **React와 Ink를 조합**하여 터미널(CLI) 환경에서 풍부한 대화형 UI를 구현한다. 브라우저의 DOM 대신 터미널 표준 출력을 렌더링 대상으로 삼는다는 점에서, 이 시스템은 전통적인 웹 프론트엔드와 구조적으로 동일하면서도 렌더링 파이프라인이 근본적으로 다르다. + +주요 규모 지표: + +| 항목 | 수치 | +|------|------| +| `src/components/` 직속 파일·디렉터리 수 | 140+ | +| `src/screens/` 화면 수 | 3 (REPL, Doctor, ResumeConversation) | +| `src/ink/` 커스텀 Ink 엔진 파일 수 | 40+ | +| 디자인 시스템 컴포넌트 수 | 16 | + +아키텍처 관점에서 UI 시스템의 핵심 특성은 세 가지다. + +1. **React 컴파일러 최적화**: 모든 컴포넌트에 `react/compiler-runtime`의 `_c()` 캐시 훅이 자동 삽입되어, 수동 `useMemo`/`useCallback` 없이도 세밀한 리렌더링 방지가 이루어진다. +2. **커스텀 Ink 포크**: 오픈소스 Ink 라이브러리를 포크하여 Yoga 레이아웃 엔진, 이중 버퍼(front/back frame) 렌더러, 선택 오버레이, 하이퍼링크 풀 등 고성능 기능을 추가했다. +3. **ThemeProvider 래핑**: 모든 `render()` 호출은 `src/ink.ts`의 `withTheme()` 함수를 통해 자동으로 `ThemeProvider`로 감싸져, 테마-무관 Ink 엔진 위에 디자인 시스템 토큰이 주입된다. + +--- + +## 2. 아키텍처 다이어그램 + +```mermaid +flowchart TD + subgraph EntryPoint["진입점 (src/ink.ts)"] + IT[ink.ts\nrender / createRoot] + WT[withTheme wrapper] + end + + subgraph Providers["컨텍스트 프로바이더 계층"] + TP[ThemeProvider\n테마 토큰 주입] + FPS[FpsMetricsProvider] + STATS[StatsProvider] + APP[AppStateProvider] + end + + subgraph Screens["화면 계층 (src/screens/)"] + REPL[REPL.tsx\n메인 대화 화면] + DOC[Doctor.tsx\n진단 화면] + RES[ResumeConversation.tsx\n세션 복원 화면] + end + + subgraph Components["컴포넌트 계층 (src/components/)"] + PI[PromptInput\n사용자 입력] + MSG[Message\n메시지 렌더링] + VML[VirtualMessageList\n가상 스크롤] + SP[Spinner\n스트리밍 상태] + DS[Design System\nBox · Text · Dialog…] + end + + subgraph InkEngine["Ink 렌더링 엔진 (src/ink/)"] + REC[reconciler.ts\nReact Reconciler] + DOM[dom.ts\nVirtual DOM] + YOGA[Yoga Layout\n박스 모델 계산] + REND[renderer.ts\n이중 버퍼 렌더러] + SCREEN[screen.ts\n문자 셀 버퍼] + OUT[output.ts\nANSI 이스케이프 출력] + end + + IT --> WT --> TP + TP --> FPS --> STATS --> APP + APP --> REPL + REPL --> PI & MSG & VML & SP + MSG & PI --> DS + DS --> REC + REC --> DOM --> YOGA --> REND + REND --> SCREEN --> OUT + OUT -->|"ANSI 이스케이프 시퀀스"| Terminal["터미널 (stdout)"] +``` + +데이터 흐름 방향은 단방향이다. React 상태 변경 → Reconciler가 Virtual DOM 패치 → Yoga 레이아웃 재계산 → 이중 버퍼 비교(diff) → 변경된 셀만 ANSI 시퀀스로 출력. + +--- + +## 3. Ink 프레임워크 개요 + +### 3.1 React for CLI의 원리 + +Ink는 React의 `react-reconciler` 패키지를 사용하여 브라우저 DOM 대신 **커스텀 호스트 환경(터미널)**을 렌더링 대상으로 연결한다. 웹의 `ReactDOM.createRoot()`가 DOM 노드를 받는 것처럼, Ink의 reconciler는 터미널 stdout 스트림에 연결된다. + +`src/ink/reconciler.ts`는 `createReconciler()`를 호출하며, 커스텀 호스트 메서드(`createInstance`, `appendChildToContainer`, `commitUpdate` 등)를 구현한다. 이 메서드들은 `src/ink/dom.ts`의 가상 DOM을 조작하며, 각 DOM 노드는 Yoga 레이아웃 노드(`yogaNode`)를 보유한다. + +``` +React 컴포넌트 트리 + ↓ (react-reconciler) + Virtual DOM (DOMElement) + ↓ (Yoga 레이아웃) + 계산된 위치·크기 + ↓ (renderer.ts) + 이중 버퍼 (front / back frame) + ↓ (screen.ts) + 문자 셀 배열 (CharCell[]) + ↓ (output.ts) + ANSI 이스케이프 시퀀스 → stdout +``` + +### 3.2 이중 버퍼 렌더러 + +`src/ink/renderer.ts`는 `frontFrame`과 `backFrame` 두 버퍼를 유지한다. 매 프레임마다 backFrame에 새 화면을 그린 후, frontFrame(현재 화면)과 비교하여 변경된 셀만 터미널에 기록한다. 이는 화면 깜박임을 제거하고 출력 바이트 수를 최소화한다. + +렌더러는 `prevFrameContaminated` 플래그를 통해 선택 오버레이, 대체 화면(alt-screen) 전환, 창 크기 변경(SIGWINCH) 등으로 이전 프레임이 오염된 경우 전체 재출력을 수행한다. + +### 3.3 Yoga 레이아웃 엔진 + +Meta의 Yoga 레이아웃 엔진이 박스 모델과 Flexbox 레이아웃을 계산한다. `src/ink/dom.ts`의 각 `DOMElement`는 `yogaNode`를 보유하며, `setStyle()`을 통해 스타일이 적용된다. `calculateLayout()` 호출 후 `getComputedWidth()` / `getComputedHeight()`로 확정된 크기를 얻는다. + +렌더러는 `computedHeight`가 `NaN`이거나 음수인 경우(레이아웃 미완료) 빈 프레임을 반환하여 방어적으로 처리한다. + +### 3.4 성능 최적화: 문자 캐시와 그래핀 클러스터링 + +`src/ink/output.ts`는 `charCache`를 통해 토큰화와 유니코드 그래핀 클러스터 분리 결과를 프레임 간에 재사용한다. 대부분의 줄은 프레임 사이에 변경되지 않으므로 이 캐시의 적중률이 높다. + +### 3.5 커스텀 진입점 (src/ink.ts) + +오픈소스 Ink를 그대로 쓰지 않고, `src/ink.ts`가 공식 진입점 역할을 한다. 이 파일은: + +- `render()` / `createRoot()`를 `withTheme()` 래퍼로 감싸 자동 테마 주입 +- `ThemedBox`, `ThemedText`를 `Box`, `Text`로 재수출하여 코드베이스 전체가 테마 인식 컴포넌트를 사용하도록 강제 +- `FocusManager`, `InputEvent`, `ClickEvent` 등 이벤트 인프라 재수출 +- `useInput`, `useApp`, `useStdin` 등 Ink 훅 재수출 + +--- + +## 4. 주요 화면 구성 + +### 4.1 REPL.tsx — 메인 대화 화면 + +`src/screens/REPL.tsx`는 시스템에서 가장 복잡한 단일 컴포넌트다. 이 파일은 Claude Code의 메인 대화 루프 전체를 조율한다. + +**핵심 역할:** + +- `PromptInput` 컴포넌트를 통한 사용자 입력 수집 및 제출 +- `VirtualMessageList`를 통한 대화 이력 렌더링(가상 스크롤) +- 스트리밍 응답 수신 중 `Spinner` 상태 표시 +- 권한 요청 다이얼로그(`PermissionRequest`) 표시 +- 멀티-에이전트 스웜(swarm) 세션 관리 +- 세션 비용, 토큰 예산, 탭 상태, 터미널 알림 관리 + +**의존 훅 (주요):** + +| 훅 | 역할 | +|----|------| +| `useReplBridge` | IDE 연동 브릿지 | +| `useRemoteSession` | 원격 세션 관리 | +| `useSwarmInitialization` | 멀티-에이전트 초기화 | +| `useApiKeyVerification` | API 키 검증 | +| `useBackgroundTaskNavigation` | 백그라운드 작업 탐색 | +| `useAssistantHistory` | 어시스턴트 응답 이력 | +| `useLogMessages` | 로그 메시지 처리 | + +**특수 기능:** `feature('VOICE_MODE')` 플래그를 통한 음성 통합이 조건부로 활성화된다. Bun 번들러의 DCE(Dead Code Elimination)가 비활성화 시 해당 코드를 제거한다. + +### 4.2 Doctor.tsx — 진단 화면 + +시스템 상태, 환경 변수, 연결 상태 등을 점검하는 진단 도구. `--doctor` 플래그로 진입하는 독립 화면이다. + +### 4.3 ResumeConversation.tsx — 세션 복원 화면 + +이전 대화 세션 목록을 표시하고 선택하는 화면. `--resume` 플래그 진입 시 렌더링된다. + +--- + +## 5. 핵심 컴포넌트 분석 + +### 5.1 메시지 렌더링 시스템 + +#### Message.tsx — 메시지 라우터 + +`src/components/Message.tsx`는 메시지 유형에 따라 적절한 하위 컴포넌트로 라우팅하는 **디스패처** 역할을 한다. React 컴파일러가 생성한 `_c(94)` 캐시(94개 슬롯)가 이 컴포넌트의 렌더링 비용을 최소화한다. + +지원 메시지 유형과 대응 컴포넌트: + +| 메시지 유형 | 렌더링 컴포넌트 | +|-------------|----------------| +| 어시스턴트 텍스트 | `AssistantTextMessage` | +| 어시스턴트 thinking | `AssistantThinkingMessage` | +| 어시스턴트 redacted thinking | `AssistantRedactedThinkingMessage` | +| 도구 사용 요청 | `AssistantToolUseMessage` | +| 도구 사용 결과 | `UserToolResultMessage` | +| 사용자 텍스트 | `UserTextMessage` | +| 사용자 이미지 | `UserImageMessage` | +| 시스템 텍스트 | `SystemTextMessage` | +| 첨부 파일 | `AttachmentMessage` | +| 압축 요약 경계 | `CompactBoundaryMessage` | +| 축약된 읽기/검색 | `CollapsedReadSearchContent` | +| 그룹화된 도구 사용 | `GroupedToolUseContent` | +| 어드바이저 | `AdvisorMessage` | + +모든 메시지는 `OffscreenFreeze`로 감싸져, 뷰포트 밖으로 스크롤된 메시지는 React 업데이트를 받지 않는다(렌더링 동결). + +#### Markdown.tsx — 마크다운 파서 + +`marked` 라이브러리로 마크다운을 토큰화한 뒤 Ink 컴포넌트로 변환한다. + +**최적화 전략:** + +1. **구문 감지 조기 탈출**: 정규식 `MD_SYNTAX_RE`로 마크다운 문법 기호 존재 여부를 먼저 검사한다. 없으면 `marked.lexer` 호출 자체를 건너뛰고 단순 단락 토큰을 직접 생성한다(~3ms 절감). +2. **모듈 수준 LRU 토큰 캐시**: `tokenCache` 맵이 최대 500개 항목을 보관한다. `useMemo`는 언마운트 시 소멸되지만 이 캐시는 컴포넌트 생명주기를 초월하므로, 가상 스크롤로 재마운트된 메시지도 재파싱 없이 토큰을 재사용한다. +3. **코드 하이라이팅 지연 로딩**: `Suspense` + `use()` 패턴으로 구문 강조 결과를 비동기 로드하여 초기 렌더링을 차단하지 않는다. + +### 5.2 입력 시스템 + +#### PromptInput — 사용자 입력 오케스트레이터 + +`src/components/PromptInput/PromptInput.tsx`는 사용자 입력과 관련된 거의 모든 기능을 통합한다. + +**주요 책임:** + +- Vim 모드와 일반 모드 간 전환 (`VimTextInput` / 일반 입력) +- 자동완성 및 타입어헤드 제안 (`useTypeahead`, `usePromptSuggestion`) +- 이미지 클립보드 붙여넣기 (`getImageFromClipboard`) +- 화살표 키 이력 탐색 (`useArrowKeyHistory`) +- 이력 검색 (`useHistorySearch`) +- 대기 중인 명령 큐 표시 (`PromptInputQueuedCommands`) +- 권한 모드 표시 및 전환 (`cyclePermissionMode`) +- 에이전트 스웜 직접 메시지 (`parseDirectMemberMessage`) +- 빠른 모드(fast mode) 지원 +- 외부 편집기 연동 (`editPromptInEditor`) + +`useKeybinding` / `useKeybindings` 훅을 통해 `~/.claude/keybindings.json`의 사용자 정의 단축키를 동적으로 반영한다. + +#### VimTextInput.tsx — Vim 모드 입력 + +`useVimInput` 훅을 통해 Normal/Insert/Visual 모드 전환을 처리한다. `BaseTextInput` 위에 Vim 키바인딩 레이어를 추가하는 구조다. `isTerminalFocused` 상태에 따라 커서 표시 방식(`chalk.inverse` vs 기본)을 전환한다. + +#### BaseTextInput.tsx — 저수준 텍스트 입력 + +커서 위치, 텍스트 선택, 멀티라인 편집, 마스킹(비밀번호) 등 텍스트 입력의 기초 기능을 구현한다. `useInput` 훅으로 키보드 이벤트를 수신한다. + +### 5.3 디자인 시스템 + +`src/components/design-system/`은 테마 인식 기본 컴포넌트 세트를 제공한다. + +#### ThemedBox / ThemedText + +Ink 기본 `Box` / `Text` 컴포넌트를 래핑하여 `ThemeProvider`의 현재 테마를 자동 적용한다. `src/ink.ts`에서 `Box`, `Text`로 재수출되어 코드베이스 전체가 이 컴포넌트를 사용한다. + +#### ThemeProvider + +`dark` / `light` / `auto` 세 가지 테마 설정을 관리한다. `auto` 모드에서는 `getSystemThemeName()`으로 터미널 배경색을 감지한 뒤, OSC 11 시퀀스 폴링으로 실시간 보정한다. + +**테마 전환 흐름:** + +``` +사용자 ThemePicker 선택 + → setThemeSetting() / setPreviewTheme() + → ThemeContext 업데이트 + → useTheme() 구독 컴포넌트 리렌더링 + → 확정 시 saveGlobalConfig() 영속화 +``` + +#### 기타 디자인 시스템 컴포넌트 + +| 컴포넌트 | 역할 | +|----------|------| +| `Dialog` | 모달 대화상자 기본 레이아웃 | +| `FuzzyPicker` | 퍼지 검색 선택 UI | +| `ProgressBar` | 진행률 표시 | +| `Tabs` | 탭 네비게이션 | +| `Ratchet` | 단방향 값 증가 애니메이션 | +| `StatusIcon` | 상태 아이콘 (성공/실패/대기) | +| `Byline` | 메시지 발신자 표시 | +| `LoadingState` | 로딩 스켈레톤 | +| `KeyboardShortcutHint` | 단축키 힌트 표시 | +| `Divider` | 구분선 | +| `ListItem` | 목록 항목 | +| `Pane` | 분할 패널 | + +### 5.4 다이얼로그 및 오버레이 + +수십 개의 다이얼로그 컴포넌트가 존재하며, 대표적인 것들은 다음과 같다. + +| 컴포넌트 | 목적 | +|----------|------| +| `PermissionRequest` | 도구 실행 권한 확인 | +| `MCPServerApprovalDialog` | MCP 서버 승인 | +| `CostThresholdDialog` | 비용 임계값 경고 | +| `GlobalSearchDialog` | 전역 검색 | +| `HistorySearchDialog` | 이력 검색 | +| `ExportDialog` | 대화 내보내기 | +| `IdleReturnDialog` | 유휴 상태 복귀 확인 | +| `BridgeDialog` | IDE 브릿지 연결 | + +모든 다이얼로그는 `overlayContext`를 통해 포커스 관리와 배경 UI 차단을 처리한다. + +### 5.5 가상 스크롤 + +`VirtualMessageList.tsx`는 대화 이력이 길어질 때 렌더링 성능을 유지하기 위해 뷰포트에 보이는 메시지만 마운트한다. 스크롤 오프셋을 추적하며 범위 밖의 메시지는 `OffscreenFreeze`를 통해 렌더링을 동결시킨다. `JumpHandle` ref로 특정 메시지로 즉시 이동하는 기능도 지원한다. + +### 5.6 MCP 및 권한 관련 컴포넌트 + +`src/components/mcp/`와 `src/components/permissions/` 디렉터리에는 MCP 서버 연결, 권한 요청 UI가 구현되어 있다. `WorkerPendingPermission`은 멀티-에이전트 환경에서 워커가 권한 대기 중임을 리더에게 시각적으로 알린다. + +--- + +## 6. 스트리밍 응답 렌더링 + +### 6.1 Spinner 컴포넌트 + +`src/components/Spinner.tsx`는 Claude가 응답을 생성하는 동안 표시되는 시각적 피드백 컴포넌트다. 단순한 회전 애니메이션을 넘어 다양한 상태 정보를 표현한다. + +**SpinnerMode 열거:** + +| 모드 | 의미 | +|------|------| +| 일반 | 표준 처리 중 | +| `brief` | 간결한 모드 (최소 UI) | +| 스웜 활성 | 멀티-에이전트 실행 중 | + +**표시 정보:** +- 회전 애니메이션 (SHIMMER_INTERVAL_MS 기반 글리머 효과) +- 경과 시간 (`formatSecondsShort`) +- 생성 중인 토큰 수 (`getTurnOutputTokens`) +- 토큰 예산 잔량 (`getCurrentTurnTokenBudget`) +- 활성 도구 이름 (`spinnerTip`) +- 에이전트 트리 (`TeammateSpinnerTree`) — 멀티-에이전트 시 +- 스톨 감지 시 색상 변경 (빨간색) + +`useAnimationFrame` 훅으로 렌더링 프레임에 동기화된 부드러운 애니메이션을 구현한다. + +### 6.2 스트리밍 메시지 렌더링 흐름 + +``` +API 스트림 청크 수신 + → REPL.tsx의 메시지 상태 업데이트 + → Messages.tsx → Message.tsx 리렌더링 + → AssistantTextMessage → Markdown.tsx + → 증분 토큰 추가 → 부분 파싱 + → Ink reconciler가 변경된 텍스트 셀만 갱신 + → 터미널에 최소 diff 출력 +``` + +React 컴파일러의 자동 메모이제이션이 스트리밍 중에도 변경되지 않은 컴포넌트(이전 메시지들, 상태 표시줄 등)의 불필요한 리렌더링을 방지한다. + +### 6.3 MessageResponse.tsx + +응답 완료 후 최종 메시지 표시 및 후처리(비용 계산, 이력 저장 등)를 담당하는 컴포넌트. `Spinner`의 `verbose` 플래그와 연동하여 상세/간결 모드를 전환한다. + +--- + +## 7. Vim 모드 연동 + +### 7.1 아키텍처 + +Vim 모드는 `PromptInput` → `VimTextInput` → `useVimInput` 훅 계층으로 구성된다. + +``` +PromptInput (모드 관리자) + → VimMode 상태 ('normal' | 'insert' | 'visual') + → VimTextInput (렌더러) + → useVimInput (키 처리 로직) + → BaseTextInput (저수준 텍스트 조작) +``` + +### 7.2 useVimInput 훅 + +Vim 키바인딩의 실제 처리를 담당한다. 주요 구현 사항: + +- **Normal 모드**: `h/j/k/l` 이동, `w/b/e` 단어 이동, `0/$` 줄 처음/끝, `dd` 줄 삭제, `yy` 복사, `p` 붙여넣기, `u` 실행 취소, `i/a/A/I/o/O` Insert 진입 +- **Insert 모드**: 일반 텍스트 입력, `Esc`로 Normal 복귀 +- **Visual 모드**: 텍스트 선택, `y` 복사, `d` 삭제 + +### 7.3 VimTextInput의 렌더링 전략 + +`isTerminalFocused`와 `props.showCursor`에 따라 커서 표시 방식을 결정한다: + +- 터미널 포커스 O + Normal 모드: `chalk.inverse` (반전 배경으로 블록 커서) +- Insert 모드: 밑줄 커서 또는 숨김 +- 터미널 포커스 X: 커서 숨김 + +React 컴파일러가 생성한 `_c(38)` 캐시(38개 슬롯)가 커서 위치나 모드 이외의 props 변경 시 불필요한 `useVimInput` 재실행을 방지한다. + +--- + +## 내비게이션 + +**상위 문서:** [Level 2 시스템 개요](../README.md) + +**동일 레벨 문서:** +- [쿼리 엔진](query-engine.md) — API 통신 및 스트리밍 처리 +- [도구 시스템](tool-system.md) — 도구 실행 및 결과 처리 +- [권한 시스템](permission-system.md) — 권한 요청 및 승인 흐름 +- [에이전트 코디네이터](agent-coordinator.md) — 멀티-에이전트 조율 + +**하위 문서 (Level 3):** +- `src/ink/reconciler.ts` — React Reconciler 구현 상세 +- `src/ink/renderer.ts` — 이중 버퍼 렌더러 상세 +- `src/components/design-system/` — 디자인 시스템 컴포넌트 전체 목록 diff --git a/docs/ko/level-3-internals/bridge-ide.md b/docs/ko/level-3-internals/bridge-ide.md new file mode 100644 index 0000000..5245fc4 --- /dev/null +++ b/docs/ko/level-3-internals/bridge-ide.md @@ -0,0 +1,263 @@ +# IDE 브릿지 분석: VS Code/JetBrains 양방향 통신 + +> **레벨**: 내부 구현 (Level 3) +> **대상 독자**: Claude Code 핵심 기여자, 브릿지 프로토콜 확장 개발자 +> **관련 소스**: `src/bridge/` + +--- + +## 개요 + +Claude Code의 IDE 브릿지(Remote Control Bridge)는 로컬 개발 환경과 claude.ai 웹 클라이언트 사이의 양방향 통신 채널을 구현한다. 단순한 WebSocket 래퍼가 아니라, 환경 등록(Environment Registration), 작업 폴링(Work Polling), 세션 수명주기 관리, JWT 기반 토큰 갱신을 통합한 완결된 원격 제어 프레임워크다. + +브릿지는 두 가지 아키텍처 경로를 지원한다: + +- **환경 기반 경로(Env-based, `bridgeMain.ts`)**: Environments API 레이어를 통해 작업을 디스패치하는 고전적 방식. `claude remote-control` 명령이 이 경로를 사용한다. +- **환경 비의존 경로(Env-less, `remoteBridgeCore.ts`)**: `/v1/code/sessions/{id}/bridge` 엔드포인트를 통해 직접 세션 인그레스에 연결하는 신형 경로. REPL 전용이며 `tengu_bridge_repl_v2` GrowthBook 플래그로 활성화된다. + +--- + +## 아키텍처 다이어그램 + +```mermaid +graph TB + subgraph "클라이언트 (로컬 머신)" + RC["claude remote-control\n(bridgeMain.ts)"] + RB["REPL Bridge\n(replBridge.ts)"] + RBC["Env-less Core\n(remoteBridgeCore.ts)"] + end + + subgraph "브릿지 서브시스템" + BA["BridgeApiClient\n(bridgeApi.ts)"] + JWT["Token Refresh Scheduler\n(jwtUtils.ts)"] + SS["SessionSpawner\n(sessionRunner.ts)"] + FG["FlushGate\n(flushGate.ts)"] + MSG["BridgeMessaging\n(bridgeMessaging.ts)"] + UI["BridgeUI\n(bridgeUI.ts)"] + end + + subgraph "Anthropic 백엔드" + ENV["Environments API\n/v1/environments"] + POLL["Work Polling\n/environments/{id}/work"] + SES["Session Ingress\n(WebSocket/SSE)"] + BRIDGE_EP["Bridge Endpoint\n/v1/code/sessions/{id}/bridge"] + end + + RC --> BA + BA --> ENV + BA --> POLL + RC --> SS + SS --> SES + RC --> JWT + + RB --> RBC + RBC --> BRIDGE_EP + RBC --> JWT + RBC --> MSG + RBC --> FG + + JWT -->|"OAuth 토큰으로 새 JWT 발급"| SES + MSG -->|"인바운드 메시지 파싱"| FG + UI -->|"상태 표시"| RC + + style RC fill:#2d6a9f,color:#fff + style RBC fill:#2d6a9f,color:#fff + style JWT fill:#5a4f9a,color:#fff + style SES fill:#1e7a4f,color:#fff +``` + +### 환경 기반 경로 상세 시퀀스 + +```mermaid +sequenceDiagram + participant CLI as claude remote-control + participant API as BridgeApiClient + participant ENV as Environments API + participant SS as SessionSpawner + participant SI as Session Ingress + + CLI->>API: registerBridgeEnvironment(config) + API->>ENV: POST /v1/environments + ENV-->>API: {environment_id, environment_secret} + + loop 작업 폴링 (long-poll) + CLI->>API: pollForWork(envId, envSecret) + API->>ENV: GET /environments/{id}/work + ENV-->>API: WorkResponse {id, type, data, secret} + CLI->>API: acknowledgeWork(envId, workId, sessionToken) + CLI->>SS: spawn({sessionId, sdkUrl, accessToken}) + SS->>SI: WebSocket/SSE 연결 수립 + Note over CLI,SI: 세션 실행 중 + CLI->>API: heartbeatWork(envId, workId, sessionToken) + SS-->>CLI: SessionDoneStatus + CLI->>API: archiveSession(sessionId) + end + + CLI->>API: deregisterEnvironment(envId) +``` + +--- + +## 핵심 구현 분석 + +### 1. 환경 등록 및 작업 디스패치 (`bridgeMain.ts`) + +브릿지 초기화는 `initBridgeCore()`에서 시작된다. 이 함수는 `BridgeConfig` 구조체를 받아 전체 폴링 루프를 구동한다. + +```typescript +// types.ts - BridgeConfig 핵심 필드 +export type BridgeConfig = { + dir: string // 작업 디렉토리 + machineName: string // 환경 표시명 + bridgeId: string // 클라이언트 생성 UUID (멱등성) + environmentId: string // 환경 등록용 UUID + reuseEnvironmentId?: string // 재연결 시 백엔드 환경 ID + spawnMode: SpawnMode // 'single-session' | 'worktree' | 'same-dir' + workerType: string // 'claude_code' | 'claude_code_assistant' + sessionTimeoutMs?: number // 기본값: 24시간 +} +``` + +`SpawnMode`는 다중 세션 처리 전략을 결정한다. `worktree` 모드에서는 각 세션마다 독립된 git worktree를 생성하여 병렬 세션 간 파일 충돌을 방지한다. + +### 2. JWT 토큰 갱신 스케줄러 (`jwtUtils.ts`) + +세션 토큰의 선제적(proactive) 갱신은 `createTokenRefreshScheduler()`가 담당한다. 만료 5분 전에 갱신을 시도하는 버퍼 기반 설계다. + +```typescript +const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000 // 만료 5분 전 갱신 +const FALLBACK_REFRESH_INTERVAL_MS = 30 * 60 * 1000 // JWT 불투명 시 30분 주기 +const MAX_REFRESH_FAILURES = 3 // 연속 실패 서킷 브레이커 +``` + +**세대(generation) 카운터 패턴**: 비동기 갱신 경쟁 조건을 방지하기 위해 세션별 세대 카운터를 사용한다. `schedule()` 또는 `cancel()` 호출 시 세대가 증가하며, 진행 중인 `doRefresh()`는 세대 불일치를 감지하면 즉시 중단한다. + +```typescript +// 세대 불일치 감지 - 갱신 경쟁 방지 +if (generations.get(sessionId) !== gen) { + logForDebugging(`stale gen ${gen} vs ${generations.get(sessionId)}, skipping`) + return +} +``` + +**두 가지 스케줄링 방식**: +- `schedule(sessionId, token)`: JWT `exp` 클레임을 디코딩하여 정확한 타이머 설정 +- `scheduleFromExpiresIn(sessionId, expiresInSeconds)`: 서버가 `expires_in`을 직접 반환할 때 사용. `/v1/code/sessions/{id}/bridge` 응답이 이 방식을 사용 + +갱신 성공 후에는 `FALLBACK_REFRESH_INTERVAL_MS` 뒤에 후속 갱신을 예약하여, 장기 세션에서 인증이 끊기지 않도록 보장한다. + +### 3. 환경 비의존 브릿지 코어 (`remoteBridgeCore.ts`) + +신형 REPL 브릿지는 Environments API 레이어를 완전히 우회한다. 연결 수립 순서: + +``` +1. POST /v1/code/sessions (OAuth 인증) → session.id +2. POST /v1/code/sessions/{id}/bridge (OAuth 인증) → {worker_jwt, expires_in, api_base_url, worker_epoch} +3. createV2ReplTransport(worker_jwt, worker_epoch) → SSE + CCRClient +4. createTokenRefreshScheduler → /bridge 재호출로 선제적 JWT 갱신 +5. 401 발생 시 → 새 /bridge 자격증명으로 트랜스포트 재구성 +``` + +`/bridge` 엔드포인트 호출 자체가 worker 등록 역할을 겸한다. 호출마다 epoch가 증가하므로 별도의 `/worker/register` 단계가 불필요하다. + +세 가지 재연결 원인(`ConnectCause`)이 구분된다: +- `initial`: 초기 연결 +- `proactive_refresh`: 토큰 만료 전 선제적 재연결 +- `auth_401_recovery`: 401 응답 후 복구 + +### 4. 메시지 프로토콜 (`bridgeMessaging.ts`) + +`handleIngressMessage()`와 `handleServerControlRequest()`가 양방향 메시지 처리를 담당한다. `BoundedUUIDSet`은 중복 메시지 처리 방지를 위한 링 버퍼 기반 UUID 집합이다. + +```typescript +// 메시지 적격성 검사 - 브릿지가 처리할 메시지 필터링 +export function isEligibleBridgeMessage(message: unknown): boolean +// 서버 제어 요청 처리 (권한 결정 등) +export function handleServerControlRequest(request: SDKControlRequest): SDKControlResponse +// 세션 제목 추출 (최초 사용자 메시지 기반) +export function extractTitleText(messages: Message[]): string | null +``` + +`FlushGate`는 메시지 플러싱을 제어하는 게이트 메커니즘으로, 세션 초기화 완료 전 조기 메시지 전송을 차단한다. + +### 5. WorkSecret 디코딩 (`workSecret.ts`) + +폴링으로 수신한 `WorkResponse`의 `secret` 필드는 base64url 인코딩된 JSON이다. + +```typescript +export type WorkSecret = { + version: number + session_ingress_token: string // 세션 인그레스 JWT + api_base_url: string + sources: Array<{ type: string; git_info?: {...} }> + auth: Array<{ type: string; token: string }> + claude_code_args?: Record | null + mcp_config?: unknown | null + environment_variables?: Record | null + use_code_sessions?: boolean // CCR v2 선택자 +} +``` + +`buildSdkUrl()` 및 `buildCCRv2SdkUrl()`은 이 시크릿에서 SDK URL을 구성한다. + +### 6. 신뢰 디바이스 토큰 (`trustedDevice.ts`) + +`getTrustedDeviceToken()`은 디바이스 식별을 위한 신뢰 토큰을 반환한다. 이 토큰은 환경 등록 요청에 포함되어 백엔드가 재연결 시 동일 디바이스임을 확인하는 데 사용된다. + +--- + +## 설계 결정 + +### 백오프 전략 + +연결 실패 시 지수 백오프를 적용하되 두 개의 독립적 파라미터 집합을 사용한다: + +```typescript +const DEFAULT_BACKOFF: BackoffConfig = { + connInitialMs: 2_000, // 연결 실패: 2초 시작 + connCapMs: 120_000, // 최대 2분 + connGiveUpMs: 600_000, // 10분 후 포기 + generalInitialMs: 500, // 일반 오류: 500ms 시작 + generalCapMs: 30_000, // 최대 30초 + generalGiveUpMs: 600_000, // 10분 후 포기 +} +``` + +연결 오류(네트워크 단절)와 일반 오류(API 오류)를 구분하여 서로 다른 백오프 파라미터를 적용한다. 이는 일시적인 네트워크 장애에서 더 공격적으로 재연결을 시도하면서, 서버 측 오류에서는 보수적으로 처리하기 위함이다. + +### 다중 세션 GrowthBook 게이팅 + +다중 세션 생성 모드(`--spawn`, `--capacity`)는 `tengu_ccr_bridge_multi_session` GrowthBook 플래그로 제어된다. 캐시 미스 시에도 차단(blocking) 게이트 확인을 사용하여 내부 사용자가 불공정하게 기능을 거부당하지 않도록 보장한다. + +### 멱등 환경 등록 + +`bridgeId`와 `environmentId`는 모두 클라이언트에서 생성한 UUID다. 브릿지가 재시작될 때 동일한 UUID를 사용하면 백엔드가 중복 환경 생성을 방지할 수 있다. `reuseEnvironmentId`는 `--session-id` 재개 시 백엔드 형식의 환경 ID를 재사용하기 위한 별도 필드다. + +### 세션 활동 링 버퍼 + +`SessionHandle.activities`는 최근 약 10개의 활동을 추적하는 링 버퍼다. 이 설계는 메모리를 상한으로 제한하면서 UI 상태 표시(`bridgeUI.ts`)에 충분한 컨텍스트를 제공한다. + +--- + +## 관련 파일 참조 + +| 파일 | 역할 | +|------|------| +| `src/bridge/bridgeMain.ts` | 환경 기반 브릿지 진입점, 메인 폴링 루프 | +| `src/bridge/remoteBridgeCore.ts` | 환경 비의존 REPL 브릿지 코어 | +| `src/bridge/jwtUtils.ts` | JWT 디코딩, 토큰 갱신 스케줄러 | +| `src/bridge/types.ts` | 핵심 타입 정의 (BridgeConfig, WorkSecret 등) | +| `src/bridge/bridgeMessaging.ts` | 인바운드/아웃바운드 메시지 처리 | +| `src/bridge/bridgeApi.ts` | Environments API 클라이언트 구현 | +| `src/bridge/sessionRunner.ts` | 세션 프로세스 스폰 및 관리 | +| `src/bridge/replBridgeTransport.ts` | CCR v2 SSE 트랜스포트 | +| `src/bridge/workSecret.ts` | WorkSecret 디코딩, SDK URL 빌더 | +| `src/bridge/trustedDevice.ts` | 신뢰 디바이스 토큰 | + +--- + +## 탐색 링크 + +- [컨텍스트 압축 & 토큰 관리](./context-compression.md) +- [인증 흐름 분석 (OAuth 2.0)](./oauth-auth.md) +- [Level 2: 아키텍처 개요](../level-2-architecture/) diff --git a/docs/ko/level-3-internals/context-compression.md b/docs/ko/level-3-internals/context-compression.md new file mode 100644 index 0000000..e077585 --- /dev/null +++ b/docs/ko/level-3-internals/context-compression.md @@ -0,0 +1,326 @@ +# 컨텍스트 압축 & 토큰 관리 + +> **레벨**: 내부 구현 (Level 3) +> **대상 독자**: Claude Code 핵심 기여자, 컨텍스트 관리 전략 연구자 +> **관련 소스**: `src/services/compact/`, `src/services/tokenEstimation.ts`, `src/memdir/`, `src/context/` + +--- + +## 개요 + +Claude Code의 컨텍스트 관리 시스템은 단일 메커니즘이 아니라 다중 계층으로 구성된 통합 전략이다. 대화가 길어질수록 토큰 사용량이 컨텍스트 윈도우 한계에 근접하며, 이를 처리하기 위해 다음 네 가지 서브시스템이 협력한다: + +1. **토큰 추정 엔진** (`tokenEstimation.ts`): API 호출 없이 빠른 토큰 수 계산 +2. **자동 압축(AutoCompact)** (`autoCompact.ts`): 임계값 초과 시 대화를 자동 요약 +3. **대화 압축(Compact)** (`compact.ts`): 실제 요약 생성 및 메시지 교체 +4. **메모리 디렉토리(Memdir)** (`memdir/`): 장기 기억을 파일 시스템에 영속화 + +세 가지 상호 배타적인 컨텍스트 관리 모드가 존재하며, GrowthBook 플래그를 통해 선택된다: 표준 AutoCompact, 리액티브 압축(`REACTIVE_COMPACT`), 컨텍스트 콜랩스(`CONTEXT_COLLAPSE`). + +--- + +## 아키텍처 다이어그램 + +```mermaid +graph TB + subgraph "토큰 모니터링" + TE["tokenEstimation.ts\n빠른 토큰 추정"] + TW["calculateTokenWarningState()\n임계값 상태 계산"] + CWH["compactWarningHook.ts\nUI 경고 표시"] + end + + subgraph "압축 의사결정" + SAC["shouldAutoCompact()\n압축 필요성 판단"] + ACIN["autoCompactIfNeeded()\n압축 실행 조정"] + CB["Circuit Breaker\n연속 실패 3회 시 중단"] + end + + subgraph "압축 실행" + SMC["trySessionMemoryCompaction()\nSession Memory 우선 시도"] + CC["compactConversation()\n전체 대화 요약"] + FA["runForkedAgent()\n독립 에이전트로 요약 생성"] + PCC["runPostCompactCleanup()\n압축 후 정리"] + end + + subgraph "메모리 시스템" + MD["memdir/\n장기 기억 저장소"] + FRM["findRelevantMemories.ts\n관련 기억 검색"] + MS["memoryScan.ts\n기억 스캔"] + end + + subgraph "컨텍스트 윈도우" + CW["getEffectiveContextWindowSize()\n실효 윈도우 계산"] + ACT["getAutoCompactThreshold()\n압축 임계값"] + end + + TE --> TW + TW --> SAC + SAC --> ACIN + ACIN --> CB + CB --> SMC + SMC -->|"실패 시"| CC + CC --> FA + FA -->|"요약 완료"| PCC + CW --> ACT + ACT --> SAC + MD --> FRM + FRM --> CC + + style ACIN fill:#2d6a9f,color:#fff + style CC fill:#5a4f9a,color:#fff + style SMC fill:#1e7a4f,color:#fff + style CB fill:#7a2020,color:#fff +``` + +### 토큰 임계값 계층 + +```mermaid +graph LR + CW["컨텍스트 윈도우\n(모델별 상이)"] + RES["예약 토큰\n(최대 20,000)"] + ECW["실효 윈도우\n= CW - RES"] + ACT["AutoCompact 임계값\n= ECW - 13,000"] + WT["경고 임계값\n= ECW - 20,000"] + ET["오류 임계값\n= ECW - 20,000"] + BL["차단 한계\n= ECW - 3,000"] + + CW --> RES --> ECW + ECW --> ACT + ECW --> WT + ECW --> ET + ECW --> BL + + style ACT fill:#d4a017,color:#000 + style BL fill:#c0392b,color:#fff + style WT fill:#e67e22,color:#fff +``` + +--- + +## 핵심 구현 분석 + +### 1. 토큰 추정 엔진 (`tokenEstimation.ts`) + +실제 API 토큰 카운팅은 네트워크 왕복 비용이 발생한다. `tokenEstimation.ts`는 로컬에서 빠르게 추정값을 계산하여 실시간 의사결정에 사용한다. + +다중 API 공급자(Anthropic 직접, Bedrock, Vertex)를 지원하며, 각 공급자에 특화된 토큰 카운팅 로직이 구현되어 있다. Bedrock의 경우 `@aws-sdk/client-bedrock-runtime`을 동적으로 임포트하여 초기 번들 크기(약 279KB)를 절약한다. + +**thinking 블록 처리**: 어시스턴트 메시지에 `thinking` 또는 `redacted_thinking` 블록이 포함된 경우 `hasThinkingBlocks()`가 이를 감지하여 토큰 카운팅 시 최소 예산(`TOKEN_COUNT_THINKING_BUDGET = 1024`)을 적용한다. + +**tool search 필드 정제**: `stripToolSearchFieldsFromMessages()`는 tool search 베타에서만 유효한 `caller` 필드를 tool_use 블록에서 제거한다. API 오류를 방지하기 위해 토큰 카운팅 전에 메시지를 정제하는 방어적 설계다. + +### 2. 실효 컨텍스트 윈도우 계산 (`autoCompact.ts`) + +```typescript +// 출력 예약 토큰 상한: 20,000 (p99.99 압축 요약 크기) +const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 + +export function getEffectiveContextWindowSize(model: string): number { + const reservedTokensForSummary = Math.min( + getMaxOutputTokensForModel(model), + MAX_OUTPUT_TOKENS_FOR_SUMMARY, + ) + let contextWindow = getContextWindowForModel(model, getSdkBetas()) + + // 환경변수 오버라이드 (테스트용) + const autoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW + if (autoCompactWindow) { + const parsed = parseInt(autoCompactWindow, 10) + if (!isNaN(parsed) && parsed > 0) { + contextWindow = Math.min(contextWindow, parsed) + } + } + + return contextWindow - reservedTokensForSummary +} +``` + +`MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20,000`은 압축 요약 출력의 p99.99 관찰값(17,387 토큰)에서 도출된 경험적 수치다. + +### 3. 임계값 상태 계산 + +다섯 가지 임계값 상태가 계산된다: + +```typescript +export const AUTOCOMPACT_BUFFER_TOKENS = 13_000 // AutoCompact 발동 버퍼 +export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 // 경고 표시 버퍼 +export const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000 // 오류 표시 버퍼 +export const MANUAL_COMPACT_BUFFER_TOKENS = 3_000 // 차단 한계 버퍼 + +// calculateTokenWarningState() 반환 타입 +{ + percentLeft: number // 잔여 비율 (0-100) + isAboveWarningThreshold: boolean + isAboveErrorThreshold: boolean + isAboveAutoCompactThreshold: boolean + isAtBlockingLimit: boolean // 입력 차단 여부 +} +``` + +`isAtBlockingLimit`이 `true`이면 새 사용자 입력이 거부된다. 이는 API `prompt_too_long` 오류가 발생하기 전에 시스템이 선제적으로 개입하는 최후 방어선이다. + +### 4. 자동 압축 의사결정 (`shouldAutoCompact()`) + +압축 실행 전 여러 가드 조건이 확인된다: + +```typescript +// 재귀 방지: session_memory, compact 소스는 압축 불가 +if (querySource === 'session_memory' || querySource === 'compact') return false + +// marble_origami (컨텍스트 에이전트) 방지 +if (feature('CONTEXT_COLLAPSE') && querySource === 'marble_origami') return false + +// 리액티브 전용 모드: 선제적 AutoCompact 억제 +if (feature('REACTIVE_COMPACT') && getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) return false + +// 컨텍스트 콜랩스 모드: AutoCompact 억제 (콜랩스가 90%/95% 지점을 처리) +if (feature('CONTEXT_COLLAPSE') && isContextCollapseEnabled()) return false +``` + +`snipTokensFreed` 파라미터는 snip 작업으로 이미 회수된 토큰을 차감하여 이중 계산을 방지한다. + +### 5. 서킷 브레이커 패턴 + +```typescript +const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 + +// 연속 실패 3회 후 해당 세션의 AutoCompact를 완전히 비활성화 +if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) { + return { wasCompacted: false } +} +``` + +이 서킷 브레이커는 2026-03-10 BQ 분석 데이터에 기반한다: 단일 세션에서 50회 이상 연속 실패가 1,279개 세션에서 관찰되었으며 (최대 3,272회), 이로 인해 하루 약 250,000건의 불필요한 API 호출이 발생했다. + +### 6. 압축 실행 순서 (`autoCompactIfNeeded()`) + +```mermaid +flowchart TD + A[shouldAutoCompact 확인] -->|true| B[trySessionMemoryCompaction 시도] + B -->|성공| C[setLastSummarizedMessageId 초기화] + C --> D[runPostCompactCleanup] + D --> E[notifyCompaction 캐시 베이스라인 초기화] + E --> F[markPostCompaction] + + B -->|실패/없음| G[compactConversation 실행] + G --> H[runForkedAgent로 요약 생성] + H --> I[setLastSummarizedMessageId 초기화] + I --> J[runPostCompactCleanup] + + G -->|오류| K[consecutiveFailures 증가] + K --> L{3회 이상?} + L -->|yes| M[서킷 브레이커 트립] + L -->|no| N[실패 반환] +``` + +Session Memory 압축이 우선 시도되는 이유: 세션 메모리는 메시지를 직접 정리하므로 비용이 낮다. 실패 시에만 전체 대화 요약(`compactConversation`)으로 폴백한다. + +### 7. 대화 압축 구현 (`compact.ts`) + +`compactConversation()`은 현재 대화를 독립 포크 에이전트(`runForkedAgent`)에게 전달하여 요약을 생성한다. 요약 생성에 사용되는 최대 출력 토큰은 `COMPACT_MAX_OUTPUT_TOKENS`로 제한된다. + +주요 처리 단계: +1. `executePreCompactHooks()`: 압축 전 훅 실행 +2. `analyzeContext()`: 현재 컨텍스트 분석 (통계 수집) +3. `runForkedAgent()`: 독립 에이전트로 요약 프롬프트 실행 +4. `createCompactBoundaryMessage()`: 압축 경계 메시지 생성 +5. `normalizeMessagesForAPI()`: API 전송용 메시지 정규화 +6. `reAppendSessionMetadata()`: 세션 메타데이터 재추가 +7. `executePostCompactHooks()`: 압축 후 훅 실행 + +`RecompactionInfo` 구조체는 연쇄 압축 추적에 사용된다: + +```typescript +export type RecompactionInfo = { + isRecompactionInChain: boolean // 이전 압축 후 재압축 여부 + turnsSincePreviousCompact: number // 이전 압축 이후 턴 수 + previousCompactTurnId?: string // 이전 압축 턴 ID + autoCompactThreshold: number // 적용된 임계값 + querySource?: QuerySource // 압축 유발 소스 +} +``` + +### 8. 메모리 디렉토리 시스템 (`memdir/`) + +`memdir`은 세션 간에 유지되는 장기 기억 저장소다. 파일 시스템 기반으로 구현되어 있으며, 기억은 유형별로 분류된다. + +```typescript +// memoryTypes.ts +export const MEMORY_TYPE_VALUES = [ + 'episodic', // 특정 사건 기억 + 'semantic', // 지식/사실 기억 + 'procedural', // 절차/방법 기억 +] +``` + +`findRelevantMemories.ts`는 현재 대화 컨텍스트와 관련된 기억을 검색하며, `memoryScan.ts`는 기억 디렉토리를 스캔하여 유효한 기억 항목을 필터링한다. `memoryAge.ts`는 기억의 노화(aging) 정책을 구현한다. + +팀 메모리(`teamMemPaths.ts`, `teamMemPrompts.ts`)는 다중 에이전트 시나리오에서 에이전트 간 공유 기억을 관리한다. + +### 9. 컨텍스트 윈도우 모달 컨텍스트 (`src/context/`) + +`src/context/` 디렉토리는 UI 레이어의 React 컨텍스트를 포함한다. `mailbox.tsx`는 에이전트 간 메시지 큐를 관리하며, `stats.tsx`는 토큰 통계를 React 상태로 노출한다. + +```typescript +// stats.tsx - 토큰 통계 컨텍스트 +// modalContext.tsx - 모달 상태 관리 +// QueuedMessageContext.tsx - 큐잉된 메시지 상태 +// fpsMetrics.tsx - 렌더링 성능 메트릭 +// voice.tsx - 음성 입력 컨텍스트 +``` + +--- + +## 설계 결정 + +### 세 가지 압축 모드의 공존 + +AutoCompact, 리액티브 압축, 컨텍스트 콜랩스는 서로 배타적이며 GrowthBook 플래그로 제어된다. 이 설계는 프로덕션 환경에서 A/B 테스트를 가능하게 하면서 기능 플래그 없이 빌드에서는 불필요한 코드를 tree-shaking으로 제거할 수 있게 한다(`feature('CONTEXT_COLLAPSE')` 패턴). + +컨텍스트 콜랩스 모드에서 AutoCompact가 억제되는 이유: 콜랩스의 90% 커밋 / 95% 차단 흐름이 실효 컨텍스트의 약 93%에 해당하는 AutoCompact 임계값(13,000 토큰 버퍼)과 겹치기 때문이다. 두 시스템이 동시에 활성화되면 경쟁 조건이 발생한다. + +### 프롬프트 캐시 연속성 보장 + +압축 후 `notifyCompaction()`을 호출하여 프롬프트 캐시 베이스라인을 초기화한다. 이를 누락하면 압축 직후 캐시 읽기 드롭이 `systemPromptChanged=true`로 잘못 집계되어 `tengu_prompt_cache_break` 이벤트가 허위 양성으로 기록된다. 2026-03-01 BQ 분석에서 해당 이벤트의 20%가 허위 양성이었음이 확인되어 수정된 사항이다. + +### Session Memory 압축 우선순위 + +`trySessionMemoryCompaction()`이 `compactConversation()`보다 먼저 시도되는 이유는 비용 효율성이다. 세션 메모리 압축은 중요한 메시지를 선택적으로 정리하므로 전체 요약 생성 API 호출을 회피할 수 있다. `setLastSummarizedMessageId(undefined)` 초기화는 두 경로 모두에서 수행되어, 이전 메시지 UUID가 새 메시지 배열에 존재하지 않는 상황에서 발생하는 참조 오류를 방지한다. + +### 환경변수 오버라이드 + +다수의 임계값이 환경변수로 오버라이드 가능하다: +- `CLAUDE_CODE_AUTO_COMPACT_WINDOW`: 컨텍스트 윈도우 상한 설정 +- `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`: 퍼센트 기반 임계값 설정 (0-100) +- `CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE`: 차단 한계 오버라이드 +- `DISABLE_COMPACT`: 압축 전체 비활성화 +- `DISABLE_AUTO_COMPACT`: 자동 압축만 비활성화 (수동 `/compact`는 유지) + +--- + +## 관련 파일 참조 + +| 파일 | 역할 | +|------|------| +| `src/services/compact/autoCompact.ts` | AutoCompact 임계값, 의사결정, 서킷 브레이커 | +| `src/services/compact/compact.ts` | 대화 요약 생성 핵심 로직 | +| `src/services/compact/sessionMemoryCompact.ts` | Session Memory 기반 압축 | +| `src/services/compact/compactWarningHook.ts` | UI 경고 훅 | +| `src/services/compact/microCompact.ts` | 마이크로 압축 | +| `src/services/compact/prompt.ts` | 압축 프롬프트 템플릿 | +| `src/services/compact/postCompactCleanup.ts` | 압축 후 정리 | +| `src/services/tokenEstimation.ts` | 토큰 추정 엔진 (Anthropic/Bedrock/Vertex) | +| `src/memdir/memdir.ts` | 장기 기억 저장소 진입점 | +| `src/memdir/findRelevantMemories.ts` | 컨텍스트 기반 기억 검색 | +| `src/memdir/memoryScan.ts` | 기억 디렉토리 스캔 | +| `src/memdir/memoryTypes.ts` | 기억 유형 정의 | +| `src/context/stats.tsx` | 토큰 통계 React 컨텍스트 | +| `src/context/mailbox.tsx` | 에이전트 메시지 큐 | + +--- + +## 탐색 링크 + +- [IDE 브릿지 분석](./bridge-ide.md) +- [인증 흐름 분석 (OAuth 2.0)](./oauth-auth.md) +- [Level 2: 아키텍처 개요](../level-2-architecture/) diff --git a/docs/ko/level-3-internals/mcp-lsp-integration.md b/docs/ko/level-3-internals/mcp-lsp-integration.md new file mode 100644 index 0000000..2f40433 --- /dev/null +++ b/docs/ko/level-3-internals/mcp-lsp-integration.md @@ -0,0 +1,410 @@ +# MCP/LSP 프로토콜 연동 분석 + +> **레벨**: 3 — 내부 구현 심층 분석 +> **대상 독자**: Claude Code 내부 아키텍처를 이해하고자 하는 엔지니어, 프로토콜 통합 방식을 연구하는 개발자 +> **전제 지식**: TypeScript, JSON-RPC 2.0, MCP 사양(modelcontextprotocol.io), LSP 사양(microsoft.github.io/language-server-protocol) + +--- + +## 1. 개요 + +Claude Code는 두 가지 독립적인 확장 프로토콜을 병렬로 운영한다. + +| 프로토콜 | 목적 | 방향성 | 주요 표준 | +|----------|------|--------|-----------| +| **MCP** (Model Context Protocol) | AI 모델에 외부 도구·리소스·프롬프트 제공 | 양방향 (클라이언트 ↔ 서버) | Anthropic MCP Spec 2025-03-26 | +| **LSP** (Language Server Protocol) | 코드 인텔리전스 (정의, 참조, 진단 등) | 클라이언트 → 서버 (단방향 요청 + 서버 알림) | Microsoft LSP 3.17 | + +MCP는 Claude 모델이 사용할 수 있는 **도구(Tool)** 를 외부 서버로부터 동적으로 수급하는 메커니즘이며, LSP는 파일 편집·분석 작업 시 언어 서버로부터 **코드 인텔리전스**를 얻기 위한 메커니즘이다. 두 프로토콜은 서로 독립적으로 초기화되며, 각각 별도의 관리자(Manager) 싱글턴을 통해 생명주기가 제어된다. + +``` +┌─────────────────────────────────────────┐ +│ Claude Code │ +│ │ +│ ┌───────────────┐ ┌────────────────┐ │ +│ │ MCP 클라이언트 │ │ LSP 관리자 │ │ +│ │ (mcpClient) │ │ (manager.ts) │ │ +│ └──────┬────────┘ └───────┬────────┘ │ +│ │ │ │ +│ ┌──────▼────────┐ ┌───────▼────────┐ │ +│ │ MCPTool │ │ LSPTool │ │ +│ │ (Tool 매핑) │ │ (Tool 래퍼) │ │ +│ └───────────────┘ └────────────────┘ │ +└─────────────────────────────────────────┘ + │ │ + MCP 서버들 LSP 서버들 (플러그인 제공) + (stdio/SSE/HTTP/WS) (stdio, vscode-jsonrpc) +``` + +--- + +## 2. MCP (Model Context Protocol) 통합 + +### 2.1 서버 설정 및 스코프 계층 + +MCP 서버 설정은 `src/services/mcp/types.ts`에 Zod 스키마로 엄밀하게 정의된다. 설정은 **스코프(scope)** 개념을 통해 계층화된다. + +```typescript +// ConfigScopeSchema — 우선순위 낮은 순서 +type ConfigScope = + | 'enterprise' // 관리형 정책 파일 (최고 우선순위) + | 'user' // 사용자 전역 설정 + | 'project' // .mcp.json (프로젝트별) + | 'local' // 로컬 오버라이드 + | 'dynamic' // 런타임 동적 추가 + | 'claudeai' // claude.ai 클라우드 서버 + | 'managed' // 플러그인 제공 서버 +``` + +각 서버 항목은 `ScopedMcpServerConfig` 타입으로 래핑되어 원본 설정과 스코프가 함께 보존된다. `config.ts`의 `getAllMcpConfigs()`가 모든 소스를 병합하여 최종 서버 목록을 반환한다. + +프로젝트 수준 설정은 `.mcp.json` 파일에 저장된다. `writeMcpjsonFile()` 함수는 원자적 파일 쓰기를 구현한다 — 임시 파일에 기록(`O_WRONLY`), `datasync()`로 디스크 플러시, `rename()`으로 원자적 교체. 이는 설정 파일 손상을 방지하기 위한 의도적인 설계다. + +### 2.2 전송 계층 (Transport Layer) + +MCP는 여섯 가지 전송 유형을 지원하며, 각각 독립적인 연결 전략을 사용한다. + +``` +TransportType: + stdio — 서브프로세스 stdin/stdout (로컬 서버, 가장 일반적) + sse — Server-Sent Events over HTTP (원격, OAuth 지원) + sse-ide — IDE 확장용 SSE (인증 없음) + http — Streamable HTTP (MCP Spec 2025-03-26) + ws — WebSocket + ws-ide — IDE 확장용 WebSocket (authToken 지원) + sdk — 인프로세스 (InProcessTransport) + claudeai-proxy — claude.ai 프록시 서버 +``` + +`connectToServer()` 함수(`client.ts:595`)는 `memoize`로 래핑되어 동일한 서버에 대한 중복 연결을 방지한다. 캐시 키는 서버 이름과 설정 JSON의 조합(`getServerCacheKey()`)으로 생성된다. + +**연결 타임아웃**: 기본 30초(`MCP_TIMEOUT` 환경변수로 조정 가능). `Promise.race()`를 통해 연결과 타임아웃 프로미스를 경쟁시키며, 타임아웃 발생 시 전송 계층을 강제 종료한다. + +**배치 연결**: 로컬 서버(stdio/sdk)는 동시 3개, 원격 서버(SSE/HTTP/WS)는 동시 20개까지 병렬 연결한다(`pMap` 활용). 환경변수 `MCP_SERVER_CONNECTION_BATCH_SIZE`와 `MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE`로 조정 가능. + +#### HTTP 전송의 특수 처리 + +Streamable HTTP(`type: 'http'`)는 MCP 사양 2025-03-26을 따른다. POST 요청에는 반드시 `Accept: application/json, text/event-stream` 헤더가 포함되어야 한다. `wrapFetchWithTimeout()` 함수가 이 헤더를 보장하며, 동시에 60초 타임아웃을 적용한다. + +```typescript +// GET 요청(SSE 스트림)은 타임아웃에서 제외 — 장시간 유지되는 연결이기 때문 +if (method === 'GET') { + return baseFetch(url, init) +} +``` + +`AbortSignal.timeout()` 대신 `setTimeout` + `clearTimeout` 패턴을 사용한다. 이는 Bun 런타임에서 `AbortSignal.timeout()`이 GC 전까지 요청당 ~2.4KB의 네이티브 메모리를 누수하는 버그를 회피하기 위함이다. + +#### 인프로세스 서버 + +Chrome과 Computer Use 서버는 서브프로세스 대신 **인프로세스**로 실행된다. `createLinkedTransportPair()`로 연결된 `InProcessTransport` 쌍을 통해 통신하며, ~325MB의 서브프로세스 오버헤드를 제거한다. + +### 2.3 리소스 및 프롬프트 + +MCP 서버가 제공하는 **리소스(Resource)** 와 **프롬프트(Prompt)** 는 각각 `ListMcpResourcesTool`과 `ReadMcpResourceTool`을 통해 Claude에 노출된다. + +```typescript +type ServerResource = Resource & { server: string } +// server 필드를 추가하여 리소스 출처 서버를 식별 +``` + +`MCPCliState` 인터페이스는 연결된 클라이언트, 설정, 도구, 리소스, 정규화된 이름 매핑을 하나의 직렬화 가능한 상태로 묶는다. CLI 세션 간 상태 전달을 위한 구조다. + +### 2.4 Tool 매핑 + +MCP 서버가 제공하는 도구는 `MCPTool` 정적 템플릿을 기반으로 **동적으로 생성**된다. `MCPTool.ts`의 정적 정의는 모두 기본값(빈 이름, 빈 설명)이며, `mcpClient.ts`에서 실제 서버의 도구 메타데이터로 오버라이드된다. + +**도구 이름 정규화**: MCP 도구는 `mcp__{서버명}__{도구명}` 형식의 이름을 갖는다. `normalizeNameForMCP()` 함수가 API 패턴 `^[a-zA-Z0-9_-]{1,64}$`에 맞지 않는 문자를 언더스코어로 치환한다. + +```typescript +// normalization.ts +export function normalizeNameForMCP(name: string): string { + let normalized = name.replace(/[^a-zA-Z0-9_-]/g, '_') + if (name.startsWith('claude.ai ')) { + // claude.ai 서버: 연속 언더스코어 축소, 선두/말미 언더스코어 제거 + // __ 구분자와의 충돌 방지 + normalized = normalized.replace(/_+/g, '_').replace(/^_|_$/g, '') + } + return normalized +} +``` + +`MCPCliState.normalizedNames`는 정규화된 이름과 원본 이름 간의 역방향 매핑을 보존한다. + +**도구 설명 길이 제한**: OpenAPI 기반 MCP 서버가 15~60KB의 설명을 주입하는 사례가 관찰됨에 따라, `MAX_MCP_DESCRIPTION_LENGTH = 2048` 상수로 잘라낸다. + +**IDE 도구 필터링**: `mcp__ide__` 접두사를 가진 도구는 화이트리스트(`mcp__ide__executeCode`, `mcp__ide__getDiagnostics`)에 있는 것만 포함된다. + +**도구 호출 타임아웃**: 기본값 `100_000_000ms` (~27.8시간, 사실상 무한대). `MCP_TOOL_TIMEOUT` 환경변수로 조정 가능. + +### 2.5 OAuth 인증 통합 + +MCP OAuth 흐름은 `src/services/mcp/auth.ts`의 `ClaudeAuthProvider`가 담당한다. `@modelcontextprotocol/sdk/client/auth.js`의 `OAuthClientProvider` 인터페이스를 구현한다. + +``` +OAuth 인증 흐름: +1. 서버 연결 시도 → UnauthorizedError 발생 +2. handleRemoteAuthFailure() → needs-auth 상태로 전환 +3. 인증 캐시 확인 (isMcpAuthCached, TTL 15분) +4. ClaudeAuthProvider.auth() → 브라우저에서 인증 페이지 열기 +5. PKCE 코드 챌린지 생성 (SHA-256) +6. 콜백 서버 시작 (임의 포트) → 인가 코드 수신 +7. 토큰 교환 → Keychain 저장 +8. 재연결 시도 +``` + +**민감한 OAuth 파라미터 로그 보호**: `SENSITIVE_OAUTH_PARAMS`(`state`, `nonce`, `code_challenge`, `code_verifier`, `code`)는 로그 출력 전 `[REDACTED]`로 치환된다. + +**Slack 비표준 에러 정규화**: Slack의 `invalid_refresh_token`, `expired_refresh_token`, `token_expired`는 RFC 6749 표준 `invalid_grant`로 정규화된다. + +**XAA (Cross-App Access, SEP-990)**: 서버별 `xaa: true` 플래그를 통해 IdP 연동 토큰 교환을 수행하는 기업용 인증 확장. `xaaIdpLogin.ts`에서 OIDC 디스커버리와 IdP ID 토큰 획득을 처리한다. + +**인증 캐시**: `~/.claude/mcp-needs-auth-cache.json`에 서버별 인증 필요 상태를 캐싱한다. 동시 쓰기 경쟁(`writeChain` 프로미스 체인)을 직렬화하여 read-modify-write 레이스 조건을 방지한다. + +**세션 만료 처리**: HTTP 404 + JSON-RPC 에러 코드 `-32001`의 조합을 세션 만료로 판별한다. 이 조합이 아닌 일반 404(잘못된 URL, 서버 다운)와 구분하기 위해 두 가지 신호를 모두 확인한다. + +--- + +## 3. LSP (Language Server Protocol) 클라이언트 + +### 3.1 아키텍처 개요 + +LSP 통합은 세 계층으로 구성된다. + +``` +manager.ts — 싱글턴 생명주기 관리 + └─ LSPServerManager.ts — 다중 서버 라우팅 + └─ LSPServerInstance.ts — 단일 서버 인스턴스 + └─ LSPClient.ts — vscode-jsonrpc 래퍼 +``` + +LSP 서버는 **플러그인만을 통해** 제공된다. `config.ts`의 `getAllLspServers()`는 활성화된 플러그인들로부터 서버 설정을 로드한다. 사용자 설정이나 프로젝트 설정으로는 LSP 서버를 직접 등록할 수 없다. + +### 3.2 싱글턴 초기화 (`manager.ts`) + +LSP 관리자는 모듈 스코프 싱글턴 패턴으로 구현된다. 초기화 상태는 `'not-started' | 'pending' | 'success' | 'failed'` 네 가지다. + +```typescript +// 비동기 초기화 — 시작 시간을 차단하지 않음 +export function initializeLspServerManager(): void { + if (isBareMode()) return // --bare 모드에서는 LSP 비활성화 + lspManagerInstance = createLSPServerManager() + initializationState = 'pending' + initializationPromise = lspManagerInstance.initialize() + .then(() => { initializationState = 'success' }) + .catch((error) => { + initializationState = 'failed' + lspManagerInstance = undefined + }) +} +``` + +**세대 카운터(Generation Counter)**: `initializationGeneration` 값을 증가시켜 진행 중인 초기화 프로미스를 무효화한다. 플러그인 새로고침(`reinitializeLspServerManager()`) 시 이전 초기화 결과가 새 상태를 덮어쓰는 것을 방지한다. + +**플러그인 재초기화 문제**: `loadAllPlugins()`가 메모이즈되어 있어, 마켓플레이스 조정 전에 빈 플러그인 목록으로 캐싱될 수 있다. `reinitializeLspServerManager()`는 플러그인 캐시 갱신 후 LSP를 재초기화하여 이 문제를 해결한다. + +`isLspConnected()`는 `LSPTool.isEnabled()`의 게이트로, 실행 중인 서버가 하나 이상이고 `error` 상태가 아닌 경우에만 `true`를 반환한다. + +### 3.3 다중 서버 라우팅 (`LSPServerManager.ts`) + +`LSPServerManager`는 파일 확장자 기반 라우팅을 구현한다. + +```typescript +// 확장자 → 서버명 매핑 +const extensionMap: Map = new Map() + +function getServerForFile(filePath: string): LSPServerInstance | undefined { + const ext = path.extname(filePath).toLowerCase() + const serverNames = extensionMap.get(ext) + return serverNames ? servers.get(serverNames[0]!) : undefined +} +``` + +동일 확장자를 처리하는 서버가 여럿일 경우 첫 번째 등록된 서버를 사용한다(우선순위 로직 추가 예정). + +**열린 파일 추적**: `openedFiles: Map` (URI → 서버명)를 통해 동일 파일의 중복 `didOpen` 알림을 방지한다. + +**`workspace/configuration` 처리**: TypeScript 언어 서버 등은 `workspace/configuration` 요청을 보내지만, Claude Code는 이를 지원하지 않는다. 모든 항목에 `null`을 반환하여 프로토콜 요구사항을 충족시킨다. + +**셧다운 전략**: `running` 또는 `error` 상태의 서버만 명시적으로 중지한다. `Promise.allSettled()`를 사용하여 일부 서버 중지 실패가 나머지 서버 정리를 방해하지 않도록 한다. + +### 3.4 단일 서버 인스턴스 (`LSPServerInstance.ts`) + +서버 인스턴스의 상태 머신: + +``` +stopped → starting → running +running → stopping → stopped +any → error (크래시/실패) +error → starting (재시도, maxRestarts 상한 있음) +``` + +**크래시 복구**: `createLSPClient()`의 `onCrash` 콜백으로 크래시를 감지하여 `state = 'error'`로 전환한다. 다음 요청 시 `ensureServerStarted()`가 자동으로 재시작을 시도한다. `config.maxRestarts`(기본값 3)를 초과하면 `Error`를 던지고 재시도를 포기한다. + +**일시적 에러 재시도**: LSP 에러 코드 `-32801`("Content Modified")은 서버가 아직 인덱싱 중일 때 발생하는 일시적 에러다. 최대 3회, 500ms/1000ms/2000ms 지수 백오프로 재시도한다. + +**지연 로딩(Lazy Loading)**: `vscode-jsonrpc`(~129KB)는 LSP 서버가 실제로 인스턴스화될 때까지 로드하지 않는다. `require('./LSPClient.js')`를 런타임에 호출하여 정적 임포트 체인에서 제외한다. + +### 3.5 JSON-RPC 연결 (`LSPClient.ts`) + +`createLSPClient()`는 `child_process.spawn()`으로 LSP 서버 프로세스를 시작하고, `vscode-jsonrpc`의 `createMessageConnection()`으로 stdio 기반 JSON-RPC 연결을 수립한다. + +**스폰 경쟁 조건 처리**: `spawn()` 반환 직후 스트림을 사용하면 `ENOENT`(명령어 없음) 에러가 비동기적으로 발생하여 처리되지 않은 프로미스 거부가 생길 수 있다. 이를 방지하기 위해 `'spawn'` 이벤트를 기다린 후에 스트림을 사용한다. + +```typescript +await new Promise((resolve, reject) => { + spawnedProcess.once('spawn', () => { cleanup(); resolve() }) + spawnedProcess.once('error', (error) => { cleanup(); reject(error) }) +}) +``` + +**에러 핸들러 등록 순서**: `connection.onError()`와 `connection.onClose()`를 `connection.listen()` **이전에** 등록한다. 서버가 즉시 크래시하는 경우 모든 에러를 캡처하기 위함이다. + +**stdin 에러 격리**: LSP 서버 프로세스가 종료된 후 stdin에 쓰기 시도가 발생하면 처리되지 않은 프로미스 거부가 발생할 수 있다. `process.stdin.on('error', ...)` 핸들러로 이를 격리한다. + +### 3.6 주요 LSP 작업 + +`LSPTool`이 지원하는 작업과 각각의 LSP 메서드: + +| 작업 (`operation`) | LSP 메서드 | 설명 | +|-------------------|-----------|------| +| `goToDefinition` | `textDocument/definition` | 심볼 정의 위치로 이동 | +| `findReferences` | `textDocument/references` | 심볼 참조 위치 목록 | +| `hover` | `textDocument/hover` | 커서 위치 심볼 정보 | +| `documentSymbol` | `textDocument/documentSymbol` | 파일 내 심볼 트리 | +| `workspaceSymbol` | `workspace/symbol` | 워크스페이스 전체 심볼 검색 | +| `goToImplementation` | `textDocument/implementation` | 인터페이스 구현체로 이동 | +| `prepareCallHierarchy` | `textDocument/prepareCallHierarchy` | 호출 계층 준비 | +| `incomingCalls` | `callHierarchy/incomingCalls` | 호출자 목록 | +| `outgoingCalls` | `callHierarchy/outgoingCalls` | 피호출자 목록 | + +입력 좌표는 **1-based** (에디터 표시 기준)이며, LSP로 전달할 때 0-based로 변환한다. + +**파일 크기 제한**: 10MB(`MAX_LSP_FILE_SIZE_BYTES`)를 초과하는 파일은 처리를 거부한다. + +**보안**: UNC 경로(`\\` 또는 `//`로 시작)는 NTLM 자격증명 누수를 방지하기 위해 파일시스템 작업 없이 검증을 통과시킨다. + +### 3.7 진단 레지스트리 (`LSPDiagnosticRegistry.ts`) + +LSP 서버는 `textDocument/publishDiagnostics` 알림을 비동기적으로 전송한다. `LSPDiagnosticRegistry`는 이를 수신하여 다음 대화 턴에 첨부파일(Attachment)로 전달한다. + +``` +publishDiagnostics 수신 + → registerPendingLSPDiagnostic() (UUID 키로 저장) + → checkForLSPDiagnostics() (다음 쿼리 시 조회) + → getLSPDiagnosticAttachments() → Attachment[] + → getAttachments() → 대화에 자동 주입 +``` + +**볼륨 제한**: +- 파일당 최대 10개 진단 +- 전체 최대 30개 진단 +- 심각도 순(Error > Warning > Info > Hint)으로 정렬하여 상위 항목 우선 전달 + +**교차 턴 중복 제거**: `deliveredDiagnostics` LRU 캐시(최대 500개 파일)에 이미 전달한 진단의 해시(메시지 + 심각도 + 범위 + 소스 + 코드)를 저장한다. 동일한 진단이 여러 턴에 걸쳐 반복 전달되지 않도록 방지한다. + +--- + +## 4. MCPTool / LSPTool 구현 분석 + +### 4.1 MCPTool + +`MCPTool`(`src/tools/MCPTool/MCPTool.ts`)은 모든 MCP 도구의 **정적 템플릿**이다. 실제 MCP 서버 도구들은 이 템플릿을 기반으로 `mcpClient.ts`에서 동적으로 생성된다. + +```typescript +export const MCPTool = buildTool({ + isMcp: true, + name: 'mcp', // mcpClient.ts에서 실제 이름으로 오버라이드 + maxResultSizeChars: 100_000, + inputSchema: z.object({}).passthrough(), // MCP 서버가 스키마 정의 + async call() { return { data: '' } }, // mcpClient.ts에서 오버라이드 + async checkPermissions() { + return { behavior: 'passthrough', message: '...' } + }, +}) +``` + +주요 특징: +- `isMcp: true` 플래그로 MCP 도구임을 표시 +- `inputSchema`는 `passthrough()`를 사용하여 MCP 서버가 정의하는 임의의 입력 구조를 허용 +- 권한 검사는 항상 `passthrough` — MCP 도구의 권한은 서버 수준에서 관리됨 +- `isOpenWorld(): false` — 알려진 도구 집합 내에서만 동작 +- `classifyForCollapse` 및 `renderToolUseProgressMessage`를 통해 UI에 진행 상태 표시 지원 + +### 4.2 LSPTool + +`LSPTool`(`src/tools/LSPTool/LSPTool.ts`)은 LSP 기능을 Claude의 도구 인터페이스로 노출하는 **단일 도구**다. 여러 LSP 작업을 하나의 도구로 통합하여 입력 `operation` 필드로 구분한다. + +```typescript +export const LSPTool = buildTool({ + name: LSP_TOOL_NAME, + isLsp: true, + shouldDefer: true, // 초기화 완료 대기 + isEnabled() { return isLspConnected() }, // 동적 활성화 상태 + isConcurrencySafe() { return true }, // 병렬 실행 안전 + isReadOnly() { return true }, // 파일 수정 없음 +}) +``` + +**`shouldDefer: true`**: 도구 실행 전 LSP 초기화 완료를 기다린다. `waitForInitialization()`을 호출하여 `pending` 상태가 해소될 때까지 블로킹한다. + +**입력 스키마 이중 검증**: `z.strictObject()`로 1차 검증 후, `lspToolInputSchema()` 판별 유니온으로 2차 검증하여 더 정확한 에러 메시지를 제공한다. 결과 포맷팅은 `formatters.ts`의 작업별 함수(`formatHoverResult`, `formatGoToDefinitionResult` 등)가 담당한다. + +**`symbolContext.ts`**: 심볼 컨텍스트를 보강하는 유틸리티. 정의 위치 주변 코드를 포함하여 Claude가 더 풍부한 컨텍스트를 얻을 수 있도록 한다. + +--- + +## 5. 설계 결정 + +### 5.1 MCP 도구의 동적 생성 vs. 정적 정의 + +MCP 도구는 서버에 연결하기 전까지 스키마를 알 수 없다. `MCPTool`이 정적 템플릿이고 실제 도구가 런타임에 생성되는 이유다. `buildTool()`이 반환하는 `ToolDef`를 기반으로, `mcpClient.ts`는 각 서버의 `tools/list` 응답을 받아 도구 인스턴스를 동적으로 구성한다. + +### 5.2 LSP가 플러그인 전용인 이유 + +LSP 서버는 언어별 바이너리(`typescript-language-server`, `rust-analyzer` 등)를 실행한다. 이 바이너리들은 사용자 환경에 설치되어 있어야 하며, 플러그인이 이 의존성 확인과 경로 설정을 캡슐화하기에 적합하다. 직접 설정을 허용하면 잘못된 설정으로 인한 크래시 루프 가능성이 높아진다. + +### 5.3 연결 캐시와 세션 만료 + +`connectToServer()`의 `memoize`는 동일 설정에 대해 하나의 연결만 유지한다. Streamable HTTP 서버의 세션 만료(HTTP 404 + JSON-RPC `-32001`)가 감지되면 캐시를 무효화하고 재연결한다. 일반 HTTP 404(잘못된 URL, 서버 다운)와 구분하기 위해 응답 본문의 JSON-RPC 에러 코드를 추가로 확인한다. + +### 5.4 인증 상태의 캐싱 + +OAuth 인증 필요 상태를 15분간 캐싱하는 이유: 인증이 필요한 서버에 매 요청마다 재연결을 시도하면 30+ 커넥터가 동시에 401을 반환하여 인증 루프에 빠질 수 있다. 캐시는 이 폭발적 재시도를 완충한다. + +### 5.5 Bun 런타임 특수 처리 + +일부 API(`WebSocket`, `AbortSignal.timeout`)는 Bun과 Node.js 간 동작이 다르다. `typeof Bun !== 'undefined'` 조건부 분기로 런타임을 감지하여 각각에 최적화된 구현을 선택한다. + +### 5.6 도구 결과 크기 제어 + +MCP 도구 결과의 최대 크기는 `maxResultSizeChars: 100_000`이다. 이를 초과하는 결과는 `truncateMcpContentIfNeeded()`로 잘라내거나 `persistBinaryContent()`로 파일 시스템에 저장한다. 이진 데이터(이미지 등)는 base64 인코딩 크기를 추정하여 별도 처리한다. + +--- + +## Navigation + +**상위 레벨** +- [Level 2: Tool System](../level-2-systems/tool-system.md) — MCPTool과 LSPTool이 통합되는 도구 레지스트리 전체 설명 +- [Level 2: Agent Coordinator](../level-2-systems/agent-coordinator.md) — MCP 서버 초기화 타이밍과 에이전트 루프의 관계 + +**동급 문서 (Level 3)** +- [Query Engine](query-engine.md) — 도구 호출 결과가 모델 입력으로 변환되는 과정 +- [Permission System](permission-system.md) — MCP 도구 권한 결정 흐름 + +**핵심 소스 파일** + +| 파일 | 역할 | +|------|------| +| `src/services/mcp/client.ts` | MCP 연결 관리, Transport 선택, 도구 동적 생성 | +| `src/services/mcp/types.ts` | MCP 설정 Zod 스키마 및 연결 상태 타입 | +| `src/services/mcp/config.ts` | 스코프별 설정 병합, `.mcp.json` 읽기/쓰기 | +| `src/services/mcp/auth.ts` | OAuth PKCE 플로우, XAA 토큰 교환 | +| `src/services/mcp/normalization.ts` | 도구 이름 정규화 | +| `src/services/lsp/manager.ts` | LSP 싱글턴 생명주기 | +| `src/services/lsp/LSPServerManager.ts` | 파일 확장자 기반 서버 라우팅 | +| `src/services/lsp/LSPServerInstance.ts` | 서버 상태 머신, 크래시 복구 | +| `src/services/lsp/LSPClient.ts` | vscode-jsonrpc stdio 연결 | +| `src/services/lsp/LSPDiagnosticRegistry.ts` | 진단 수신 및 중복 제거 | +| `src/tools/MCPTool/MCPTool.ts` | MCP 도구 정적 템플릿 | +| `src/tools/LSPTool/LSPTool.ts` | LSP 작업 통합 도구 | diff --git a/docs/ko/level-3-internals/oauth-auth.md b/docs/ko/level-3-internals/oauth-auth.md new file mode 100644 index 0000000..df80930 --- /dev/null +++ b/docs/ko/level-3-internals/oauth-auth.md @@ -0,0 +1,391 @@ +# 인증 흐름 분석: OAuth 2.0, macOS Keychain, JWT 검증 + +> **레벨**: 내부 구현 (Level 3) +> **대상 독자**: Claude Code 핵심 기여자, 인증 통합 개발자 +> **관련 소스**: `src/services/oauth/` + +--- + +## 개요 + +Claude Code의 인증 시스템은 OAuth 2.0 Authorization Code Flow with PKCE(Proof Key for Code Exchange)를 기반으로 구현되어 있다. 단순한 OAuth 클라이언트 구현을 넘어, 두 가지 인증 경로(자동/수동), 토큰 갱신 최적화, 프로필 캐싱, 그리고 비-브라우저 환경 지원을 통합한다. + +핵심 클래스인 `OAuthService`는 상태 기계(state machine)처럼 동작한다: `codeVerifier`와 `authCodeListener`를 인스턴스 내부에 캡슐화하여, 플로우의 각 단계가 올바른 순서로 실행되도록 보장한다. 외부 저장소(macOS Keychain)와의 연동은 `src/utils/auth.ts`의 상위 계층에서 처리된다. + +--- + +## 아키텍처 다이어그램 + +### OAuth 2.0 PKCE 플로우 + +```mermaid +sequenceDiagram + participant User as 사용자 + participant CLI as OAuthService + participant ACL as AuthCodeListener\n(localhost HTTP 서버) + participant Browser as 브라우저 + participant Auth as Anthropic 인증 서버 + participant Token as Token 엔드포인트 + participant Profile as Profile API + + CLI->>CLI: generateCodeVerifier() — 32바이트 무작위 + CLI->>CLI: generateCodeChallenge() — SHA-256(verifier) → base64url + CLI->>CLI: generateState() — 32바이트 무작위 (CSRF 방지) + CLI->>ACL: authCodeListener.start() — OS 할당 포트 + ACL-->>CLI: port 번호 + + CLI->>Browser: openBrowser(automaticFlowUrl)\n→ ?client_id=...&code_challenge=...&state=... + CLI->>User: manualFlowUrl 표시 (수동 대안) + + Browser->>Auth: GET /authorize?... + Auth->>User: 로그인 페이지 + User->>Auth: 자격증명 입력 + Auth->>Browser: 302 → http://localhost:{port}/callback?code=AUTH_CODE&state=STATE + Browser->>ACL: GET /callback?code=AUTH_CODE&state=STATE + + ACL->>ACL: state 검증 (CSRF 방지) + ACL-->>CLI: authorizationCode (Promise resolve) + + CLI->>Token: POST /oauth/token\n{grant_type: authorization_code, code, code_verifier, ...} + Token-->>CLI: {access_token, refresh_token, expires_in, scope} + CLI->>Profile: GET /api/oauth/profile\n(Authorization: Bearer access_token) + Profile-->>CLI: {organization_type, rate_limit_tier, ...} + + ACL->>Browser: 302 → CLAUDEAI_SUCCESS_URL (성공 리다이렉트) + CLI-->>User: OAuthTokens 반환 +``` + +### 인증 컴포넌트 의존 관계 + +```mermaid +graph TB + subgraph "OAuth 서비스 계층" + OS["OAuthService\n(index.ts)"] + ACL["AuthCodeListener\n(auth-code-listener.ts)"] + CL["OAuth Client\n(client.ts)"] + CR["Crypto Utils\n(crypto.ts)"] + GP["getOauthProfile\n(getOauthProfile.ts)"] + end + + subgraph "설정" + OC["getOauthConfig()\n(constants/oauth.ts)"] + AC["ALL_OAUTH_SCOPES\nCLAUDE_AI_INFERENCE_SCOPE"] + end + + subgraph "저장소" + GC["getGlobalConfig()\n전역 설정 (JSON 파일)"] + KCH["macOS Keychain\n(utils/auth.ts)"] + SS["Secure Storage\nOAuthTokens 영속화"] + end + + subgraph "토큰 갱신" + CAN["checkAndRefreshOAuthTokenIfNeeded()\n(utils/auth.ts)"] + ROAT["refreshOAuthToken()\n(client.ts)"] + end + + OS --> ACL + OS --> CL + OS --> CR + CL --> GP + CL --> OC + CL --> AC + OS --> KCH + ROAT --> GC + ROAT --> SS + CAN --> ROAT + + style OS fill:#2d6a9f,color:#fff + style ACL fill:#1e7a4f,color:#fff + style KCH fill:#7a2020,color:#fff +``` + +--- + +## 핵심 구현 분석 + +### 1. PKCE 암호 기법 (`crypto.ts`) + +PKCE는 인증 코드 탈취 공격을 방지하기 위한 RFC 7636 표준 확장이다. + +```typescript +// 코드 검증자: 32바이트 무작위 → base64url +export function generateCodeVerifier(): string { + return base64URLEncode(randomBytes(32)) // Node.js crypto 모듈 +} + +// 코드 챌린지: SHA-256(verifier) → base64url +export function generateCodeChallenge(verifier: string): string { + const hash = createHash('sha256') + hash.update(verifier) + return base64URLEncode(hash.digest()) +} + +// CSRF 방지 상태값: 32바이트 무작위 +export function generateState(): string { + return base64URLEncode(randomBytes(32)) +} + +// base64url: 표준 base64에서 +→-, /→_, = 제거 +function base64URLEncode(buffer: Buffer): string { + return buffer.toString('base64') + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') +} +``` + +`code_challenge_method: 'S256'`을 사용하여 검증자의 SHA-256 해시를 챌린지로 전송한다. 인증 서버는 토큰 교환 시 수신한 `code_verifier`를 해싱하여 챌린지와 일치하는지 검증한다. + +### 2. 로컬 콜백 서버 (`auth-code-listener.ts`) + +`AuthCodeListener`는 OAuth 리다이렉트를 포착하기 위한 임시 HTTP 서버다. OS 할당 포트를 사용하여 포트 충돌을 방지한다. + +```typescript +// 포트 0으로 리슨 → OS가 사용 가능한 포트 자동 할당 +this.localServer.listen(port ?? 0, 'localhost', () => { + const address = this.localServer.address() as AddressInfo + this.port = address.port + resolve(this.port) +}) +``` + +**상태 검증**: 콜백 수신 시 `state` 파라미터를 초기 생성값과 비교하여 CSRF 공격을 방지한다. + +```typescript +if (state !== this.expectedState) { + res.writeHead(400) + res.end('Invalid state parameter') + this.reject(new Error('Invalid state parameter')) + return +} +``` + +**지연 리다이렉트 패턴**: 인증 코드 포착 시 브라우저 응답(`ServerResponse`)을 즉시 종료하지 않고 `pendingResponse`에 보관한다. 토큰 교환 성공 후 `handleSuccessRedirect()`에서 최종 리다이렉트를 수행한다. 이 패턴은 사용자가 성공 페이지를 보기 전에 브라우저 연결이 끊기는 경쟁 조건을 방지한다. + +```typescript +// 인증 코드 포착 → 응답 보류 +this.pendingResponse = res +this.resolve(authCode) + +// 나중에 토큰 교환 성공 후 +handleSuccessRedirect(scopes: string[]): void { + const successUrl = shouldUseClaudeAIAuth(scopes) + ? getOauthConfig().CLAUDEAI_SUCCESS_URL + : getOauthConfig().CONSOLE_SUCCESS_URL + this.pendingResponse.writeHead(302, { Location: successUrl }) + this.pendingResponse.end() + this.pendingResponse = null +} +``` + +### 3. 이중 플로우 지원 (`OAuthService.startOAuthFlow()`) + +자동 플로우와 수동 플로우가 동시에 준비되어 경쟁한다: + +```typescript +// 두 URL을 각각 생성 +const manualFlowUrl = client.buildAuthUrl({ ...opts, isManual: true }) +const automaticFlowUrl = client.buildAuthUrl({ ...opts, isManual: false }) + +// 자동: 브라우저를 자동으로 열기 + 콜백 리스너 대기 +// 수동: 사용자가 URL을 복사해 붙여넣기 +await authURLHandler(manualFlowUrl) // 수동 옵션 표시 +await openBrowser(automaticFlowUrl) // 자동 플로우 시도 +``` + +`skipBrowserOpen` 옵션은 SDK 제어 프로토콜(`claude_authenticate`)에서 사용된다. 이 경우 클라이언트가 표시 화면을 제어하므로 OAuthService가 직접 브라우저를 열지 않는다. + +어느 플로우가 먼저 완료되든 `waitForAuthorizationCode()`의 Promise가 resolve된다. `isAutomaticFlow`는 `hasPendingResponse()`로 판별하며, 이에 따라 토큰 교환의 `redirect_uri` 파라미터가 결정된다. + +### 4. URL 빌더 및 스코프 관리 (`client.ts`) + +```typescript +export function buildAuthUrl({ codeChallenge, state, port, isManual, ... }): string { + const authUrlBase = loginWithClaudeAi + ? getOauthConfig().CLAUDE_AI_AUTHORIZE_URL + : getOauthConfig().CONSOLE_AUTHORIZE_URL + + authUrl.searchParams.append('code', 'true') // Claude Max 업셀 표시 트리거 + authUrl.searchParams.append('client_id', getOauthConfig().CLIENT_ID) + authUrl.searchParams.append('response_type', 'code') + authUrl.searchParams.append( + 'redirect_uri', + isManual + ? getOauthConfig().MANUAL_REDIRECT_URL // 수동: 고정 URL + : `http://localhost:${port}/callback`, // 자동: 로컬 콜백 + ) + // inferenceOnly: 장기 추론 전용 토큰 (스코프 최소화) + const scopesToUse = inferenceOnly ? [CLAUDE_AI_INFERENCE_SCOPE] : ALL_OAUTH_SCOPES + authUrl.searchParams.append('scope', scopesToUse.join(' ')) + authUrl.searchParams.append('code_challenge', codeChallenge) + authUrl.searchParams.append('code_challenge_method', 'S256') + authUrl.searchParams.append('state', state) +} +``` + +`CLAUDE_AI_INFERENCE_SCOPE`는 추론 전용 장기 토큰에 사용되는 최소 스코프다. 이 스코프로 발급된 토큰은 프로필 조회 등 다른 기능을 위해 추가 스코프 확장이 필요할 수 있다. + +### 5. 토큰 교환 (`exchangeCodeForTokens()`) + +```typescript +const requestBody = { + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: useManualRedirect + ? getOauthConfig().MANUAL_REDIRECT_URL + : `http://localhost:${port}/callback`, + client_id: getOauthConfig().CLIENT_ID, + code_verifier: codeVerifier, // PKCE 검증자 + state, + ...(expiresIn !== undefined && { expires_in: expiresIn }), +} + +const response = await axios.post(getOauthConfig().TOKEN_URL, requestBody, { + headers: { 'Content-Type': 'application/json' }, + timeout: 15000, // 15초 타임아웃 +}) +``` + +`redirect_uri`는 인증 요청에서 사용한 값과 정확히 일치해야 한다. 자동/수동 플로우에 따라 이 값이 달라지므로, `isManual` 플래그를 `exchangeCodeForTokens()`에 전달하여 올바른 URI를 사용한다. + +### 6. 토큰 갱신 최적화 (`refreshOAuthToken()`) + +갱신 시 불필요한 프로필 API 호출을 회피하는 캐시 활용 로직이 핵심이다: + +```typescript +// 이미 캐시된 프로필 정보가 있으면 API 호출 생략 +const haveProfileAlready = + config.oauthAccount?.billingType !== undefined && + config.oauthAccount?.accountCreatedAt !== undefined && + config.oauthAccount?.subscriptionCreatedAt !== undefined && + existing?.subscriptionType != null && + existing?.rateLimitTier != null + +const profileInfo = haveProfileAlready ? null : await fetchProfileInfo(accessToken) +``` + +이 최적화는 하루 약 7백만 건의 불필요한 `/api/oauth/profile` 요청을 절감하는 효과가 있다. 단, `CLAUDE_CODE_OAUTH_REFRESH_TOKEN` 재로그인 경로에서는 `installOAuthTokens()`가 `performLogout()`을 실행하여 시큐어 스토리지를 초기화하기 때문에, `existing` 값이 `null`일 수 있다. 이 경우 캐시된 값을 우선 사용(`profileInfo?.subscriptionType ?? existing?.subscriptionType ?? null`)하여 가입 유형 정보 손실을 방지한다. + +스코프 확장도 지원한다: +```typescript +scope: (requestedScopes?.length ? requestedScopes : CLAUDE_AI_OAUTH_SCOPES).join(' ') +``` + +백엔드의 refresh-token 그랜트는 `ALLOWED_SCOPE_EXPANSIONS`에 정의된 범위 내에서 초기 인증 시 부여된 스코프를 초과하는 스코프 요청을 허용한다. + +### 7. 프로필 정보 조회 및 구독 유형 분류 (`fetchProfileInfo()`) + +```typescript +export async function fetchProfileInfo(accessToken: string) { + const profile = await getOauthProfileFromOauthToken(accessToken) + const orgType = profile?.organization?.organization_type + + // 조직 유형 → 구독 유형 매핑 + switch (orgType) { + case 'claude_max': subscriptionType = 'max'; break + case 'claude_pro': subscriptionType = 'pro'; break + case 'claude_enterprise': subscriptionType = 'enterprise'; break + case 'claude_team': subscriptionType = 'team'; break + default: subscriptionType = null; break + } + + return { + subscriptionType, + rateLimitTier: profile?.organization?.rate_limit_tier ?? null, + hasExtraUsageEnabled: profile?.organization?.has_extra_usage_enabled ?? null, + billingType: profile?.organization?.billing_type ?? null, + displayName: profile?.account?.display_name, + accountCreatedAt: profile?.account?.created_at, + subscriptionCreatedAt: profile?.organization?.subscription_created_at, + rawProfile: profile, + } +} +``` + +`rawProfile`을 포함하여 반환하는 이유는 상위 계층에서 추가 필드에 접근할 수 있도록 하기 위함이다. + +### 8. 토큰 만료 검사 (`isOAuthTokenExpired()`) + +```typescript +export function isOAuthTokenExpired(expiresAt: number | null): boolean { + if (expiresAt === null) return false // 만료 정보 없으면 유효로 처리 + + const bufferTime = 5 * 60 * 1000 // 5분 버퍼 + const expiresWithBuffer = Date.now() + bufferTime + return expiresWithBuffer >= expiresAt +} +``` + +만료 5분 전을 만료로 처리하는 버퍼 설계는 `jwtUtils.ts`의 `TOKEN_REFRESH_BUFFER_MS`와 동일한 값을 사용하여 일관성을 유지한다. + +### 9. 환경변수 폴백 메커니즘 (`populateOAuthAccountInfoIfNeeded()`) + +SDK 통합 시나리오(예: Cowork)에서는 환경변수로 계정 정보를 주입할 수 있다: + +```typescript +const envAccountUuid = process.env.CLAUDE_CODE_ACCOUNT_UUID +const envUserEmail = process.env.CLAUDE_CODE_USER_EMAIL +const envOrganizationUuid = process.env.CLAUDE_CODE_ORGANIZATION_UUID + +if (envAccountUuid && envUserEmail && envOrganizationUuid) { + storeOAuthAccountInfo({ accountUuid: envAccountUuid, emailAddress: envUserEmail, ... }) +} +``` + +환경변수 설정 후에도 실제 프로필 API 조회가 성공하면 API 응답이 환경변수 값을 덮어쓴다. 초기 텔레메트리 이벤트에서 계정 정보가 누락되는 경쟁 조건을 해결하기 위한 설계다. + +--- + +## 설계 결정 + +### PKCE 의무화 + +모든 OAuth 플로우에서 PKCE가 필수다. 클라이언트 시크릿을 사용하지 않는 공개 클라이언트(CLI 도구)에서 인증 코드 가로채기 공격을 방지하기 위함이다. `code_challenge_method: 'S256'` (SHA-256)이 평문 방식(`plain`)보다 항상 선호된다. + +### 이중 플로우 동시 경쟁 + +자동 플로우(브라우저 리다이렉트)와 수동 플로우(코드 붙여넣기)가 동시에 준비되어 먼저 완료되는 쪽이 채택된다. 헤드리스 서버 환경에서 브라우저를 열 수 없는 경우에도 수동 플로우로 인증이 가능하다. `skipBrowserOpen` 옵션은 이 패턴을 SDK 호출자가 완전히 제어할 수 있도록 확장한 것이다. + +### 조직 UUID 조회 전략 + +`getOrganizationUUID()`는 네트워크 왕복을 최소화하기 위해 전역 설정을 우선 확인한다: + +```typescript +// 1순위: 전역 설정 캐시 +const orgUUID = globalConfig.oauthAccount?.organizationUuid +if (orgUUID) return orgUUID + +// 2순위: 프로필 API (user:profile 스코프 필요) +const profile = await getOauthProfileFromOauthToken(accessToken) +``` + +`user:profile` 스코프가 없는 토큰은 프로필 API를 호출할 수 없으므로 `hasProfileScope()` 검사 후 폴백한다. + +### 역할 정보 별도 API (`fetchAndStoreUserRoles()`) + +`/roles` 엔드포인트에서 조직 역할(`organization_role`), 워크스페이스 역할(`workspace_role`), 조직명을 가져와 전역 설정에 저장한다. 프로필 API와 별개의 엔드포인트를 사용하는 이유는 역할 정보가 자주 변경될 수 있어 선택적으로 새로고침이 필요하기 때문이다. + +### API 키 프로비저닝 (`createAndStoreApiKey()`) + +OAuth 플로우 완료 후 자동으로 API 키를 생성하여 Keychain에 저장하는 옵션을 제공한다. 이는 OAuth 토큰 없이 직접 API 키로 인증하는 시나리오를 지원하며, 특히 자동화 환경에서 유용하다. + +--- + +## 관련 파일 참조 + +| 파일 | 역할 | +|------|------| +| `src/services/oauth/index.ts` | `OAuthService` 메인 클래스 | +| `src/services/oauth/auth-code-listener.ts` | 로컬 OAuth 콜백 HTTP 서버 | +| `src/services/oauth/client.ts` | URL 빌더, 토큰 교환, 갱신, 프로필 조회 | +| `src/services/oauth/crypto.ts` | PKCE 암호 유틸리티 | +| `src/services/oauth/getOauthProfile.ts` | 프로필 API 호출 | +| `src/constants/oauth.ts` | OAuth 설정 (엔드포인트 URL, 클라이언트 ID, 스코프) | +| `src/utils/auth.ts` | Keychain 연동, 토큰 저장/조회, 갱신 조율 | +| `src/bridge/jwtUtils.ts` | JWT 디코딩, 브릿지 세션 토큰 갱신 스케줄러 | + +--- + +## 탐색 링크 + +- [IDE 브릿지 분석](./bridge-ide.md) +- [컨텍스트 압축 & 토큰 관리](./context-compression.md) +- [Level 2: 아키텍처 개요](../level-2-architecture/) diff --git a/docs/ko/level-3-internals/plugin-skill-system.md b/docs/ko/level-3-internals/plugin-skill-system.md new file mode 100644 index 0000000..d85003a --- /dev/null +++ b/docs/ko/level-3-internals/plugin-skill-system.md @@ -0,0 +1,530 @@ +# 플러그인 & 스킬 확장 시스템 분석 + +## 1. 개요 + +Claude Code의 확장 시스템은 두 개의 독립적이지만 상호 연관된 레이어로 구성된다: **플러그인 시스템(Plugin System)**과 **스킬 시스템(Skill System)**. 두 시스템 모두 CLI 바이너리 외부에서 기능을 추가하거나 내장 기능을 사용자가 제어할 수 있도록 설계된 확장 메커니즘이다. + +**시스템 간 주요 차이점:** + +| 구분 | 플러그인 시스템 | 스킬 시스템 | +|------|---------------|------------| +| 사용자 제어 | `/plugin` UI에서 활성화/비활성화 가능 | 항상 활성화(번들) 또는 디렉토리 기반 로딩 | +| 진입점 | `builtinPlugins.ts` 레지스트리 | `bundledSkills.ts` 레지스트리 또는 `loadSkillsDir.ts` | +| 식별자 형식 | `{name}@builtin` (내장) / `{name}@{marketplace}` (마켓플레이스) | 스킬 이름 (예: `commit`, `review-pr`) | +| 구성 요소 | 스킬, 훅(hooks), MCP 서버를 복합 제공 가능 | 단일 스킬 단위 | +| 현재 상태 | 스캐폴딩(scaffolding) 단계 — 등록된 내장 플러그인 없음 | 10개 이상의 번들 스킬 제공 | + +**파일 구조 요약:** + +``` +src/plugins/ + builtinPlugins.ts — 내장 플러그인 레지스트리 및 쿼리 함수 + bundled/ + index.ts — initBuiltinPlugins() 진입점 (현재 빈 스캐폴딩) + +src/skills/ + bundledSkills.ts — 번들 스킬 레지스트리, 파일 추출 로직 + loadSkillsDir.ts — 디스크 기반 스킬 로딩, 프런트매터 파싱 + mcpSkillBuilders.ts — MCP 스킬 빌더 싱글턴 레지스트리 + bundled/ + index.ts — initBundledSkills() 진입점 + remember.ts, simplify.ts, verify.ts, ... — 개별 스킬 구현체 + +src/tools/SkillTool/ + SkillTool.ts — 스킬 실행 도구 본체 + constants.ts — SKILL_TOOL_NAME 상수 + prompt.ts — 스킬 목록 프롬프트 생성 및 예산 관리 + UI.tsx — 렌더링 컴포넌트 +``` + +--- + +## 2. 플러그인 시스템 + +### 2.1 플러그인 로딩 메커니즘 + +플러그인 시스템은 시작 시점에 `initBuiltinPlugins()`를 호출하여 초기화된다. 이 함수는 `src/plugins/bundled/index.ts`에 위치하며, 각 플러그인은 `registerBuiltinPlugin()`을 통해 중앙 `Map` 레지스트리에 등록된다. + +```typescript +// builtinPlugins.ts +const BUILTIN_PLUGINS: Map = new Map() + +export function registerBuiltinPlugin( + definition: BuiltinPluginDefinition, +): void { + BUILTIN_PLUGINS.set(definition.name, definition) +} +``` + +플러그인 활성화 여부는 다음 우선순위로 결정된다: + +1. **사용자 설정** (`settings.enabledPlugins[pluginId]`): 명시적으로 `true`/`false` 지정 시 최우선 적용 +2. **플러그인 기본값** (`definition.defaultEnabled`): 사용자 설정이 없을 때 플러그인이 선언한 기본값 +3. **시스템 기본값** (`true`): 위 두 값이 모두 없을 때 활성화 상태가 기본 + +플러그인 가용성 필터링은 `isAvailable()` 콜백으로 처리된다. 이 콜백이 `false`를 반환하면 해당 플러그인은 활성화/비활성화 목록 모두에서 제외된다: + +```typescript +for (const [name, definition] of BUILTIN_PLUGINS) { + if (definition.isAvailable && !definition.isAvailable()) { + continue // 조건 미충족 시 목록에서 완전히 제외 + } + // ... +} +``` + +### 2.2 번들 플러그인 구조 + +`src/plugins/bundled/index.ts`는 현재 빈 스캐폴딩 상태다. 주석에 따르면 이 구조는 기존 번들 스킬 중 사용자 토글이 필요한 것들을 마이그레이션하기 위한 준비 단계다: + +```typescript +// bundled/index.ts +export function initBuiltinPlugins(): void { + // No built-in plugins registered yet — this is the scaffolding for + // migrating bundled skills that should be user-toggleable. +} +``` + +새 내장 플러그인 추가 절차는 주석에 명시되어 있다: +1. `builtinPlugins.ts`에서 `registerBuiltinPlugin` 임포트 +2. `initBuiltinPlugins()` 내에서 `registerBuiltinPlugin()` 호출 + +### 2.3 플러그인 확장 포인트 + +`BuiltinPluginDefinition` 타입은 플러그인이 제공할 수 있는 세 가지 확장 포인트를 정의한다: + +| 확장 포인트 | 타입 | 설명 | +|------------|------|------| +| `skills` | `BundledSkillDefinition[]` | 스킬 목록 (플러그인이 비활성화되면 스킬도 비활성화) | +| `hooks` | `HooksConfig` | 전처리/후처리 훅 설정 | +| `mcpServers` | `MCPServerConfig[]` | MCP 서버 자동 연결 설정 | + +플러그인에서 제공된 스킬은 `skillDefinitionToCommand()` 함수를 통해 `Command` 객체로 변환된다. 이 변환 과정에서 `source: 'bundled'`로 설정되는 것이 중요한 설계 결정이다: + +```typescript +// builtinPlugins.ts - skillDefinitionToCommand() +return { + // ... + // 'bundled' not 'builtin' — 'builtin' in Command.source means hardcoded + // slash commands (/help, /clear). Using 'bundled' keeps these skills in + // the Skill tool's listing, analytics name logging, and prompt-truncation + // exemption. The user-toggleable aspect is tracked on LoadedPlugin.isBuiltin. + source: 'bundled', + loadedFrom: 'bundled', +} +``` + +이 설계를 통해 플러그인 스킬은 스킬 목록에서 번들 스킬과 동일하게 취급되며, 프롬프트 예산 우선 배정과 분석 이름 로깅 대상에 포함된다. + +### 2.4 LoadedPlugin 구조체 + +`getBuiltinPlugins()`가 반환하는 `LoadedPlugin` 객체는 다음 주요 필드를 포함한다: + +```typescript +const plugin: LoadedPlugin = { + name, + manifest: { name, description, version }, + path: BUILTIN_MARKETPLACE_NAME, // 'builtin' — 파일시스템 경로 없음 + source: pluginId, // '{name}@builtin' + repository: pluginId, + enabled: isEnabled, + isBuiltin: true, + hooksConfig: definition.hooks, + mcpServers: definition.mcpServers, +} +``` + +`path: 'builtin'`은 센티넬(sentinel) 값으로, 파일시스템 경로가 없는 내장 플러그인임을 나타낸다. + +--- + +## 3. 스킬 시스템 + +### 3.1 스킬 로딩 메커니즘 + +스킬 로딩은 두 가지 경로로 진행된다: + +**경로 1 — 번들 스킬 (programmatic registration):** +시작 시 `initBundledSkills()`가 호출되어 각 번들 스킬의 `register*()` 함수를 순차 호출한다. 각 `register*()` 함수는 `registerBundledSkill(definition)`을 호출하여 모듈 수준의 `bundledSkills: Command[]` 배열에 추가한다. + +**경로 2 — 디스크 기반 스킬 (file-system loading):** +`loadSkillsDir.ts`의 `loadSkillsFromSkillsDir()`가 다음 경로들을 순회하여 스킬을 로드한다: + +| 소스 | 경로 | +|------|------| +| `policySettings` | `{managedFilePath}/.claude/skills/` | +| `userSettings` | `{claudeConfigHomeDir}/skills/` | +| `projectSettings` | `.claude/skills/` | +| `plugin` | 플러그인 디렉토리 | + +파일 기반 스킬은 `{skill-name}/SKILL.md` 형식의 디렉토리 구조를 따른다. 단일 `.md` 파일은 `/skills/` 디렉토리에서 지원되지 않는다. + +`getSkillsPath()` 함수는 소스별 경로 해석을 담당한다: + +```typescript +export function getSkillsPath( + source: SettingSource | 'plugin', + dir: 'skills' | 'commands', +): string { + switch (source) { + case 'policySettings': return join(getManagedFilePath(), '.claude', dir) + case 'userSettings': return join(getClaudeConfigHomeDir(), dir) + case 'projectSettings': return `.claude/${dir}` + case 'plugin': return 'plugin' + default: return '' + } +} +``` + +### 3.2 번들 스킬 목록 + +`src/skills/bundled/index.ts`의 `initBundledSkills()`는 다음 스킬들을 등록한다: + +| 스킬 | 파일 | 특이사항 | +|------|------|---------| +| `update-config` | `updateConfig.ts` | 설정 파일 업데이트 | +| `keybindings-help` | `keybindings.ts` | 키 바인딩 설명 | +| `verify` | `verify.ts` | 코드 검증 | +| `debug` | `debug.ts` | 디버깅 보조 | +| `lorem-ipsum` | `loremIpsum.ts` | 더미 텍스트 생성 | +| `skillify` | `skillify.ts` | 스킬 자동 생성 | +| `remember` | `remember.ts` | 메모리 레이어 검토 | +| `simplify` | `simplify.ts` | 코드 단순화 | +| `batch` | `batch.ts` | 배치 작업 | +| `stuck` | `stuck.ts` | 막힌 상황 탈출 보조 | + +기능 플래그(feature flag)로 제어되는 조건부 스킬도 존재한다: + +```typescript +if (feature('KAIROS') || feature('KAIROS_DREAM')) { + const { registerDreamSkill } = require('./dream.js') + registerDreamSkill() +} +if (feature('AGENT_TRIGGERS')) { + const { registerLoopSkill } = require('./loop.js') + registerLoopSkill() +} +if (feature('BUILDING_CLAUDE_APPS')) { + const { registerClaudeApiSkill } = require('./claudeApi.js') + registerClaudeApiSkill() +} +``` + +조건부 스킬에 `require()`를 사용하는 이유는 정적 임포트(static import)를 사용할 경우 트리 셰이킹(tree-shaking)을 통과하는 사이드 이펙트(side-effecting initializer)가 Bun 번들 바이너리에 포함되어 불필요한 모듈 초기화가 발생하기 때문이다. + +### 3.3 번들 스킬의 파일 추출 메커니즘 + +`BundledSkillDefinition`의 `files` 필드를 통해 번들 스킬은 추가 참조 파일을 디스크에 추출할 수 있다. 이 메커니즘은 모델이 `Read`/`Grep` 도구로 스킬 파일을 접근할 수 있도록 허용한다: + +```typescript +export type BundledSkillDefinition = { + // ... + files?: Record // 상대경로 → 파일 내용 + getPromptForCommand: (args: string, context: ToolUseContext) => Promise +} +``` + +`files`가 존재할 경우 `registerBundledSkill()`은 `getPromptForCommand`를 래핑하여 지연(lazy) 추출을 수행한다: + +```typescript +// 프로세스당 한 번만 추출 — Promise 메모이제이션으로 경쟁 조건 방지 +let extractionPromise: Promise | undefined +getPromptForCommand = async (args, ctx) => { + extractionPromise ??= extractBundledSkillFiles(definition.name, files) + const extractedDir = await extractionPromise + const blocks = await inner(args, ctx) + if (extractedDir === null) return blocks + return prependBaseDir(blocks, extractedDir) +} +``` + +**보안 설계:** 파일 추출 시 경로 순회 공격(path traversal)을 방지하기 위해 엄격한 검증을 적용한다: + +```typescript +function resolveSkillFilePath(baseDir: string, relPath: string): string { + const normalized = normalize(relPath) + if ( + isAbsolute(normalized) || + normalized.split(pathSep).includes('..') || + normalized.split('/').includes('..') + ) { + throw new Error(`bundled skill file path escapes skill dir: ${relPath}`) + } + return join(baseDir, normalized) +} +``` + +파일 쓰기는 `O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW` 플래그와 `0o600` 퍼미션으로 수행된다. `getBundledSkillsRoot()`의 프로세스별 난수(nonce)가 심볼릭 링크 선점 공격에 대한 1차 방어선이며, `O_NOFOLLOW | O_EXCL`는 추가 방어층이다. + +### 3.4 스킬 프런트매터 파싱 + +디스크 기반 스킬의 `SKILL.md`는 YAML 프런트매터를 통해 메타데이터를 정의한다. `parseSkillFrontmatterFields()`가 처리하는 주요 필드: + +| 프런트매터 키 | 타입 | 설명 | +|-------------|------|------| +| `description` | string | 스킬 설명 | +| `when_to_use` | string | 모델에게 제공하는 사용 시점 힌트 | +| `allowed-tools` | string[] | 허용된 도구 목록 | +| `argument-hint` | string | 인수 힌트 | +| `arguments` | string \| string[] | 인수 이름 목록 | +| `model` | string | 모델 오버라이드 (`inherit`이면 undefined) | +| `user-invocable` | boolean | 사용자 직접 호출 가능 여부 | +| `disable-model-invocation` | boolean | SkillTool 사용 비활성화 | +| `context` | `'fork'` | 실행 컨텍스트 (`fork` 시 서브에이전트 실행) | +| `agent` | string | 에이전트 타입 지정 | +| `effort` | string \| number | 에포트(effort) 레벨 | +| `paths` | string[] | 적용 경로 패턴 | +| `hooks` | HooksSettings | 스킬별 훅 설정 | + +`paths` 필드는 `ignore` 라이브러리를 활용하여 glob 패턴 매칭을 수행한다. `/**` 접미사는 자동 제거되고 `**` 패턴만 있으면 `undefined`(전체 적용)로 처리된다. + +### 3.5 MCP 기반 스킬 빌더 + +`mcpSkillBuilders.ts`는 모듈 간 순환 의존성(circular dependency) 문제를 해결하기 위한 쓰기 전용(write-once) 레지스트리 패턴을 구현한다. + +**문제 배경:** +``` +client.ts → mcpSkills.ts → loadSkillsDir.ts → ... → client.ts +``` +이 의존성 그래프는 순환을 형성한다. `loadSkillsDir.ts`를 동적 임포트(`await import(variable)`)로 처리하면 Bun 번들 바이너리에서 `/$bunfs/root/…` 경로로 해석되어 런타임 오류가 발생한다. + +**해결 방법 — 쓰기 전용 레지스트리:** +```typescript +// mcpSkillBuilders.ts — 의존성 그래프의 리프(leaf) 노드 +export type MCPSkillBuilders = { + createSkillCommand: typeof createSkillCommand + parseSkillFrontmatterFields: typeof parseSkillFrontmatterFields +} + +let builders: MCPSkillBuilders | null = null + +export function registerMCPSkillBuilders(b: MCPSkillBuilders): void { + builders = b +} + +export function getMCPSkillBuilders(): MCPSkillBuilders { + if (!builders) { + throw new Error( + 'MCP skill builders not registered — loadSkillsDir.ts has not been evaluated yet', + ) + } + return builders +} +``` + +`loadSkillsDir.ts`가 모듈 초기화 시점에 `registerMCPSkillBuilders()`를 호출하여 빌더 함수를 등록한다. `commands.ts`의 정적 임포트를 통해 시작 시점에 항상 평가되므로, MCP 서버가 연결되기 훨씬 전에 등록이 완료된다. + +--- + +## 4. SkillTool 구현 분석 + +### 4.1 SkillTool 개요 + +`SkillTool`은 Claude Code의 스킬 실행 전용 도구(`name: 'Skill'`)다. 모델은 이 도구를 통해 슬래시 커맨드(slash command) 스킬을 호출한다. + +**입력 스키마:** +```typescript +z.object({ + skill: z.string().describe('The skill name. E.g., "commit", "review-pr", or "pdf"'), + args: z.string().optional().describe('Optional arguments for the skill'), +}) +``` + +**출력 스키마 (유니언 타입):** +```typescript +// 인라인 실행 결과 +z.object({ + success: z.boolean(), + commandName: z.string(), + allowedTools: z.array(z.string()).optional(), + model: z.string().optional(), + status: z.literal('inline').optional(), +}) + +// 포크 실행 결과 +z.object({ + success: z.boolean(), + commandName: z.string(), + status: z.literal('forked'), + agentId: z.string(), + result: z.string(), +}) +``` + +### 4.2 커맨드 조회 및 MCP 스킬 통합 + +`getAllCommands()`는 로컬/번들 스킬과 MCP 스킬을 통합한다: + +```typescript +async function getAllCommands(context: ToolUseContext): Promise { + const mcpSkills = context + .getAppState() + .mcp.commands.filter( + cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp', + ) + if (mcpSkills.length === 0) return getCommands(getProjectRoot()) + const localCommands = await getCommands(getProjectRoot()) + return uniqBy([...localCommands, ...mcpSkills], 'name') +} +``` + +MCP 스킬(`loadedFrom === 'mcp'`)만 포함하고 일반 MCP 프롬프트는 제외한다. 이전에는 모델이 `mcp__server__prompt` 이름을 추측하여 일반 MCP 프롬프트를 SkillTool로 호출할 수 있었으나, 이 필터로 차단된다. + +### 4.3 입력 검증 (validateInput) + +검증은 다음 단계로 진행된다: + +1. **빈 스킬 이름 검사** — `errorCode: 1` +2. **슬래시 접두사 정규화** — `/commit` → `commit` (이벤트 로깅 후 정규화) +3. **원격 스킬 처리** (`EXPERIMENTAL_SKILL_SEARCH`) — `_canonical_` 형식은 별도 경로 처리 +4. **커맨드 존재 확인** — `findCommand()` 실패 시 `errorCode: 2` +5. **`disableModelInvocation` 확인** — 모델 호출 비활성화 스킬 거부 (`errorCode: 4`) +6. **프롬프트 타입 확인** — `type !== 'prompt'` 거부 (`errorCode: 5`) + +### 4.4 실행 컨텍스트: 인라인 vs. 포크 + +스킬 실행은 `command.context` 필드에 따라 두 가지 모드로 분기된다: + +**인라인 실행 (기본값):** +스킬 프롬프트를 현재 대화의 다음 응답으로 주입한다. 모델은 스킬 내용을 읽고 현재 컨텍스트에서 직접 응답한다. `SkillTool`이 `{ success: true, status: 'inline', allowedTools, model }` 결과를 반환하면 호출 루프가 허용된 도구와 모델을 적용하여 다음 턴을 실행한다. + +**포크 실행 (`context: 'fork'`):** +`executeForkedSkill()`이 독립적인 서브에이전트를 생성하여 스킬을 실행한다: + +```typescript +async function executeForkedSkill( + command, commandName, args, context, canUseTool, parentMessage, onProgress +): Promise> { + const agentId = createAgentId() + // prepareForkedCommandContext()로 서브에이전트 환경 구성 + const { modifiedGetAppState, baseAgent, promptMessages, skillContent } = + await prepareForkedCommandContext(command, args || '', context) + + // 스킬의 effort를 에이전트 정의에 병합 + const agentDefinition = command.effort !== undefined + ? { ...baseAgent, effort: command.effort } + : baseAgent + + for await (const message of runAgent({ agentDefinition, promptMessages, ... })) { + agentMessages.push(message) + // 도구 사용 진행 상황을 onProgress로 보고 + } + + return { data: { success: true, status: 'forked', agentId, result: resultText } } +} +``` + +포크 실행의 특성: +- 독립적인 토큰 예산을 가진 격리된 에이전트 실행 +- 완료 후 결과 텍스트만 상위 컨텍스트에 반환 +- 종료 시 `clearInvokedSkillsForAgent(agentId)`로 메모리 해제 + +### 4.5 스킬 목록 프롬프트 및 예산 관리 + +`prompt.ts`의 `formatCommandsWithinBudget()`은 스킬 목록을 컨텍스트 창 예산 내에서 포맷한다: + +**예산 계산:** +```typescript +export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01 // 컨텍스트 창의 1% +export const CHARS_PER_TOKEN = 4 +export const DEFAULT_CHAR_BUDGET = 8_000 // 200k 컨텍스트 × 4 × 1% + +export function getCharBudget(contextWindowTokens?: number): number { + if (Number(process.env.SLASH_COMMAND_TOOL_CHAR_BUDGET)) { + return Number(process.env.SLASH_COMMAND_TOOL_CHAR_BUDGET) + } + if (contextWindowTokens) { + return Math.floor(contextWindowTokens * CHARS_PER_TOKEN * SKILL_BUDGET_CONTEXT_PERCENT) + } + return DEFAULT_CHAR_BUDGET +} +``` + +**예산 초과 시 우선순위 처리:** + +번들 스킬(`source === 'bundled'`)은 항상 전체 설명이 보존되며, 나머지 스킬에 잔여 예산이 배분된다. 극단적 예산 부족 시에는 비번들 스킬을 이름만 표시하는 축약 모드로 전환한다: + +``` +// 정상: - commit: Commit staged changes with a conventional message +// 예산 부족: - commit +``` + +**항목당 최대 설명 길이:** +```typescript +export const MAX_LISTING_DESC_CHARS = 250 +``` +`whenToUse`와 `description`을 결합한 문자열이 250자를 초과하면 말줄임표(`…`)로 절단된다. + +### 4.6 보안: MCP 스킬의 셸 실행 차단 + +`createSkillCommand()`의 `getPromptForCommand` 구현에서 MCP 스킬은 인라인 셸 실행(`!``...```)이 명시적으로 차단된다: + +```typescript +// Security: MCP skills are remote and untrusted — never execute inline +// shell commands (!`…` / ```! … ```) from their markdown body. +if (loadedFrom !== 'mcp') { + finalContent = await executeShellCommandsInPrompt(finalContent, ...) +} +``` + +로컬 스킬(번들, 사용자, 프로젝트, 플러그인)만 셸 실행(`executeShellCommandsInPrompt`)을 허용한다. + +--- + +## 5. 설계 결정 + +### 5.1 플러그인과 번들 스킬의 이원화 + +현재 아키텍처는 사용자 토글 가능 여부에 따라 플러그인과 번들 스킬을 명확히 분리한다. 이 결정은 `src/plugins/bundled/index.ts`의 주석에 명시되어 있다: + +> "Not all bundled features should be built-in plugins — use this for features that users should be able to explicitly enable/disable. For features with complex setup or automatic-enabling logic (e.g. claude-in-chrome), use src/skills/bundled/ instead." + +`claude-in-chrome`처럼 환경 자동 감지(`shouldAutoEnableClaudeInChrome()`)로 활성화되는 스킬은 플러그인 시스템이 아닌 번들 스킬 시스템에 남아있다. + +### 5.2 Command.source 필드의 의미론적 일관성 + +`source: 'bundled'` vs `source: 'builtin'`의 구분은 미묘하지만 중요하다: +- `'builtin'`: 하드코딩된 슬래시 커맨드(`/help`, `/clear` 등) — `Command` 시스템 외부에 위치 +- `'bundled'`: 번들 스킬 — `Command` 객체로 등록되어 스킬 목록, 분석, 프롬프트 예산 로직에 포함 + +플러그인 스킬이 `source: 'bundled'`를 사용하는 이유는 이 값이 세 가지 시스템 동작을 결정하기 때문이다: (1) 스킬 목록 표시, (2) 분석 이벤트 이름 로깅, (3) 프롬프트 예산에서 설명 보존 우선순위. + +### 5.3 싱글턴 도구 실행 정책 + +`SkillTool`은 `toAutoClassifierInput: ({ skill }) => skill ?? ''`을 정의하여 자동 분류기(auto-classifier)에 스킬 이름만 전달한다. 주석에 따르면 한 번에 하나의 스킬만 실행되어야 한다: + +> "Only one skill/command should run at a time, since the tool expands the command into a full prompt that Claude must process before continuing." + +이 제약은 스킬 확장 프롬프트가 다음 턴 처리 전에 완전히 평가되어야 한다는 구조적 요구에서 비롯된다. + +### 5.4 지연 로딩과 메모이제이션 전략 + +번들 스킬의 파일 추출은 두 가지 레벨의 지연 로딩을 적용한다: + +1. **첫 호출 시 추출** (`??=` 연산자로 Promise 메모이제이션): 동일 프로세스에서 동시 호출이 와도 단일 쓰기 작업만 수행 +2. **실패 시 계속 실행** (`null` 반환): 파일 쓰기 실패가 스킬 실행을 중단하지 않음 + +스킬 목록 프롬프트는 `memoize(async (_cwd: string) => ...)` 패턴으로 캐시된다. `_cwd` 파라미터는 캐시 키로만 사용되며, 작업 디렉토리별로 별도 캐시가 유지된다. + +### 5.5 로딩 출처(LoadedFrom) 추적 + +`LoadedFrom` 타입은 스킬의 출처를 세분화하여 추적한다: + +```typescript +export type LoadedFrom = + | 'commands_DEPRECATED' // 레거시 /commands/ 디렉토리 + | 'skills' // .claude/skills/ 디렉토리 + | 'plugin' // 플러그인 제공 + | 'managed' // 정책 관리자 배포 + | 'bundled' // CLI 내장 + | 'mcp' // MCP 서버 제공 +``` + +이 값은 보안 정책(셸 실행 허용 여부), 분석 이벤트 메타데이터, 스킬 중복 제거 로직에서 활용된다. + +--- + +## 내비게이션 + +- 이전: [level-3-internals 개요](../README.md) +- 상위: [목차](../README.md) diff --git a/docs/ko/level-3-internals/telemetry.md b/docs/ko/level-3-internals/telemetry.md new file mode 100644 index 0000000..8eaf9ee --- /dev/null +++ b/docs/ko/level-3-internals/telemetry.md @@ -0,0 +1,355 @@ +# 텔레메트리 시스템 + +> **대상 독자**: Claude Code 내부 구현을 심층적으로 이해하려는 개발자 +> **소스 경로**: `src/services/analytics/` + +--- + +## 1. 아키텍처 개요 + +Claude Code의 텔레메트리 시스템은 세 개의 독립적인 백엔드로 구성된다. + +``` +이벤트 발생 + │ + ▼ +logEvent() / logEventAsync() ← src/services/analytics/index.ts + │ + ├─ 싱크 미부착 시 → 이벤트 큐 (메모리) + │ + ▼ +AnalyticsSink (attachAnalyticsSink 이후) + │ + ├─ shouldSampleEvent() → 샘플링 결정 + │ + ├─ Datadog HTTP Intake ← src/services/analytics/datadog.ts + │ + └─ 1P OpenTelemetry ← src/services/analytics/firstPartyEventLogger.ts + │ + └─ FirstPartyEventLoggingExporter (gRPC-over-HTTP) +``` + +이 설계의 핵심 원칙은 **싱크와 발행자의 완전한 분리**다. `index.ts`는 어떤 외부 의존성도 갖지 않으며, 앱 초기화 전에 발생한 이벤트를 메모리 큐에 보관한 후 싱크 부착 시 `queueMicrotask`를 통해 비동기적으로 소화한다. + +--- + +## 2. 이벤트 발행 레이어 (`index.ts`) + +### 2.1 공개 API + +```typescript +// src/services/analytics/index.ts + +export function logEvent(eventName: string, metadata: LogEventMetadata): void +export async function logEventAsync(eventName: string, metadata: LogEventMetadata): Promise +export function attachAnalyticsSink(newSink: AnalyticsSink): void +``` + +`LogEventMetadata`는 `{ [key: string]: boolean | number | undefined }` 타입으로 제한된다. **문자열은 의도적으로 제외**되어 있으며, 코드 스니펫이나 파일 경로의 우발적 기록을 타입 시스템 수준에서 방지한다. + +### 2.2 마커 타입 (Marker Types) + +```typescript +// 개발자가 "이 값이 코드/파일경로가 아님을 확인했다"고 명시할 때 사용 +export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never + +// PII 태깅된 proto 컬럼으로 라우팅되는 값 (BQ 특권 접근 컬럼) +export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never +``` + +두 타입 모두 `never`로 선언되어 실제 값을 담을 수 없다. 타입 캐스팅(`as`)의 형태로만 사용되며, 코드 리뷰 시 감사 추적 역할을 한다. + +### 2.3 `_PROTO_*` 키 처리 + +```typescript +export function stripProtoFields(metadata: Record): Record +``` + +`_PROTO_` 접두사를 가진 키는 PII 태깅된 값으로서 1P 익스포터만 접근할 수 있다. Datadog 전송 전 반드시 `stripProtoFields()`를 호출해야 하며, `sink.ts`가 이 역할을 중앙에서 수행한다. + +### 2.4 싱크 큐 드레인 흐름 + +```typescript +export function attachAnalyticsSink(newSink: AnalyticsSink): void { + if (sink !== null) return // 멱등성 보장 + sink = newSink + + if (eventQueue.length > 0) { + const queuedEvents = [...eventQueue] + eventQueue.length = 0 + + queueMicrotask(() => { // 스타트업 레이턴시 차단 방지 + for (const event of queuedEvents) { + if (event.async) void sink!.logEventAsync(event.eventName, event.metadata) + else sink!.logEvent(event.eventName, event.metadata) + } + }) + } +} +``` + +`queueMicrotask`를 사용해 큐 드레인이 현재 실행 컨텍스트를 차단하지 않도록 한다. + +--- + +## 3. 분석 비활성화 조건 (`config.ts`) + +```typescript +// src/services/analytics/config.ts + +export function isAnalyticsDisabled(): boolean { + return ( + process.env.NODE_ENV === 'test' || + isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) || + isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) || + isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) || + isTelemetryDisabled() + ) +} +``` + +서드파티 클라우드 제공자(Bedrock, Vertex, Foundry)를 사용하는 경우 분석이 자동으로 비활성화된다. 피드백 설문(`isFeedbackSurveyDisabled`)은 로컬 UI 프롬프트로서 서드파티 차단을 적용하지 않는다. + +--- + +## 4. Datadog 백엔드 (`datadog.ts`) + +### 4.1 허용 이벤트 화이트리스트 + +Datadog로 전송되는 이벤트는 `DATADOG_ALLOWED_EVENTS` Set에 명시적으로 등록된 것만 허용된다. + +```typescript +const DATADOG_ALLOWED_EVENTS = new Set([ + 'tengu_init', + 'tengu_api_success', + 'tengu_api_error', + 'tengu_tool_use_success', + 'tengu_tool_use_error', + 'chrome_bridge_connection_succeeded', + // ... (총 약 40개 이벤트) +]) +``` + +### 4.2 배치 전송 메커니즘 + +``` +이벤트 → logBatch[] 추가 + │ + ├─ 배치 크기 >= 100 → 즉시 플러시 + └─ 그 외 → 15초 타이머 (scheduleFlush) +``` + +- `MAX_BATCH_SIZE`: 100 +- `DEFAULT_FLUSH_INTERVAL_MS`: 15,000ms +- `NETWORK_TIMEOUT_MS`: 5,000ms +- 엔드포인트: `https://http-intake.logs.us5.datadoghq.com/api/v2/logs` + +### 4.3 카디널리티 감소 전처리 + +Datadog 전송 전 다음 변환이 적용된다. + +| 필드 | 처리 | +|------|------| +| MCP 도구 이름 (`mcp__*`) | `"mcp"`로 정규화 | +| 모델 이름 (외부 사용자) | 단축 이름으로 정규화, 미등록 모델은 `"other"` | +| 개발 버전 문자열 | 타임스탬프/SHA 제거 (예: `2.0.53-dev.20251124`) | +| HTTP 상태 코드 | `http_status` + `http_status_range` (1xx~5xx)로 분리 | + +### 4.4 사용자 버킷 (`getUserBucket`) + +사용자 ID를 SHA-256 해시하여 0~29 사이의 버킷 번호에 할당한다. 사용자 ID 직접 노출 없이 영향받은 고유 사용자 수를 근사 추정하는 데 사용된다. + +```typescript +const NUM_USER_BUCKETS = 30 +const getUserBucket = memoize((): number => { + const hash = createHash('sha256').update(getOrCreateUserID()).digest('hex') + return parseInt(hash.slice(0, 8), 16) % NUM_USER_BUCKETS +}) +``` + +### 4.5 태그 필드 목록 + +다음 필드는 Datadog `ddtags`로 인덱싱된다. + +``` +arch, clientType, errorType, http_status_range, http_status, +kairosActive, model, platform, provider, skillMode, +subscriptionType, toolName, userBucket, userType, version, versionBase +``` + +--- + +## 5. OpenTelemetry + 1P 이벤트 로깅 (`firstPartyEventLogger.ts`, `firstPartyEventLoggingExporter.ts`) + +### 5.1 OpenTelemetry 구성 + +1P 이벤트 로깅은 OpenTelemetry SDK를 사용한다. + +```typescript +// src/services/analytics/firstPartyEventLogger.ts +import { + BatchLogRecordProcessor, + LoggerProvider, +} from '@opentelemetry/sdk-logs' +import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions' +``` + +`LoggerProvider`에 `FirstPartyEventLoggingExporter`를 `BatchLogRecordProcessor`와 함께 등록한다. 서비스 리소스 속성은 `resourceFromAttributes()`로 정의된다. + +### 5.2 이벤트 샘플링 + +```typescript +// tengu_event_sampling_config GrowthBook 동적 설정에서 로드 +export type EventSamplingConfig = { + [eventName: string]: { sample_rate: number } +} + +export function shouldSampleEvent(eventName: string): number | null { + // null → 100% 로깅 (설정 없음) + // 0 → 전량 드롭 + // 양수 → 해당 비율로 샘플링, sample_rate를 메타데이터에 추가 +} +``` + +### 5.3 `FirstPartyEventLoggingExporter` + +Anthropic의 내부 이벤트 로깅 API(`/api/event_logging/batch`)로 이벤트를 전송하는 커스텀 OpenTelemetry `LogRecordExporter`다. + +```typescript +type FirstPartyEventLoggingEvent = { + event_type: 'ClaudeCodeInternalEvent' | 'GrowthbookExperimentEvent' + event_data: unknown +} +``` + +전송 실패 시 이벤트는 `~/.claude/telemetry/` 디렉토리에 JSONL 파일로 저장되어 다음 세션에서 재전송을 시도한다. 파일명에는 `BATCH_UUID`가 포함되어 세션 간 격리를 보장한다. + +### 5.4 `_PROTO_*` 키 라우팅 + +1P 익스포터는 `_PROTO_*` 키를 proto 필드로 호이스팅한 후 `stripProtoFields()`로 나머지 메타데이터에서 제거한다. 이 방식으로 PII 태깅된 값이 일반 접근 스토리지(Datadog, BQ JSON blob)로 유출되는 것을 방지한다. + +--- + +## 6. GrowthBook 피처 플래그 시스템 (`growthbook.ts`) + +### 6.1 개요 + +GrowthBook은 Claude Code의 피처 플래그와 A/B 테스트를 관리하는 외부 서비스다. SDK는 `@growthbook/growthbook` 패키지를 사용한다. + +### 6.2 사용자 속성 + +```typescript +export type GrowthBookUserAttributes = { + id: string // 사용자 UUID + sessionId: string // 세션 ID + deviceID: string // 기기 ID + platform: 'win32' | 'darwin' | 'linux' + organizationUUID?: string + accountUUID?: string + userType?: string // 'ant' | 'external' + subscriptionType?: string + rateLimitTier?: string + email?: string + appVersion?: string + github?: GitHubActionsMetadata +} +``` + +### 6.3 캐시된 조회 패턴 + +GrowthBook API는 네트워크 레이턴시가 있으므로, 대부분의 조회 함수는 `_CACHED_MAY_BE_STALE` 접미사를 명시적으로 사용한다. + +```typescript +checkStatsigFeatureGate_CACHED_MAY_BE_STALE(gateName: string): boolean +getDynamicConfig_CACHED_MAY_BE_STALE(configName: string, defaultValue: T): T +getFeatureValue_CACHED_MAY_BE_STALE(featureName: string): unknown +``` + +이 명명 규칙은 "이 값은 이전 세션의 캐시일 수 있다"는 사실을 호출자에게 명확히 전달한다. 스타트업 초기에는 이전 세션에서 저장된 캐시를 사용하고, 네트워크 응답 후 갱신된다. + +### 6.4 remoteEval 캐시 우회 문제 + +SDK의 `setForcedFeatures`가 `remoteEval` 응답과 함께 신뢰할 수 없이 동작하는 알려진 문제가 있어, 원격 평가 피처 값에 대한 별도 캐시(`remoteEvalFeatureCache`)를 유지한다. + +### 6.5 인증 변경 후 재초기화 + +인증 상태가 변경될 때(로그인/로그아웃) `refreshGrowthBookAfterAuthChange()`를 호출하여 사용자 속성을 업데이트하고 피처 플래그를 재평가한다. 클라이언트는 인증 여부(`clientCreatedWithAuth`)를 추적하여 필요 시 재생성한다. + +--- + +## 7. 싱크 라우팅 레이어 (`sink.ts`) + +```typescript +// src/services/analytics/sink.ts + +function logEventImpl(eventName: string, metadata: LogEventMetadata): void { + const sampleResult = shouldSampleEvent(eventName) + if (sampleResult === 0) return // 드롭 + + const metadataWithSampleRate = sampleResult !== null + ? { ...metadata, sample_rate: sampleResult } + : metadata + + if (shouldTrackDatadog()) { + // Datadog: _PROTO_* 키 제거 후 전송 + void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate)) + } + + // 1P: _PROTO_* 키 포함 전체 페이로드 전송 + logEventTo1P(eventName, metadataWithSampleRate) +} +``` + +### 7.1 Datadog 게이트 + +`tengu_log_datadog_events` GrowthBook 피처 게이트로 Datadog 전송을 제어한다. 게이트 값은 스타트업 시 `initializeAnalyticsGates()`에서 캐시되며, 초기화 전에는 이전 세션 캐시를 폴백으로 사용한다. + +### 7.2 킬스위치 (`sinkKillswitch.ts`) + +런타임에서 특정 싱크를 비활성화할 수 있는 메커니즘이다. `isSinkKilled('datadog')`가 `true`이면 GrowthBook 게이트와 무관하게 Datadog 전송이 차단된다. + +--- + +## 8. 스타트업 통합 (`main.tsx`) + +```typescript +// src/main.tsx - 스타트업 순서 + +// 1단계: 싱크 초기화 (이벤트 큐 드레인 시작) +initializeAnalyticsSink() + +// 2단계: GrowthBook 초기화 (피처 플래그 로드) +await initializeGrowthBook(userAttributes) + +// 3단계: Datadog 게이트 확인 +initializeAnalyticsGates() +``` + +GrowthBook 초기화(`initializeGrowthBook`) 전에 발생한 이벤트는 `sink.ts`의 `shouldTrackDatadog()` 함수가 캐시된 이전 세션 값으로 라우팅 결정을 내린다. + +--- + +## 9. 메타데이터 풍부화 (`metadata.ts`) + +`getEventMetadata()`는 모든 이벤트에 공통으로 첨부되는 환경 정보를 수집한다. + +- 플랫폼 정보 (`platform`, `arch`) +- 앱 버전, 모델명 +- 세션 ID, 부모 세션 ID +- 구독 유형, API 제공자 +- WSL 버전, Linux 배포판 정보 +- Git 저장소 원격 해시 (식별 가능 정보 없이) +- 에이전트 컨텍스트 (팀메이트 여부, 에이전트 ID) + +--- + +## 10. 개인정보 보호 설계 원칙 + +| 원칙 | 구현 | +|------|------| +| 코드/경로 기록 방지 | `LogEventMetadata`에서 문자열 타입 제외 | +| MCP 서버 설정 보호 | MCP 도구명 `mcp_tool`로 마스킹 | +| PII 격리 | `_PROTO_*` 키 분리 및 특권 컬럼 라우팅 | +| 사용자 ID 직접 노출 방지 | 버킷 해시(`userBucket`) 사용 | +| 서드파티 클라우드 격리 | Bedrock/Vertex/Foundry 시 분석 비활성화 | +| 옵트아웃 지원 | `isTelemetryDisabled()` + 킬스위치 |