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).
36 KiB
galaxis-agent: 자율 개발 에이전트 설계 문서
개요
galaxis-po 코드베이스를 자율적으로 개발하는 SWE 에이전트. 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)│ │ (알림/대화) │
└───────────┘ └────────────┘
핵심 흐름
- Gitea 이슈에
@agent멘션 또는 Discord에서 작업 요청 - galaxis-agent가 webhook/메시지 수신
- 결정론적 스레드 ID 생성 (이슈 ID 기반)
- Docker 컨테이너 생성, 레포 클론
- Claude API로 코드 분석 → 수정 → 테스트
- Gitea에 PR 생성 + Discord 알림
- (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 패키지로 셀프 호스팅 가능하다.
# 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는 포함하지 않는다.
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 구현
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) |
코드 리뷰 수행 → 코멘트 |
서명 검증:
# 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:
thread_id = hashlib.sha256(f"gitea-issue:{repo}:{issue_id}".encode()).hexdigest()
# → UUID 포맷으로 변환
Rate limiting:
# 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 클라이언트
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에 토큰을 노출하지 않는다.
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 — 서버 메시지 수신
intents = discord.Intents.default()
intents.message_content = True
intents.guild_messages = True
bot = commands.Bot(command_prefix="!", intents=intents)
Bot Gateway 수신
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 공존
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. 시스템 프롬프트 & 자율도 제어
시스템 프롬프트 구성
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를 모두 읽는다:
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에서 로딩된다.
# 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 스키마 변경 시 필수)
자율도 제어 시스템
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로 API 접근을 제한한다.
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헤더)
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
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)
환경변수
# 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에 영속적으로 저장하여 서버 재시작 시에도 유실되지 않도록 한다.
# 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초과 컨테이너 제거
배포 절차
# 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 기록.
복구 메커니즘
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 # 컨테이너 생성 → 명령 실행 → 정리
스모크 테스트 (배포 후 검증):
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 비용 안전장치
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부터 자동 트리거 가능.