From 43ff569aa37804f4184bcc3fe8fa661b9c6e1fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A8=B8=EB=8B=88=ED=8E=98=EB=8B=88?= Date: Fri, 20 Mar 2026 14:06:53 +0900 Subject: [PATCH] docs: add galaxis-agent autonomous SWE agent design spec Design for an autonomous development agent that forks open-swe (LangGraph + Deep Agents) with Gitea webhook + Discord bot triggers, Docker sandbox execution on Oracle VM A1 (ARM64), and Claude API. Includes phased implementation roadmap from conservative (PR-only) to autonomous (auto-merge with E2E gates). --- .../specs/2026-03-20-galaxis-agent-design.md | 1019 +++++++++++++++++ 1 file changed, 1019 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-20-galaxis-agent-design.md diff --git a/docs/superpowers/specs/2026-03-20-galaxis-agent-design.md b/docs/superpowers/specs/2026-03-20-galaxis-agent-design.md new file mode 100644 index 0000000..ef329a8 --- /dev/null +++ b/docs/superpowers/specs/2026-03-20-galaxis-agent-design.md @@ -0,0 +1,1019 @@ +# galaxis-agent: 자율 개발 에이전트 설계 문서 + +## 개요 + +galaxis-po 코드베이스를 자율적으로 개발하는 SWE 에이전트. [open-swe](https://github.com/langchain-ai/open-swe)를 포크하여 LangGraph + Deep Agents 아키텍처를 재활용하고, GitHub 전용 부분을 Gitea/Discord 어댑터로 교체한다. + +### 요구사항 요약 + +| 항목 | 결정 | +|------|------| +| 트리거 | Gitea webhook + Discord bot | +| 실행환경 | Oracle VM A1 (4코어 ARM64/24GB), Docker 샌드박스 | +| LLM | Claude API (Anthropic) | +| 기반 | open-swe 포크/커스터마이즈 | +| 자율도 | conservative(PR만) → E2E 확보 시 autonomous(자동 머지) | +| Gitea | v1.23.1+, `ayuriel.duckdns.org/quant/galaxis-po` | +| Discord | 기존 봇 활용 | + +--- + +## 1. 전체 아키텍처 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Oracle VM (A1) │ +│ │ +│ ┌──────────┐ ┌─────────────────────────────────────────┐ │ +│ │ Gitea │─▶│ galaxis-agent (FastAPI) │ │ +│ │ (기존) │ │ - Gitea webhook 수신 │ │ +│ └──────────┘ │ - Discord bot 연동 │ │ +│ │ - LangGraph 에이전트 │ │ +│ ┌──────────┐ │ - 작업 관리/스레드 관리 │ │ +│ │ docker- │ └──────────┬────────────────────────────┘ │ +│ │ socket- │◀────────────┤ │ +│ │ proxy │ │ 작업 요청 시 │ +│ └──────────┘ ┌──────────▼──────────────┐ │ +│ │ Docker 샌드박스 컨테이너 │ │ +│ ┌──────────┐ │ - repo clone │ │ +│ │ LangGraph│ │ - 코드 분석/수정 │ │ +│ │ Server │ │ - uv run pytest │ │ +│ │ (스레드 │ │ - git commit & push │ │ +│ │ 관리) │ └─────────────────────────┘ │ +│ └──────────┘ │ +│ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │PostgreSQL│ │ galaxis- │ (기존 galaxis-po 서비스) │ +│ │ (기존) │ │ po app │ │ +│ └──────────┘ └──────────┘ │ +└──────────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + ┌───────────┐ ┌────────────┐ + │ Claude API │ │ Discord │ + │ (Anthropic)│ │ (알림/대화) │ + └───────────┘ └────────────┘ +``` + +### 핵심 흐름 + +1. Gitea 이슈에 `@agent` 멘션 또는 Discord에서 작업 요청 +2. galaxis-agent가 webhook/메시지 수신 +3. 결정론적 스레드 ID 생성 (이슈 ID 기반) +4. Docker 컨테이너 생성, 레포 클론 +5. Claude API로 코드 분석 → 수정 → 테스트 +6. Gitea에 PR 생성 + Discord 알림 +7. (E2E 준비 후) 테스트 통과 시 자동 머지 옵션 + +### 컨테이너 구성 + +- **에이전트 서버**: `docker-compose`로 상시 실행 +- **LangGraph Server**: 에이전트 서버와 함께 실행 (스레드/런/스토어 관리) +- **docker-socket-proxy**: Docker API 접근 제한 (보안) +- **작업 컨테이너**: 작업마다 생성/삭제 (`docker-py`로 관리) +- **네트워크**: Gitea와 같은 Docker 네트워크(`galaxis-net`)로 내부 통신 + +--- + +## 2. LangGraph Platform 의존성 + +open-swe는 LangGraph Platform (LangGraph Server)에 의존한다. `webapp.py`의 `langgraph_sdk.get_client()`가 스레드 관리, 런 생성, 키-값 스토어(메시지 큐잉)를 모두 LangGraph Server에 위임한다. + +### 셀프 호스팅 전략 + +LangGraph는 오픈소스 `langgraph-api` 패키지로 셀프 호스팅 가능하다. + +```yaml +# docker-compose.yml 에 LangGraph Server 추가 +services: + langgraph-server: + image: langchain/langgraph-api:latest + environment: + - LANGSMITH_API_KEY=${LANGSMITH_API_KEY} # 선택: 트레이싱용 + volumes: + - langgraph-data:/data + networks: + - galaxis-net + deploy: + resources: + limits: + cpus: "0.5" + memory: 1G +``` + +### 대안: LangGraph 제거 + 자체 상태 관리 + +LangGraph Server 의존성이 무거우면 Phase 2에서 다음으로 대체 가능: + +- **스레드 관리**: SQLite 기반 스레드 테이블 (thread_id, metadata, sandbox_id) +- **런 관리**: asyncio.Queue + 상태 머신 +- **스토어 (메시지 큐)**: SQLite 테이블 (thread_id, pending_messages) + +Phase 1에서 LangGraph Server ARM64 호환성을 검증한 후, 문제가 있으면 자체 구현으로 전환한다. + +### `deepagents` 패키지 호환성 + +`deepagents`는 LangChain 생태계 패키지로, 순수 Python이므로 ARM64 호환에 문제없다. Phase 1에서 `pip install deepagents`로 ARM64 설치를 검증한다. 만약 네이티브 의존성 문제가 발생하면 LangChain의 `create_react_agent`로 대체 가능하다. + +--- + +## 3. 컴포넌트별 변경 계획 + +open-swe 기준으로 유지/교체/신규/삭제 네 카테고리로 구분한다. + +### 유지 (그대로 사용) + +| 컴포넌트 | 파일 | 역할 | +|----------|------|------| +| 에이전트 코어 | `server.py:get_agent()` | Deep Agent 생성, 샌드박스 라이프사이클 | +| 미들웨어 | `middleware/tool_error_handler.py` | 도구 에러 → LLM에 에러 메시지 반환 | +| 미들웨어 | `middleware/ensure_no_empty_msg.py` | 빈 메시지 방지 | +| 미들웨어 | `middleware/check_message_queue.py` | 작업 중 추가 메시지 주입 | +| 미들웨어 | `middleware/open_pr.py` | PR 안전망 (수정 후 교체된 도구 호출) | +| 암호화 | `encryption.py` | Gitea 토큰 암호화 저장 | +| 모델 유틸 | `utils/model.py` | Claude API 모델 초기화 | +| 프롬프트 구조 | `prompt.py` | 모듈식 시스템 프롬프트 (내용은 커스터마이즈) | + +### 교체 (GitHub → Gitea/Discord로 포팅) + +| 원본 | 변경 후 | 변경 내용 | +|------|---------|----------| +| `webapp.py` GitHub webhook | `webapp.py` Gitea webhook | Gitea 서명 검증, 이벤트 페이로드 파싱 변경 | +| `webapp.py` Slack webhook | `webapp.py` Discord gateway | `discord.py` 라이브러리로 bot 이벤트 수신 | +| `tools/commit_and_open_pr.py` | 동일 파일명 | PyGithub → Gitea API | +| `tools/github_comment.py` | `tools/gitea_comment.py` | Gitea 이슈/PR 코멘트 API | +| `tools/slack_thread_reply.py` | `tools/discord_reply.py` | Discord 채널/스레드 메시지 전송 | +| `integrations/langsmith.py` | `integrations/docker_sandbox.py` | `docker-py`로 컨테이너 생성/실행/삭제 | + +### 신규 추가 + +| 컴포넌트 | 역할 | +|----------|------| +| `utils/gitea_client.py` | Gitea REST API 클라이언트 (인증, PR, 코멘트, 머지) | +| `utils/discord_client.py` | Discord bot 연동 (메시지 수신/발송, 스레드 관리) | +| `Dockerfile.sandbox` | 작업 컨테이너용 이미지 (Python 3.12, uv, Node.js, git) | +| `docker-compose.yml` | 에이전트 서버 + LangGraph + docker-socket-proxy + 네트워크 | +| `config.py` | 환경변수 관리 | + +### 삭제 (불필요) + +| 컴포넌트 | 이유 | +|----------|------| +| `tools/linear_comment.py` | Linear 미사용 | +| `integrations/modal.py`, `daytona.py`, `runloop.py` | 클라우드 샌드박스 미사용 | +| `utils/linear_team_repo_map.py` | Linear 미사용 | +| GitHub App 관련 인증 코드 | Gitea 토큰 방식으로 대체 | + +--- + +## 4. Docker 샌드박스 설계 + +### 샌드박스 컨테이너 이미지 (`Dockerfile.sandbox`) + +galaxis-po 개발에 필요한 모든 도구를 포함한 ARM64 호환 이미지. 샌드박스는 코드 실행 전용이므로 `docker-cli`는 포함하지 않는다. + +```dockerfile +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y \ + git curl postgresql-client + +# uv 패키지 매니저 +RUN curl -LsSf https://astral.sh/uv/install.sh | sh + +# Node.js LTS (프론트엔드 빌드/린트) +RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ + && apt-get install -y nodejs + +WORKDIR /workspace +``` + +### 컨테이너 라이프사이클 + +``` +작업 요청 수신 + │ + ▼ +┌─ create_sandbox() ──────────────────────────┐ +│ 1. docker.containers.run( │ +│ image="galaxis-sandbox:latest", │ +│ detach=True, │ +│ network="galaxis-net", │ +│ mem_limit="4g", │ +│ cpu_count=2, │ +│ pids_limit=256, │ +│ environment={ │ +│ "DATABASE_URL": test_db_url, │ +│ }, │ +│ ) │ +│ 2. git clone (credential helper) → /workspace/galaxis-po │ +│ 3. uv sync (백엔드 의존성) │ +│ 4. npm ci (프론트엔드 의존성) │ +└──────────────────────────────────────────────┘ + │ + ▼ + 에이전트 작업 (코드 수정, 테스트 실행) + │ + ▼ +┌─ cleanup_sandbox() ─────────────────────────┐ +│ - PR 생성 완료 확인 │ +│ - git credential 파일 삭제 │ +│ - 컨테이너 stop → remove │ +└──────────────────────────────────────────────┘ +``` + +### `SandboxBackendProtocol` 구현 + +```python +class DockerSandbox: + async def execute(self, command: str, timeout: int = 300) -> ExecuteResponse: + """컨테이너 내에서 쉘 명령 실행 (docker exec)""" + + async def read_file(self, path: str) -> str: + """컨테이너 내 파일 읽기""" + + async def write_file(self, path: str, content: str) -> None: + """컨테이너 내 파일 쓰기""" + + async def close(self) -> None: + """credential 정리 후 컨테이너 삭제""" +``` + +### 테스트용 데이터베이스 + +샌드박스 컨테이너가 `uv run pytest`를 실행하려면 PostgreSQL 접근이 필요하다. + +- **단위 테스트**: mock/fixture 기반으로 DB 불필요 +- **E2E 테스트**: 기존 PostgreSQL에 테스트 전용 DB(`galaxis_test`)를 사용 + - 샌드박스 환경변수로 `DATABASE_URL=postgresql://...galaxis_test` 전달 + - 테스트 실행 전 `alembic upgrade head`로 스키마 적용 + - 테스트 완료 후 DB 초기화 (truncate) +- 샌드박스와 PostgreSQL은 같은 `galaxis-net` 네트워크에 있으므로 접근 가능 + +### 리소스 제한 (A1 4코어/24GB 기준) + +**현재 VM 사용량 추정:** + +| 서비스 | CPU | 메모리 | +|--------|-----|--------| +| OS + Docker 데몬 | ~0.5 | ~1GB | +| Gitea | ~0.5 | ~1GB | +| PostgreSQL (기존) | ~0.5 | ~2GB | +| galaxis-po app (기존) | ~0.5 | ~1GB | +| **소계 (기존)** | **~2** | **~5GB** | + +**에이전트 추가분:** + +| 서비스 | CPU | 메모리 | +|--------|-----|--------| +| galaxis-agent 서버 | 0.5 | 1GB | +| LangGraph Server | 0.5 | 1GB | +| docker-socket-proxy | 0.1 | 128MB | +| 작업 컨테이너 (실행 중일 때만) | 1 | 4GB | +| **소계 (에이전트)** | **~2** | **~6GB** | + +**총 예상**: ~4코어 / ~11GB (24GB 중). `npm ci`가 피크 시 추가 2-3GB 사용 가능하나 여유 있음. + +| 항목 | 설정값 | +|------|--------| +| 동시 작업 수 | **1개** (리소스 제약상 직렬 처리, 큐잉으로 대기) | + +### 의존성 캐시 전략 + +- Docker named volume (`galaxis-uv-cache`, `galaxis-npm-cache`)을 컨테이너에 마운트 +- 첫 실행 이후 의존성 설치 시간 대폭 단축 +- 주기적으로(주 1회) 캐시 갱신 + +--- + +## 5. Gitea 연동 설계 + +### Webhook 수신 + +**트리거 이벤트:** + +| 이벤트 | Gitea 타입 | 에이전트 동작 | +|--------|-----------|--------------| +| 이슈 코멘트에 `@agent` 멘션 | `issue_comment` | 이슈 분석 → 코드 수정 → PR 생성 | +| PR 코멘트에 `@agent` 멘션 | `issue_comment` | PR 피드백 반영 → 커밋 추가 | +| 이슈에 특정 라벨 부착 | `issue_label` | 라벨 기반 자동 할당 (예: `agent-fix`) | +| PR 리뷰 요청 | `pull_request` (review_requested) | 코드 리뷰 수행 → 코멘트 | + +**서명 검증:** + +```python +# Gitea: X-Gitea-Signature 헤더 (HMAC-SHA256) +def verify_gitea_signature(payload: bytes, signature: str, secret: str) -> bool: + expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() + return hmac.compare_digest(expected, signature) +``` + +**결정론적 스레드 ID:** + +```python +thread_id = hashlib.sha256(f"gitea-issue:{repo}:{issue_id}".encode()).hexdigest() +# → UUID 포맷으로 변환 +``` + +**Rate limiting:** + +```python +# webhook 요청 제한 (분당 10회) +from slowapi import Limiter +limiter = Limiter(key_func=get_remote_address) + +@app.post("/webhooks/gitea") +@limiter.limit("10/minute") +async def gitea_webhook(...): + ... +``` + +### Gitea API 클라이언트 + +```python +class GiteaClient: + def __init__(self, base_url: str, token: str): + # base_url: "http://gitea:3000/api/v1" (Docker 내부 통신) + + async def create_pull_request(self, owner, repo, title, head, base, body) -> dict + async def merge_pull_request(self, owner, repo, pr_number, merge_type="merge") -> dict + async def create_issue_comment(self, owner, repo, issue_number, body) -> dict + async def create_pr_review(self, owner, repo, pr_number, body, comments) -> dict + async def get_issue(self, owner, repo, issue_number) -> dict + async def get_issue_comments(self, owner, repo, issue_number) -> list + async def add_label(self, owner, repo, issue_number, labels) -> None + async def create_branch(self, owner, repo, branch_name, old_branch) -> dict +``` + +### 에이전트 → Gitea 작업 흐름 + +``` +이슈 #42: "FactorCalculator에 듀얼 모멘텀 추가" @agent + │ + ▼ +1. Webhook 수신 → 이슈 내용 + 코멘트 파싱 +2. Gitea API로 이슈 상세 + 기존 코멘트 조회 +3. 이슈에 코멘트: "작업을 시작합니다." +4. 샌드박스에서 작업 수행 +5. 브랜치 생성: open-swe/{thread_id} + 커밋 & 푸시 +6. PR 생성 (title, body, base: master, 이슈 #42 참조) +7. 이슈에 코멘트: "PR #43 을 생성했습니다." + Discord에도 알림 전송 +``` + +### Git 인증 (샌드박스 → Gitea) + +open-swe의 credential helper 패턴을 채택한다. URL에 토큰을 노출하지 않는다. + +```python +async def setup_git_credentials(sandbox, gitea_token: str): + """git credential helper로 인증 설정""" + credential_file = "/tmp/.git-credentials" + await sandbox.execute( + f'echo "http://agent:{gitea_token}@gitea:3000" > {credential_file}' + ) + await sandbox.execute( + f"git config --global credential.helper 'store --file={credential_file}'" + ) + +async def cleanup_git_credentials(sandbox): + """credential 파일 삭제""" + await sandbox.execute("rm -f /tmp/.git-credentials") + await sandbox.execute("git config --global --unset credential.helper") +``` + +- Gitea Application Token 사용 (repo 권한) +- 작업 완료 후 credential 파일 즉시 삭제 +- 내부 Docker 네트워크(`galaxis-net`)로 통신 + +### Webhook URL 설정 + +Gitea와 에이전트가 같은 Docker 네트워크에 있으므로, webhook URL은 Docker 내부 포트를 사용한다: + +``` +URL: http://galaxis-agent:8000/webhooks/gitea +``` + +호스트 포트 매핑(`8100:8000`)은 외부 디버깅/모니터링 접근용이다. + +--- + +## 6. Discord 연동 설계 + +### 이중 인터페이스 + +| 방향 | 방식 | 용도 | +|------|------|------| +| Discord → 에이전트 | Bot Gateway (`discord.py`) | 멘션 수신, 작업 요청 | +| 에이전트 → Discord | REST API (httpx) | 알림, 진행 상황, 결과 보고 | + +### 필수 Gateway Intents + +Discord Developer Portal에서 다음 Privileged Intents를 활성화해야 한다: + +- **MESSAGE_CONTENT** — 메시지 본문 읽기 (멘션 내용 파싱) +- **GUILD_MESSAGES** — 서버 메시지 수신 + +```python +intents = discord.Intents.default() +intents.message_content = True +intents.guild_messages = True +bot = commands.Bot(command_prefix="!", intents=intents) +``` + +### Bot Gateway 수신 + +```python +class DiscordHandler: + async def on_message(self, message): + if not self.bot.user.mentioned_in(message): + return + + content = message.content.replace(f"<@{self.bot.user.id}>", "").strip() + + thread_id = hashlib.sha256( + f"discord:{message.channel.id}:{message.id}".encode() + ).hexdigest() + + # 에이전트 실행 또는 메시지 큐잉 +``` + +### 메시지 포맷 + +**작업 요청:** `@agent galaxis-po 이슈 #42 해결해줘` 또는 `@agent factor_calculator.py에 듀얼 모멘텀 추가해줘` + +repo 생략 시 기본값 `galaxis-po` 사용. + +### 알림 흐름 + +``` +작업 시작 → "작업을 시작합니다: 이슈 #42 - FactorCalculator에 듀얼 모멘텀 추가" +작업 중 → "관련 파일 분석 완료: factor_calculator.py, kjb.py" +테스트 → "테스트 실행 중..." → "테스트 통과 (23 passed, 0 failed)" +완료 → "PR #43 생성 완료: feat: add dual momentum" + → "https://ayuriel.duckdns.org/quant/galaxis-po/pulls/43" +(자동 머지) → "PR #43 자동 머지 완료" +``` + +### Discord ↔ Gitea 연결 + +- Discord에서 `이슈 #42` 참조 시 → Gitea API로 이슈 상세 조회 → 컨텍스트로 활용 +- 이슈 없이 자유형 작업 요청도 가능 → PR 본문에 Discord 대화 컨텍스트 포함 + +### Follow-up 대화 + +open-swe의 메시지 큐잉 패턴을 재활용한다. 에이전트 작업 중 추가 메시지가 도착하면 큐에 저장하고 다음 모델 호출 시 주입한다. + +### FastAPI + discord.py 공존 + +```python +async def lifespan(app: FastAPI): + discord_task = asyncio.create_task(discord_bot.start(DISCORD_TOKEN)) + yield + await discord_bot.close() + +app = FastAPI(lifespan=lifespan) +``` + +discord.py의 Gateway 재연결은 라이브러리 내장 기능으로 처리된다. 재연결 실패 시 Gitea 코멘트로 fallback 알림한다. + +--- + +## 7. 시스템 프롬프트 & 자율도 제어 + +### 시스템 프롬프트 구성 + +```python +SYSTEM_PROMPT = "\n\n".join([ + WORKING_ENVIRONMENT, # 유지: 샌드박스 환경 설명 + FILE_MANAGEMENT, # 유지: 파일 작업 규칙 + TASK_EXECUTION, # 유지: 작업 수행 가이드라인 + TOOL_USAGE, # 수정: Gitea/Discord 도구 설명 + CODING_STANDARDS, # 수정: galaxis-po 코딩 컨벤션 + AUTONOMY_LEVEL, # 신규: 자율도 제어 지침 + AGENTS_MD, # 레포 내 AGENTS.md + CLAUDE.md 주입 +]) +``` + +### 프롬프트 로딩 파이프라인 + +open-swe의 `read_agents_md_in_sandbox()` 함수를 확장하여 `AGENTS.md`와 `CLAUDE.md`를 모두 읽는다: + +```python +async def read_repo_instructions(sandbox) -> str: + """AGENTS.md와 CLAUDE.md를 모두 읽어서 프롬프트에 주입""" + sections = [] + + # AGENTS.md: 에이전트 전용 규칙 (우선순위 높음) + agents_md = await sandbox.read_file("/workspace/galaxis-po/AGENTS.md") + if agents_md: + sections.append(f"## Repository Agent Rules\n{agents_md}") + + # CLAUDE.md: 프로젝트 컨벤션 (보충 정보) + claude_md = await sandbox.read_file("/workspace/galaxis-po/CLAUDE.md") + if claude_md: + sections.append(f"## Project Conventions\n{claude_md}") + + return "\n\n".join(sections) +``` + +`GALAXIS_PROJECT_CONTEXT` 프롬프트 섹션은 제거한다. `CLAUDE.md`에서 동일 정보를 로딩하므로 중복이 발생하기 때문이다. + +### AGENTS.md (레포 루트에 배치) + +에이전트 전용 규칙만 담는다. 프로젝트 일반 컨벤션은 `CLAUDE.md`에서 로딩된다. + +```markdown +# AGENTS.md + +## 작업 전 필수 확인 +1. docs/plans/ 에서 관련 설계 문서 확인 +2. 변경 대상 파일을 먼저 읽고 이해한 후 수정 + +## 커밋 규칙 +- conventional commits: feat/fix/refactor/test/docs +- 한 PR에 하나의 논리적 변경만 포함 + +## 테스트 요구사항 +- 비즈니스 로직 변경 시 반드시 단위 테스트 추가/수정 +- PR 생성 전 uv run pytest 통과 필수 + +## 금지 사항 +- quant.md 수정 금지 +- .env 파일 수정 금지 +- 기존 API 스키마 breaking change 금지 (추가는 가능) +- alembic downgrade 실행 금지 + +## 허용 사항 +- alembic migration 파일 생성은 허용 (DB 스키마 변경 시 필수) +``` + +### 자율도 제어 시스템 + +```python +class AgentConfig: + autonomy_level: str = "conservative" # "conservative" | "autonomous" + + # conservative 모드 (기본값) + auto_merge: bool = False + require_tests: bool = True + max_files_changed: int = 10 + + # 경로 접근 제어 (읽기는 전체 허용, 쓰기만 제한) + writable_paths: list = [ + "backend/app/", + "backend/tests/", + "frontend/src/", + "backend/alembic/versions/", # migration 생성 허용 + "docs/", + ] + blocked_paths: list = [ + ".env", + "docker-compose.prod.yml", + "quant.md", + ] + + # autonomous 모드 (E2E 확보 후 전환) + # auto_merge: True + # require_e2e: True +``` + +### 자율도 전환 흐름 + +``` +[conservative] + 작업 완료 → 테스트 통과 → PR 생성 → Discord 알림 → 사람 리뷰 → 수동 머지 + +[autonomous] (config 변경으로 전환) + 작업 완료 → 단위 테스트 통과 → E2E 테스트 통과 → PR 생성 → 자동 머지 → Discord 알림 + +[안전장치] + - E2E 실패 시: 머지하지 않고 Discord에 실패 알림 + - 변경 파일 > max_files_changed: 머지하지 않고 리뷰 요청 + - blocked_paths 수정 감지: 즉시 중단 + 알림 +``` + +--- + +## 8. 보안 설계 + +### Docker 소켓 접근 제한 + +에이전트 서버는 Docker 소켓을 통해 샌드박스 컨테이너를 관리한다. 직접 소켓 마운트는 root 동등 권한을 부여하므로, [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy)로 API 접근을 제한한다. + +```yaml +services: + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:latest + environment: + - CONTAINERS=1 # 컨테이너 생성/관리 허용 + - POST=1 # POST 요청 허용 (컨테이너 생성) + - EXEC=1 # exec 허용 (명령 실행) + - IMAGES=1 # 이미지 조회 허용 (샌드박스 이미지 확인용, 빌드/삭제는 불가) + - NETWORKS=0 # 네트워크 관리 차단 + - VOLUMES=0 # 볼륨 관리 차단 + - SERVICES=0 # 서비스 관리 차단 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - galaxis-net + + galaxis-agent: + environment: + - DOCKER_HOST=tcp://docker-socket-proxy:2375 + # /var/run/docker.sock 마운트하지 않음 +``` + +### 에이전트 서버 보안 + +- 에이전트 서버 컨테이너는 **non-root 사용자**로 실행 +- `/webhooks/gitea`: HMAC-SHA256 서명 검증 필수 +- `/health/*`: 읽기 전용, 민감 정보 미포함 +- `/test/sandbox`: API key 인증 필수 (`X-API-Key` 헤더) + +```python +async def require_api_key(request: Request): + api_key = request.headers.get("X-API-Key") + if api_key != settings.AGENT_API_KEY: + raise HTTPException(status_code=401, detail="Invalid API key") +``` + +### Git 인증 보안 + +- URL에 토큰 미노출 (credential helper 사용) +- 작업 완료 후 credential 파일 즉시 삭제 +- 토큰은 Fernet 암호화 후 스레드 메타데이터에 저장 + +### 샌드박스 격리 + +- 샌드박스 컨테이너는 `galaxis-net` 네트워크에만 접근 (외부 접근은 Claude API + Gitea만) +- Docker 소켓 미마운트 (샌드박스에서 Docker 탈출 불가) +- 리소스 제한 (`mem_limit`, `cpu_count`, `pids_limit`) + +--- + +## 9. 배포 & 운영 구성 + +### Docker Compose + +```yaml +services: + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:latest + environment: + - CONTAINERS=1 + - POST=1 + - EXEC=1 + - IMAGES=0 + - NETWORKS=0 + - VOLUMES=0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - galaxis-net + restart: unless-stopped + + langgraph-server: + image: langchain/langgraph-api:latest + environment: + - LANGSMITH_API_KEY=${LANGSMITH_API_KEY:-} + volumes: + - langgraph-data:/data + networks: + - galaxis-net + restart: unless-stopped + deploy: + resources: + limits: + cpus: "0.5" + memory: 1G + + galaxis-agent: + build: . + image: galaxis-agent:latest + restart: unless-stopped + user: "1000:1000" + ports: + - "8100:8000" + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - DOCKER_HOST=tcp://docker-socket-proxy:2375 + - LANGGRAPH_URL=http://langgraph-server:8123 + - GITEA_URL=http://gitea:3000 + - GITEA_EXTERNAL_URL=https://ayuriel.duckdns.org + - GITEA_TOKEN=${GITEA_TOKEN} + - GITEA_WEBHOOK_SECRET=${GITEA_WEBHOOK_SECRET} + - DISCORD_TOKEN=${DISCORD_TOKEN} + - DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID} + - SANDBOX_IMAGE=galaxis-sandbox:latest + - AUTONOMY_LEVEL=conservative + - AGENT_API_KEY=${AGENT_API_KEY} + - FERNET_KEY=${FERNET_KEY} + - TEST_DATABASE_URL=${TEST_DATABASE_URL} + volumes: + - uv-cache:/cache/uv + - npm-cache:/cache/npm + networks: + - galaxis-net + deploy: + resources: + limits: + cpus: "1" + memory: 2G + +networks: + galaxis-net: + external: true + +volumes: + uv-cache: + npm-cache: + langgraph-data: +``` + +### Gitea Webhook 설정 + +``` +URL: http://galaxis-agent:8000/webhooks/gitea +Method: POST +Content: application/json +Secret: ${GITEA_WEBHOOK_SECRET} +Events: Issue Comments, Pull Request Comments, + Pull Requests (review_requested), Issues (labeled) +``` + +### 환경변수 + +```bash +# LLM +ANTHROPIC_API_KEY=sk-ant-... + +# Gitea +GITEA_URL=http://gitea:3000 +GITEA_EXTERNAL_URL=https://ayuriel.duckdns.org +GITEA_TOKEN=... +GITEA_WEBHOOK_SECRET=... + +# Discord +DISCORD_TOKEN=... +DISCORD_CHANNEL_ID=... +DISCORD_BOT_USER_ID=... + +# LangGraph +LANGGRAPH_URL=http://langgraph-server:8123 +LANGSMITH_API_KEY=... # 선택: 트레이싱용 + +# Agent +AUTONOMY_LEVEL=conservative +DEFAULT_REPO_OWNER=quant +DEFAULT_REPO_NAME=galaxis-po +AGENT_API_KEY=... # /test/* 엔드포인트 인증 + +# Sandbox +SANDBOX_IMAGE=galaxis-sandbox:latest +SANDBOX_MEM_LIMIT=4g +SANDBOX_CPU_COUNT=2 +SANDBOX_TIMEOUT=600 + +# Database (테스트용) +TEST_DATABASE_URL=postgresql://user:pass@postgres:5432/galaxis_test + +# 암호화 +FERNET_KEY=... +``` + +### 작업 큐잉 메커니즘 + +동시 작업 수를 1개로 제한하되, 큐는 SQLite에 영속적으로 저장하여 서버 재시작 시에도 유실되지 않도록 한다. + +```python +# task_queue.py +class PersistentTaskQueue: + """SQLite 기반 영속 작업 큐""" + + def __init__(self, db_path: str = "/data/task_queue.db"): + ... + + async def enqueue(self, thread_id: str, payload: dict) -> str: + """작업 큐에 추가. task_id 반환""" + + async def dequeue(self) -> Optional[dict]: + """다음 작업 가져오기 (FIFO)""" + + async def mark_completed(self, task_id: str, result: dict): + """작업 완료 처리""" + + async def get_pending(self) -> list[dict]: + """미처리 작업 목록 (복구용)""" +``` + +### 로깅 & 모니터링 + +- 구조화된 JSON 로깅 (timestamp, level, thread_id, event, issue, repo) +- 헬스 체크: `GET /health`, `/health/gitea`, `/health/discord` +- 작업 이력: SQLite 로컬 DB (작업ID, 이슈, 상태, 소요시간, 토큰 사용량) +- API 비용 추적: Anthropic API 응답의 `usage` 필드 기록 +- 좀비 컨테이너 정리: 30분마다 `SANDBOX_TIMEOUT * 2` 초과 컨테이너 제거 + +### 배포 절차 + +```bash +# 1. Gitea에 리포 생성: quant/galaxis-agent + +# 2. Oracle VM에서 클론 +git clone https://ayuriel.duckdns.org/quant/galaxis-agent.git +cd galaxis-agent + +# 3. 샌드박스 이미지 빌드 (ARM64) +docker build -f Dockerfile.sandbox -t galaxis-sandbox:latest . + +# 4. 테스트 DB 생성 +psql -U postgres -c "CREATE DATABASE galaxis_test;" + +# 5. 환경변수 설정 +cp .env.example .env # 편집 + +# 6. 에이전트 서버 시작 +docker compose up -d + +# 7. Gitea webhook 등록 (웹 UI) + +# 8. 동작 확인: Gitea 이슈에 "@agent 테스트" 코멘트 작성 +``` + +--- + +## 10. 에러 핸들링 & 테스트 전략 + +### 에러 시나리오별 처리 + +| 시나리오 | 처리 방식 | +|---------|----------| +| Claude API 오류 (429/500) | 지수 백오프 재시도 (3회). 실패 시 Discord 알림 + 작업 중단 | +| 샌드박스 타임아웃 | `SANDBOX_TIMEOUT` 초과 시 컨테이너 강제 종료. PR 안전망 미들웨어가 변경사항 있으면 WIP PR 생성 | +| 테스트 실패 | 에이전트가 1회 자동 수정 시도. 재실패 시 PR에 실패 로그 첨부 + 사람 리뷰 요청 | +| Gitea API 오류 | 3회 재시도. 실패 시 Discord로 fallback 알림 | +| Discord 연결 끊김 | 자동 재연결 (`discord.py` 내장). Gitea 코멘트로 fallback | +| 컨테이너 생성 실패 | 디스크/메모리 부족 가능. Discord 알림 + 리소스 상태 보고 | +| git push 실패 | 인증/충돌 확인. 충돌 시 rebase 시도 1회. 실패 시 알림 | +| 도구 실행 에러 | `ToolErrorMiddleware`가 에러를 LLM에 반환 → LLM이 대안 시도 | + +### 작업 상태 머신 + +``` +QUEUED → RUNNING → COMPLETED + → FAILED + → TIMEOUT +``` + +각 전환 시 Discord 알림 + 작업 이력 DB 기록. + +### 복구 메커니즘 + +```python +async def recover_on_startup(): + # 1. SQLite 큐에서 미처리 작업 확인 + pending = await task_queue.get_pending() + + # 2. 실행 중이던 컨테이너의 변경사항 확인 → WIP PR 생성 + running = docker_client.containers.list( + filters={"label": "galaxis-agent-sandbox", "status": "running"} + ) + for container in running: + diff = await execute_in_container(container, "git diff --stat") + if diff: + await create_wip_pr(container) + container.stop() + container.remove() + + # 3. 미처리 작업 재실행 + for task in pending: + await dispatch_agent(task) +``` + +### 테스트 전략 + +**단위 테스트:** + +``` +tests/ +├── test_gitea_webhook.py # webhook 페이로드 파싱, 서명 검증 +├── test_gitea_client.py # API 클라이언트 (mocked) +├── test_discord_handler.py # 멘션 파싱, 메시지 포맷 +├── test_docker_sandbox.py # 컨테이너 생성/정리 로직 +├── test_thread_id.py # 결정론적 스레드 ID 생성 +├── test_autonomy_config.py # 자율도 설정 검증 +├── test_middleware.py # 미들웨어 파이프라인 +└── test_prompt.py # 프롬프트 조립 +``` + +**통합 테스트:** + +``` +tests/integration/ +├── test_gitea_webhook_e2e.py # 실제 Gitea webhook → PR 생성 +├── test_discord_flow.py # Discord 멘션 → 작업 실행 → 알림 +└── test_sandbox_lifecycle.py # 컨테이너 생성 → 명령 실행 → 정리 +``` + +**스모크 테스트 (배포 후 검증):** + +```bash +curl http://localhost:8100/health +curl http://localhost:8100/health/gitea +curl http://localhost:8100/health/discord +curl -X POST http://localhost:8100/test/sandbox -H "X-API-Key: ${AGENT_API_KEY}" +``` + +### API 비용 안전장치 + +```python +class CostGuard: + daily_limit_usd: float = 10.0 + per_task_limit_usd: float = 3.0 + + async def check(self, usage: dict) -> bool: + cost = self.calculate_cost(usage) + if self.daily_total + cost > self.daily_limit_usd: + await discord.send("일일 API 비용 한도 도달. 작업 일시 중단.") + return False + return True +``` + +--- + +## 11. 구현 로드맵 + +### Phase 1: 프로젝트 기반 구축 + +open-swe를 포크하여 galaxis-agent로 변환. 빌드 가능한 상태까지. + +| 작업 | 상세 | +|------|------| +| open-swe 포크 | Gitea에 `quant/galaxis-agent` 리포 생성 | +| 불필요 코드 제거 | Linear, Slack, GitHub 전용 코드, 클라우드 샌드박스 삭제 | +| 의존성 정리 | `pyproject.toml`에서 불필요 패키지 제거 | +| ARM64 호환성 검증 | `deepagents`, `langgraph` 패키지 ARM64 설치 확인 | +| LangGraph Server 검증 | ARM64에서 `langchain/langgraph-api` 이미지 실행 확인. 실패 시 자체 상태 관리로 전환 계획 수립 | +| Dockerfile 작성 | 에이전트 서버용 + 샌드박스용 (ARM64) | +| docker-compose.yml | 에이전트 서버 + LangGraph + docker-socket-proxy + 네트워크 | +| config.py | 환경변수 관리 클래스 | +| **검증** | `docker compose build` 성공, 서버 기동 확인 | + +### Phase 2: 핵심 기능 구현 + +로컬에서 에이전트가 코드를 수정하고 PR을 생성할 수 있는 상태. + +| 작업 | 상세 | +|------|------| +| DockerSandbox | `SandboxBackendProtocol` 구현 (docker-socket-proxy 경유) | +| GiteaClient | PR 생성, 코멘트, 브랜치 관리 API 클라이언트 | +| commit_and_open_pr 포팅 | GitHub → Gitea API로 변경 | +| gitea_comment 도구 | 이슈/PR 코멘트 작성 도구 | +| Git credential helper | 안전한 인증 설정/정리 구현 | +| 프롬프트 로딩 파이프라인 | AGENTS.md + CLAUDE.md 로딩 | +| 자율도 설정 | conservative 모드, 경로 접근 제어 | +| 테스트 DB 설정 | galaxis_test DB + 샌드박스 환경변수 연동 | +| 단위 테스트 | 각 컴포넌트 테스트 작성 | +| **검증** | CLI에서 수동 에이전트 실행 → Gitea PR 생성 확인 | + +### Phase 3: 외부 연동 + +Gitea webhook과 Discord에서 자동으로 에이전트가 트리거되는 상태. + +| 작업 | 상세 | +|------|------| +| Gitea webhook | 서명 검증, 이벤트 파싱, rate limiting, 에이전트 디스패치 | +| DiscordHandler | bot gateway 수신 (MESSAGE_CONTENT intent), 멘션 파싱, 에이전트 연동 | +| discord_reply 도구 | 진행 알림, 결과 보고 메시지 전송 | +| FastAPI + discord.py 공존 | lifespan으로 동시 실행 | +| 메시지 큐잉 | 작업 중 추가 메시지 처리 | +| PersistentTaskQueue | SQLite 기반 영속 작업 큐 | +| 통합 테스트 | webhook → 작업 → PR → 알림 E2E 흐름 | +| **검증** | Gitea 이슈에 `@agent` → PR 생성 + Discord 알림 확인 | + +### Phase 4: 안정화 & 자율 모드 + +프로덕션 안정성 확보 + autonomous 모드 전환 준비. + +| 작업 | 상세 | +|------|------| +| CostGuard | 일일/작업당 API 비용 제한 | +| 복구 메커니즘 | 서버 재시작 시 미완료 작업 복구 | +| 좀비 컨테이너 정리 | 주기적 정리 크론 | +| 헬스 체크 엔드포인트 | `/health`, `/health/gitea`, `/health/discord` | +| 작업 이력 DB | SQLite에 작업 상태/비용/소요시간 기록 | +| 구조화 로깅 | JSON 포맷 로그 | +| 자동 머지 모드 | autonomous 설정 + E2E 통과 조건 | +| 스모크 테스트 | 배포 후 자동 검증 | +| **검증** | 1주일 운영 후 안정성 확인 → autonomous 전환 판단 | + +### 의존성 관계 + +``` +Phase 1 ──▶ Phase 2 ──▶ Phase 3 ──▶ Phase 4 +(기반) (핵심) (연동) (안정화) +``` + +Phase 2 완료 후 CLI로 수동 테스트 가능. Phase 3부터 자동 트리거 가능.