From 149560c0837cbf3ead9ead81336eb7e5b299da89 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:28:14 +0900 Subject: [PATCH] docs: add Phase 1 implementation plan for galaxis-agent 17-task plan covering: open-swe fork setup, code cleanup, git_utils extraction from github.py, config module, Docker sandbox/compose setup, ARM64 compatibility validation. Two review iterations applied. --- .../plans/2026-03-20-galaxis-agent-phase1.md | 1414 +++++++++++++++++ 1 file changed, 1414 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-20-galaxis-agent-phase1.md diff --git a/docs/superpowers/plans/2026-03-20-galaxis-agent-phase1.md b/docs/superpowers/plans/2026-03-20-galaxis-agent-phase1.md new file mode 100644 index 0000000..e36f181 --- /dev/null +++ b/docs/superpowers/plans/2026-03-20-galaxis-agent-phase1.md @@ -0,0 +1,1414 @@ +# galaxis-agent Phase 1: 프로젝트 기반 구축 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** open-swe를 포크하여 galaxis-agent로 변환하고, ARM64에서 빌드/기동 가능한 상태까지 구축한다. + +**Architecture:** open-swe 코드베이스에서 불필요한 플랫폼 코드(Linear, Slack, GitHub, 클라우드 샌드박스)를 제거하고, Gitea/Discord용 스텁과 Docker 샌드박스 기반 인프라를 준비한다. LangGraph Server와 docker-socket-proxy를 포함한 docker-compose 환경을 구성한다. + +**Tech Stack:** Python 3.12, FastAPI, LangGraph, Deep Agents, docker-py, Docker Compose, ARM64 (Oracle A1) + +**Spec:** `docs/superpowers/specs/2026-03-20-galaxis-agent-design.md` + +--- + +## File Structure + +### 삭제할 파일 (open-swe에서 제거) + +``` +agent/tools/linear_comment.py # Linear 미사용 +agent/tools/slack_thread_reply.py # Slack 미사용 +agent/tools/github_comment.py # GitHub → Gitea로 교체 예정 +agent/integrations/modal.py # 클라우드 샌드박스 미사용 +agent/integrations/daytona.py # 클라우드 샌드박스 미사용 +agent/integrations/runloop.py # 클라우드 샌드박스 미사용 +agent/integrations/langsmith.py # LangSmith → Docker로 교체 예정 +agent/utils/linear.py # Linear 미사용 +agent/utils/linear_team_repo_map.py # Linear 미사용 +agent/utils/slack.py # Slack 미사용 +agent/utils/github_app.py # GitHub App 미사용 +agent/utils/github_token.py # GitHub 전용 +agent/utils/github_comments.py # GitHub 전용 +agent/utils/github_user_email_map.py # GitHub 전용 +agent/utils/langsmith.py # LangSmith 전용 +agent/integrations/local.py # 로컬 샌드박스 미사용 +tests/test_github_comment_prompts.py # GitHub 전용 테스트 +tests/test_slack_context.py # Slack 전용 테스트 +tests/test_github_issue_webhook.py # GitHub 전용 테스트 +tests/test_auth_sources.py # GitHub OAuth 전용 +tests/test_recent_comments.py # GitHub 전용 +``` + +### 수정할 파일 + +``` +pyproject.toml # 프로젝트명, 의존성 정리 +agent/server.py # GitHub/Linear/Slack 참조 제거, 스텁 교체 +agent/webapp.py # GitHub/Linear/Slack webhook 제거, Gitea 스텁 +agent/tools/__init__.py # 삭제된 도구 참조 제거 +agent/tools/commit_and_open_pr.py # GitHub 참조 제거, Gitea 스텁 +agent/prompt.py # GitHub 전용 프롬프트 섹션 정리 +agent/utils/sandbox.py # 클라우드 프로바이더 제거, Docker 스텁 +agent/utils/auth.py # GitHub OAuth 제거, Gitea 토큰 스텁 +agent/utils/github.py # git_utils.py로 이름 변경 (순수 git 유틸 보존) +agent/encryption.py # TOKEN_ENCRYPTION_KEY → FERNET_KEY 통일 +agent/integrations/__init__.py # 클라우드 프로바이더 import 제거 +agent/middleware/open_pr.py # GitHub 참조 제거, git_utils import로 변경 +langgraph.json # 프로젝트명 변경 +Makefile # 프로젝트명 변경 +``` + +### 신규 생성할 파일 + +``` +agent/config.py # 환경변수 관리 (pydantic-settings) +agent/integrations/docker_sandbox.py # DockerSandbox 스텁 (Phase 2에서 구현) +agent/utils/gitea_client.py # GiteaClient 스텁 (Phase 2에서 구현) +agent/utils/discord_client.py # DiscordClient 스텁 (Phase 2에서 구현) +agent/tools/gitea_comment.py # gitea_comment 스텁 (Phase 2에서 구현) +agent/tools/discord_reply.py # discord_reply 스텁 (Phase 2에서 구현) +agent/utils/git_utils.py # github.py에서 순수 git 유틸리티 분리 +Dockerfile # 에이전트 서버용 (ARM64, 기존 덮어쓰기) +Dockerfile.sandbox # 작업 컨테이너용 (ARM64) +docker-compose.yml # 전체 서비스 구성 +.env.example # 환경변수 템플릿 +tests/test_config.py # config 테스트 +``` + +--- + +## Task 1: Gitea에 리포 생성 및 open-swe 코드 복사 + +**Files:** +- Create: 전체 프로젝트 루트 (Gitea `quant/galaxis-agent`) + +- [ ] **Step 1: Gitea에 빈 리포 생성** + +Gitea 웹 UI에서 `quant/galaxis-agent` 리포지토리를 생성한다 (Initialize 체크 해제). + +- [ ] **Step 2: 로컬에 galaxis-agent 디렉토리 생성** + +```bash +mkdir -p ~/workspace/quant/galaxis-agent +cd ~/workspace/quant/galaxis-agent +git init +``` + +- [ ] **Step 3: open-swe 코드 복사 (git 히스토리 제외)** + +```bash +cp -r ~/workspace/etc/open-swe/agent ./agent +cp -r ~/workspace/etc/open-swe/tests ./tests +cp ~/workspace/etc/open-swe/pyproject.toml . +cp ~/workspace/etc/open-swe/langgraph.json . +cp ~/workspace/etc/open-swe/Makefile . +cp ~/workspace/etc/open-swe/Dockerfile . +cp ~/workspace/etc/open-swe/README.md . +cp ~/workspace/etc/open-swe/LICENSE . +cp ~/workspace/etc/open-swe/.gitignore . 2>/dev/null || true +``` + +- [ ] **Step 4: 초기 커밋 및 Gitea에 푸시** + +```bash +git add -A +git commit -m "chore: initial copy from open-swe" +git remote add origin https://ayuriel.duckdns.org/quant/galaxis-agent.git +git push -u origin master +``` + +--- + +## Task 2: 불필요한 파일 삭제 + +**Files:** +- Delete: 위 "삭제할 파일" 목록 전체 + +- [ ] **Step 1: Linear, Slack, GitHub 전용 코드 삭제** + +```bash +cd ~/workspace/quant/galaxis-agent + +# 도구 +rm -f agent/tools/linear_comment.py +rm -f agent/tools/slack_thread_reply.py +rm -f agent/tools/github_comment.py + +# 통합 (클라우드 샌드박스) +rm -f agent/integrations/modal.py +rm -f agent/integrations/daytona.py +rm -f agent/integrations/runloop.py +rm -f agent/integrations/langsmith.py + +# 유틸리티 +rm -f agent/utils/linear.py +rm -f agent/utils/linear_team_repo_map.py +rm -f agent/utils/slack.py +rm -f agent/utils/github.py +rm -f agent/utils/github_app.py +rm -f agent/utils/github_token.py +rm -f agent/utils/github_comments.py +rm -f agent/utils/github_user_email_map.py +rm -f agent/utils/langsmith.py +rm -f agent/integrations/local.py + +# GitHub 전용 테스트 +rm -f tests/test_github_comment_prompts.py +rm -f tests/test_slack_context.py +rm -f tests/test_github_issue_webhook.py +rm -f tests/test_auth_sources.py +rm -f tests/test_recent_comments.py +``` + +- [ ] **Step 2: 삭제 확인** + +```bash +# 삭제된 파일이 없는지 확인 +ls agent/tools/linear_comment.py 2>&1 | grep "No such file" +ls agent/integrations/modal.py 2>&1 | grep "No such file" +ls agent/utils/linear.py 2>&1 | grep "No such file" +``` + +- [ ] **Step 3: 커밋** + +```bash +git add -A +git commit -m "chore: remove Linear, Slack, GitHub, and cloud sandbox code" +``` + +--- + +## Task 3: github.py에서 순수 git 유틸리티 분리 + +`agent/utils/github.py`에는 GitHub API 함수와 순수 git 유틸리티(git_add, git_commit, git_push 등)가 혼재되어 있다. `commit_and_open_pr.py`와 `open_pr.py`가 git 유틸리티를 사용하므로, GitHub API 코드만 제거하고 git 유틸리티는 `git_utils.py`로 보존한다. + +**Files:** +- Create: `agent/utils/git_utils.py` +- Delete: `agent/utils/github.py` + +- [ ] **Step 1: github.py에서 순수 git 유틸리티 함수 식별** + +원본 `agent/utils/github.py`를 읽고 다음 두 그룹으로 분류한다: + +**보존 (git_utils.py로 이동):** +- `git_add_all()` - git add +- `git_commit()` - git commit +- `git_push()` - git push +- `git_checkout_branch()` - 브랜치 체크아웃/생성 +- `git_diff_stat()` - 변경사항 확인 +- `git_has_uncommitted_changes()` - 미커밋 변경 확인 +- `git_has_unpushed_commits()` - 미푸시 커밋 확인 +- `git_set_user_config()` - git user 설정 +- `get_default_branch()` - 기본 브랜치 조회 (git 명령 기반) + +**삭제 (GitHub API 전용):** +- `create_github_pr()` - PyGithub로 PR 생성 +- `get_github_default_branch()` - GitHub API로 기본 브랜치 조회 +- GitHub App 토큰 관련 함수 + +- [ ] **Step 2: git_utils.py 생성** + +원본 `github.py`에서 순수 git 유틸리티 함수만 복사하여 `agent/utils/git_utils.py`를 생성한다. import를 정리하고 GitHub API 관련 import를 제거한다. + +```python +# agent/utils/git_utils.py +"""순수 git 명령 유틸리티. + +agent/utils/github.py에서 GitHub API 의존성을 제거하고 +git CLI 기반 유틸리티만 보존한 모듈. +""" +# 원본 github.py에서 git 관련 함수만 복사 +# sandbox.execute()를 통해 git 명령을 실행하는 함수들 +``` + +실제 구현은 원본 파일을 읽고 해당 함수들을 그대로 옮긴다. + +- [ ] **Step 3: github.py 삭제** + +```bash +rm -f agent/utils/github.py +``` + +- [ ] **Step 4: 커밋** + +```bash +git add agent/utils/git_utils.py +git rm agent/utils/github.py +git commit -m "refactor: extract git utilities from github.py into git_utils.py" +``` + +--- + +## Task 4: pyproject.toml 의존성 정리 + +**Files:** +- Modify: `pyproject.toml` + +- [ ] **Step 1: pyproject.toml 수정** + +```toml +[project] +name = "galaxis-agent" +version = "0.1.0" +description = "Autonomous SWE agent for galaxis-po development" +readme = "README.md" +requires-python = ">=3.12" +license = { text = "MIT" } +dependencies = [ + "deepagents>=0.4.3", + "fastapi>=0.104.0", + "uvicorn>=0.24.0", + "httpx>=0.25.0", + "cryptography>=41.0.0", + "langgraph-sdk>=0.1.0", + "langchain>=1.2.9", + "langgraph>=1.0.8", + "langgraph-cli[inmem]>=0.4.12", + "langchain-anthropic>1.1.0", + "markdownify>=1.2.2", + "docker>=7.0.0", + "pydantic-settings>=2.0.0", + "slowapi>=0.1.9", + "discord.py>=2.3.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "ruff>=0.1.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["agent"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "B", "C4", "UP"] +ignore = ["E501"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +``` + +변경 사항: +- `name`: `open-swe-agent` → `galaxis-agent` +- `requires-python`: `>=3.11` → `>=3.12` +- `target-version`: `py311` → `py312` +- 삭제: `PyJWT`, `langsmith`, `langchain-openai`, `langchain-daytona`, `langchain-modal`, `langchain-runloop` +- 추가: `docker`, `pydantic-settings`, `slowapi`, `discord.py` + +- [ ] **Step 2: 의존성 설치 테스트** + +```bash +uv sync +``` + +Expected: 모든 의존성 설치 성공 (ARM64 호환성 확인) + +- [ ] **Step 3: 커밋** + +```bash +git add pyproject.toml uv.lock +git commit -m "chore: update dependencies for galaxis-agent" +``` + +--- + +## Task 5: 환경변수 설정 모듈 생성 + +**Files:** +- Create: `agent/config.py` +- Create: `.env.example` +- Create: `tests/test_config.py` + +- [ ] **Step 1: 테스트 작성** + +```python +# tests/test_config.py +import pytest +from agent.config import Settings + + +@pytest.fixture +def test_settings(monkeypatch): + """테스트용 환경변수로 Settings 인스턴스 생성""" + monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") + monkeypatch.setenv("GITEA_TOKEN", "test-token") + monkeypatch.setenv("GITEA_WEBHOOK_SECRET", "test-secret") + monkeypatch.setenv("DISCORD_TOKEN", "test-token") + monkeypatch.setenv("FERNET_KEY", "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXg=") + return Settings() + + +def test_config_loads_defaults(test_settings): + """기본값으로 설정이 로딩되는지 확인""" + assert test_settings.GITEA_URL == "http://gitea:3000" + assert test_settings.AUTONOMY_LEVEL == "conservative" + assert test_settings.SANDBOX_TIMEOUT == 600 + assert test_settings.DEFAULT_REPO_OWNER == "quant" + assert test_settings.DEFAULT_REPO_NAME == "galaxis-po" + assert test_settings.SANDBOX_MEM_LIMIT == "4g" + assert test_settings.SANDBOX_CPU_COUNT == 2 + + +def test_config_autonomy_levels(test_settings): + """자율도 레벨 검증""" + assert test_settings.AUTONOMY_LEVEL in ("conservative", "autonomous") + + +def test_writable_paths_include_backend(test_settings): + """writable_paths에 백엔드 경로 포함 확인""" + assert "backend/app/" in test_settings.WRITABLE_PATHS + assert "backend/tests/" in test_settings.WRITABLE_PATHS + assert "backend/alembic/versions/" in test_settings.WRITABLE_PATHS + + +def test_blocked_paths_include_protected_files(test_settings): + """blocked_paths에 보호 파일 포함 확인""" + assert ".env" in test_settings.BLOCKED_PATHS + assert "quant.md" in test_settings.BLOCKED_PATHS + assert "docker-compose.prod.yml" in test_settings.BLOCKED_PATHS +``` + +- [ ] **Step 2: 테스트 실행 — 실패 확인** + +```bash +uv run pytest tests/test_config.py -v +``` + +Expected: FAIL — `agent.config` 모듈 없음 + +- [ ] **Step 3: config.py 구현** + +```python +# agent/config.py +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """galaxis-agent 환경변수 설정""" + + # LLM + ANTHROPIC_API_KEY: str + + # Gitea + GITEA_URL: str = "http://gitea:3000" + GITEA_EXTERNAL_URL: str = "https://ayuriel.duckdns.org" + GITEA_TOKEN: str + GITEA_WEBHOOK_SECRET: str + + # Discord + DISCORD_TOKEN: str + DISCORD_CHANNEL_ID: str = "" + DISCORD_BOT_USER_ID: str = "" + + # LangGraph + LANGGRAPH_URL: str = "http://langgraph-server:8123" + + # Agent + AUTONOMY_LEVEL: str = "conservative" + DEFAULT_REPO_OWNER: str = "quant" + DEFAULT_REPO_NAME: str = "galaxis-po" + AGENT_API_KEY: str = "" + + # Sandbox + SANDBOX_IMAGE: str = "galaxis-sandbox:latest" + SANDBOX_MEM_LIMIT: str = "4g" + SANDBOX_CPU_COUNT: int = 2 + SANDBOX_TIMEOUT: int = 600 + SANDBOX_PIDS_LIMIT: int = 256 + + # Database (테스트용) + TEST_DATABASE_URL: str = "" + + # 암호화 + FERNET_KEY: str = "" + + # 경로 접근 제어 + WRITABLE_PATHS: list[str] = [ + "backend/app/", + "backend/tests/", + "frontend/src/", + "backend/alembic/versions/", + "docs/", + ] + BLOCKED_PATHS: list[str] = [ + ".env", + "docker-compose.prod.yml", + "quant.md", + ] + + # 자동 머지 + AUTO_MERGE: bool = False + REQUIRE_TESTS: bool = True + REQUIRE_E2E: bool = False + MAX_FILES_CHANGED: int = 10 + + # 비용 제한 + DAILY_COST_LIMIT_USD: float = 10.0 + PER_TASK_COST_LIMIT_USD: float = 3.0 + + model_config = {"env_file": ".env", "extra": "ignore"} + + +settings = Settings() +``` + +- [ ] **Step 4: 테스트 실행 — 성공 확인** + +```bash +uv run pytest tests/test_config.py -v +``` + +Expected: 4 passed + +- [ ] **Step 5: .env.example 생성** + +```bash +# .env.example + +# 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= + +# 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= +``` + +- [ ] **Step 6: 커밋** + +```bash +git add agent/config.py tests/test_config.py .env.example +git commit -m "feat: add config module with pydantic-settings" +``` + +--- + +## Task 6: 스텁 파일 생성 (Phase 2 준비) + +**Files:** +- Create: `agent/integrations/docker_sandbox.py` +- Create: `agent/utils/gitea_client.py` +- Create: `agent/utils/discord_client.py` +- Create: `agent/tools/gitea_comment.py` +- Create: `agent/tools/discord_reply.py` + +- [ ] **Step 1: DockerSandbox 스텁** + +```python +# agent/integrations/docker_sandbox.py +"""Docker 컨테이너 기반 샌드박스 백엔드. + +SandboxBackendProtocol을 구현하여 docker-py로 격리된 실행 환경을 제공한다. +Phase 2에서 구현 예정. +""" + + +class DockerSandbox: + """docker-py 기반 샌드박스 백엔드 (Phase 2 구현 예정)""" + + async def execute(self, command: str, timeout: int = 300): + raise NotImplementedError("Phase 2에서 구현") + + async def read_file(self, path: str) -> str: + raise NotImplementedError("Phase 2에서 구현") + + async def write_file(self, path: str, content: str) -> None: + raise NotImplementedError("Phase 2에서 구현") + + async def close(self) -> None: + raise NotImplementedError("Phase 2에서 구현") +``` + +- [ ] **Step 2: GiteaClient 스텁** + +```python +# agent/utils/gitea_client.py +"""Gitea REST API v1 클라이언트. + +Phase 2에서 구현 예정. +""" + +import httpx + + +class GiteaClient: + """Gitea REST API 클라이언트 (Phase 2 구현 예정)""" + + def __init__(self, base_url: str, token: str): + self.base_url = base_url.rstrip("/") + self.token = token + self._client = httpx.AsyncClient( + base_url=f"{self.base_url}/api/v1", + headers={"Authorization": f"token {self.token}"}, + ) + + async def create_pull_request( + self, owner: str, repo: str, title: str, head: str, base: str, body: str + ) -> dict: + raise NotImplementedError("Phase 2에서 구현") + + async def merge_pull_request( + self, owner: str, repo: str, pr_number: int, merge_type: str = "merge" + ) -> dict: + raise NotImplementedError("Phase 2에서 구현") + + async def create_issue_comment( + self, owner: str, repo: str, issue_number: int, body: str + ) -> dict: + raise NotImplementedError("Phase 2에서 구현") + + async def get_issue(self, owner: str, repo: str, issue_number: int) -> dict: + raise NotImplementedError("Phase 2에서 구현") + + async def get_issue_comments( + self, owner: str, repo: str, issue_number: int + ) -> list: + raise NotImplementedError("Phase 2에서 구현") + + async def create_branch( + self, owner: str, repo: str, branch_name: str, old_branch: str + ) -> dict: + raise NotImplementedError("Phase 2에서 구현") + + async def close(self): + await self._client.aclose() +``` + +- [ ] **Step 3: DiscordClient 스텁** + +```python +# agent/utils/discord_client.py +"""Discord bot 연동 클라이언트. + +Phase 2에서 구현 예정. +""" + + +class DiscordClient: + """Discord 메시지 수신/발송 (Phase 2 구현 예정)""" + + async def send_message(self, channel_id: str, content: str) -> dict: + raise NotImplementedError("Phase 2에서 구현") + + async def send_thread_reply( + self, channel_id: str, thread_id: str, content: str + ) -> dict: + raise NotImplementedError("Phase 2에서 구현") +``` + +- [ ] **Step 4: gitea_comment 도구 스텁** + +```python +# agent/tools/gitea_comment.py +"""Gitea 이슈/PR 코멘트 작성 도구. + +Phase 2에서 구현 예정. +""" + + +def gitea_comment(message: str, issue_number: int) -> dict: + """Gitea 이슈 또는 PR에 코멘트를 작성한다.""" + raise NotImplementedError("Phase 2에서 구현") +``` + +- [ ] **Step 5: discord_reply 도구 스텁** + +```python +# agent/tools/discord_reply.py +"""Discord 채널/스레드 메시지 전송 도구. + +Phase 2에서 구현 예정. +""" + + +def discord_reply(message: str) -> dict: + """Discord 채널 또는 스레드에 메시지를 전송한다.""" + raise NotImplementedError("Phase 2에서 구현") +``` + +- [ ] **Step 6: 커밋** + +```bash +git add agent/integrations/docker_sandbox.py agent/utils/gitea_client.py \ + agent/utils/discord_client.py agent/tools/gitea_comment.py agent/tools/discord_reply.py +git commit -m "feat: add stub modules for Phase 2 (Docker, Gitea, Discord)" +``` + +--- + +## Task 7: 기존 파일 수정 — import 및 참조 정리 + +**Files:** +- Modify: `agent/tools/__init__.py` +- Modify: `agent/integrations/__init__.py` +- Modify: `agent/utils/sandbox.py` +- Modify: `agent/utils/auth.py` +- Modify: `langgraph.json` +- Modify: `Makefile` + +- [ ] **Step 1: agent/tools/__init__.py 수정** + +삭제된 도구 import를 제거하고 새 스텁 도구를 추가한다. + +원본에서 `linear_comment`, `slack_thread_reply`, `github_comment`를 제거하고 `gitea_comment`, `discord_reply`를 추가: + +```python +# agent/tools/__init__.py +from agent.tools.commit_and_open_pr import commit_and_open_pr +from agent.tools.discord_reply import discord_reply +from agent.tools.fetch_url import fetch_url +from agent.tools.gitea_comment import gitea_comment +from agent.tools.http_request import http_request + +__all__ = [ + "commit_and_open_pr", + "discord_reply", + "fetch_url", + "gitea_comment", + "http_request", +] +``` + +- [ ] **Step 2: agent/integrations/__init__.py 수정** + +클라우드 프로바이더 import를 제거하고 Docker 스텁만 남긴다. 원본 파일을 읽고, 삭제된 프로바이더 참조를 제거한다. 최종 내용: + +```python +# agent/integrations/__init__.py +from agent.integrations.docker_sandbox import DockerSandbox + +__all__ = ["DockerSandbox"] +``` + +- [ ] **Step 3: agent/utils/sandbox.py 수정** + +클라우드 프로바이더 팩토리를 제거하고 Docker 샌드박스만 사용하도록 변경: + +```python +# agent/utils/sandbox.py +from agent.integrations.docker_sandbox import DockerSandbox + + +def create_sandbox(sandbox_id: str | None = None) -> DockerSandbox: + """Docker 샌드박스를 생성하거나 기존 것에 연결한다.""" + return DockerSandbox() # Phase 2에서 실제 구현 +``` + +- [ ] **Step 4: agent/encryption.py 수정** + +원본은 `TOKEN_ENCRYPTION_KEY` 환경변수를 사용한다. `config.py`의 `FERNET_KEY`로 통일: + +```python +# agent/encryption.py — 수정할 부분 +# 변경 전: key = os.environ.get("TOKEN_ENCRYPTION_KEY") +# 변경 후: +from agent.config import settings + +def _get_key() -> bytes | None: + if settings.FERNET_KEY: + return settings.FERNET_KEY.encode() + return None +``` + +원본 파일을 읽고 `TOKEN_ENCRYPTION_KEY` 참조를 `settings.FERNET_KEY`로 교체한다. + +- [ ] **Step 5: agent/utils/auth.py 수정** + +GitHub OAuth 관련 코드를 제거하고 Gitea 토큰 기반 인증으로 교체. 원본 파일을 읽고 다음으로 교체: + +```python +# agent/utils/auth.py +"""Gitea 토큰 기반 인증. + +GitHub OAuth를 제거하고 Gitea Application Token을 사용한다. +""" +from agent.config import settings +from agent.encryption import encrypt_token, decrypt_token + + +async def get_gitea_token() -> str: + """설정에서 Gitea 토큰을 가져온다.""" + return settings.GITEA_TOKEN + + +async def get_encrypted_gitea_token() -> tuple[str, str]: + """Gitea 토큰과 암호화된 버전을 반환한다.""" + token = settings.GITEA_TOKEN + encrypted = encrypt_token(token) if settings.FERNET_KEY else token + return token, encrypted +``` + +- [ ] **Step 5: langgraph.json 및 Makefile 수정** + +`langgraph.json`: +```json +{ + "python_version": "3.12", + "dependencies": ["."], + "graphs": { + "agent": "agent.server:get_agent" + }, + "http": { + "app": "agent.webapp:app" + }, + "env": ".env" +} +``` + +`Makefile`: `open-swe` 문자열을 `galaxis-agent`로 변경. + +- [ ] **Step 6: 커밋** + +```bash +git add agent/tools/__init__.py agent/integrations/__init__.py \ + agent/utils/sandbox.py agent/utils/auth.py langgraph.json Makefile +git commit -m "refactor: clean up imports and references after code removal" +``` + +--- + +## Task 8: server.py 정리 — 의존성 교체 (핵심 작업) + +**Files:** +- Modify: `agent/server.py` + +이 파일은 에이전트의 핵심이며 삭제된 모듈에 대한 깊은 의존성이 있다. 신중하게 수정한다. + +### Step 8a: import 정리 + +- [ ] **원본 server.py 읽기** + +전체 파일을 읽고 변경 포인트를 파악한다. + +- [ ] **삭제할 import 제거** + +```python +# 삭제: +from langsmith.sandbox import SandboxClientError # langsmith 패키지 제거됨 +from .tools import linear_comment, slack_thread_reply, github_comment +from .utils.github_token import get_github_token_from_thread +from .utils.github import ... # 모든 GitHub API 함수 +``` + +- [ ] **대체 import 추가** + +```python +# 추가: +from agent.config import settings +from agent.tools import gitea_comment, discord_reply +from agent.utils.auth import get_gitea_token +from agent.utils.git_utils import ( # github.py → git_utils.py + git_checkout_branch, + git_has_uncommitted_changes, + # 등 필요한 git 유틸리티 +) + +# SandboxClientError 대체 (langsmith 제거 후) +class SandboxConnectionError(Exception): + """샌드박스 연결 실패 시 발생하는 예외""" + pass +``` + +### Step 8b: get_agent() 함수 수정 + +- [ ] **도구 목록 변경** + +```python +# 변경 전: +tools=[http_request, fetch_url, commit_and_open_pr, linear_comment, slack_thread_reply, github_comment] + +# 변경 후: +tools=[http_request, fetch_url, commit_and_open_pr, gitea_comment, discord_reply] +``` + +- [ ] **GitHub 토큰 → Gitea 토큰 교체** + +`resolve_github_token()` 호출을 `get_gitea_token()`으로 교체. +`github_token` 변수를 `gitea_token`으로 변경. + +### Step 8c: _clone_or_pull_repo_in_sandbox() 수정 + +- [ ] **GitHub URL → Gitea 내부 URL로 변경** + +```python +# 변경 전: github.com 참조 +# 변경 후: +clone_url = f"http://gitea:3000/{owner}/{repo}.git" +``` + +- [ ] **credential helper 패턴 적용** + +```python +# git credential helper 설정 (URL에 토큰 미노출) +await sandbox.execute( + f'echo "http://agent:{gitea_token}@gitea:3000" > /tmp/.git-credentials' +) +await sandbox.execute( + "git config --global credential.helper 'store --file=/tmp/.git-credentials'" +) +``` + +### Step 8d: sandbox_state 의존성 처리 + +- [ ] **sandbox_state.py 확인** + +`agent/utils/sandbox_state.py`를 읽고 `langgraph_sdk` 의존성을 확인한다. +이 파일은 `SANDBOX_BACKENDS` 딕셔너리와 스레드 메타데이터 접근을 제공한다. +`langgraph_sdk.get_client()` 호출 부분이 있으면 Phase 2에서 구현할 LangGraph/자체 상태 관리로 대체할 스텁을 준비한다. + +- [ ] **SandboxClientError → SandboxConnectionError 교체** + +`server.py`에서 `SandboxClientError` catch를 `SandboxConnectionError` (또는 일반 `Exception`)로 교체. + +### Step 8e: 컴파일 검증 + +- [ ] **import 확인** + +```bash +uv run python -c "import agent.server; print('OK')" +``` + +Expected: import 성공. `langgraph_sdk` 관련 런타임 에러는 허용 (서버 기동 시에만 발생). + +- [ ] **커밋** + +```bash +git add agent/server.py +git commit -m "refactor: replace GitHub/Linear/Slack with Gitea/Discord in server.py" +``` + +--- + +## Task 9: webapp.py 정리 — Gitea webhook 스텁 + +**Files:** +- Modify: `agent/webapp.py` + +- [ ] **Step 1: 원본 webapp.py 읽기** + +전체 파일을 읽고 변경 포인트를 파악한다. + +- [ ] **Step 2: GitHub/Linear/Slack webhook 핸들러 제거, Gitea 스텁으로 교체** + +최소한의 FastAPI 앱으로 재작성: + +```python +# agent/webapp.py +"""galaxis-agent webhook 서버. + +Gitea webhook과 Discord bot 이벤트를 수신한다. +""" +import hashlib +import hmac +import logging + +from fastapi import FastAPI, Request, HTTPException + +from agent.config import settings + +logger = logging.getLogger(__name__) + +app = FastAPI(title="galaxis-agent") + + +def verify_gitea_signature(payload: bytes, signature: str, secret: str) -> bool: + """Gitea webhook HMAC-SHA256 서명을 검증한다.""" + expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() + return hmac.compare_digest(expected, signature) + + +def generate_thread_id(repo: str, issue_id: int) -> str: + """이슈 기반 결정론적 스레드 ID를 생성한다.""" + raw = hashlib.sha256(f"gitea-issue:{repo}:{issue_id}".encode()).hexdigest() + # UUID 포맷으로 변환 + return f"{raw[:8]}-{raw[8:12]}-{raw[12:16]}-{raw[16:20]}-{raw[20:32]}" + + +@app.get("/health") +async def health(): + return {"status": "ok"} + + +@app.post("/webhooks/gitea") +async def gitea_webhook(request: Request): + """Gitea webhook 수신 엔드포인트. Phase 3에서 완전 구현.""" + body = await request.body() + signature = request.headers.get("X-Gitea-Signature", "") + + if not verify_gitea_signature(body, signature, settings.GITEA_WEBHOOK_SECRET): + raise HTTPException(status_code=401, detail="Invalid signature") + + # Phase 3에서 구현: 이벤트 파싱, 에이전트 디스패치 + logger.info("Gitea webhook received (not yet implemented)") + return {"status": "received"} +``` + +- [ ] **Step 3: 컴파일 확인** + +```bash +uv run python -c "from agent.webapp import app; print(app.title)" +``` + +Expected: `galaxis-agent` + +- [ ] **Step 4: 커밋** + +```bash +git add agent/webapp.py +git commit -m "refactor: replace GitHub/Linear/Slack webhooks with Gitea stub" +``` + +--- + +## Task 10: prompt.py 정리 + +**Files:** +- Modify: `agent/prompt.py` + +- [ ] **Step 1: 원본 prompt.py 읽기** + +전체 파일을 읽고 GitHub 전용 섹션을 파악한다. + +- [ ] **Step 2: GitHub 전용 프롬프트 섹션 수정** + +- `EXTERNAL_UNTRUSTED_COMMENTS_SECTION`: "GitHub" → "Gitea" 참조 변경 +- `COMMIT_PR_SECTION`: "GitHub" PR 참조 → "Gitea" PR로 변경 +- `TOOL_USAGE_SECTION`: `github_comment` → `gitea_comment`, `slack_thread_reply` → `discord_reply` 변경 +- `construct_system_prompt()`: `linear_project_id`, `linear_issue_number` 매개변수 제거 + +- [ ] **Step 3: 컴파일 확인** + +```bash +uv run python -c "from agent.prompt import construct_system_prompt; print('OK')" +``` + +- [ ] **Step 4: 커밋** + +```bash +git add agent/prompt.py +git commit -m "refactor: update system prompt for Gitea/Discord" +``` + +--- + +## Task 11: commit_and_open_pr.py 정리 + +**Files:** +- Modify: `agent/tools/commit_and_open_pr.py` + +- [ ] **Step 1: 원본 읽기** + +전체 파일을 읽고 GitHub API 호출 부분과 git 유틸리티 사용을 파악한다. +이 파일은 `from agent.utils.github import ...`로 12개 함수를 import한다. + +- [ ] **Step 2: import를 git_utils.py로 변경 + GitHub API를 Gitea 스텁으로 교체** + +```python +# 변경 전: +from agent.utils.github import git_add_all, git_commit, git_push, ... +from agent.utils.github import create_github_pr, get_github_default_branch + +# 변경 후: +from agent.utils.git_utils import git_add_all, git_commit, git_push, ... +from agent.utils.gitea_client import GiteaClient # Phase 2에서 구현 +``` + +- `PyGithub` import 제거 +- `create_github_pr()` → `GiteaClient.create_pull_request()` 스텁 호출로 교체 +- git push URL에서 `github.com` → Gitea 내부 URL 변경 +- git user 설정: `open-swe[bot]` → `galaxis-agent[bot]` +- 브랜치 패턴: `open-swe/{thread_id}` → `galaxis-agent/{thread_id}` + +- [ ] **Step 3: 컴파일 확인** + +```bash +uv run python -c "from agent.tools.commit_and_open_pr import commit_and_open_pr; print('OK')" +``` + +- [ ] **Step 4: 커밋** + +```bash +git add agent/tools/commit_and_open_pr.py +git commit -m "refactor: replace GitHub with Gitea in commit_and_open_pr" +``` + +--- + +## Task 12: middleware/open_pr.py 정리 + +**Files:** +- Modify: `agent/middleware/open_pr.py` + +- [ ] **Step 1: 원본 읽기 및 의존성 파악** + +이 파일도 `from agent.utils.github import ...`로 다수의 git 유틸리티를 import한다. + +- [ ] **Step 2: import를 git_utils.py로 변경 + GitHub API 제거** + +```python +# 변경 전: +from agent.utils.github import git_add_all, git_commit, git_push, create_github_pr, ... + +# 변경 후: +from agent.utils.git_utils import git_add_all, git_commit, git_push, ... +from agent.utils.gitea_client import GiteaClient # Phase 2에서 구현 +``` + +- `create_github_pr()` 호출 → `GiteaClient.create_pull_request()` 스텁으로 교체 +- 브랜치 패턴: `open-swe/` → `galaxis-agent/` + +- [ ] **Step 2: 커밋** + +```bash +git add agent/middleware/open_pr.py +git commit -m "refactor: update open_pr middleware for Gitea" +``` + +--- + +## Task 13: 기존 테스트 정리 및 수정 + +**Files:** +- Modify: `tests/test_ensure_no_empty_msg.py` +- Modify: `tests/test_multimodal.py` +- Modify: `tests/test_sandbox_paths.py` + +- [ ] **Step 1: 남아있는 테스트 파일 확인** + +```bash +ls tests/ +``` + +- [ ] **Step 2: 각 테스트 파일에서 삭제된 모듈 import 수정** + +GitHub/Linear/Slack 관련 import가 있으면 제거하거나 Gitea/Discord로 교체. + +- [ ] **Step 3: 테스트 실행** + +```bash +uv run pytest tests/ -v +``` + +Expected: 남은 테스트 전부 통과 (또는 Phase 2 의존성으로 인한 skip) + +- [ ] **Step 4: 커밋** + +```bash +git add tests/ +git commit -m "test: fix remaining tests after code cleanup" +``` + +--- + +## Task 14: Dockerfile 작성 (에이전트 서버 + 샌드박스) + +**Files:** +- Create: `Dockerfile` (덮어쓰기) +- Create: `Dockerfile.sandbox` + +- [ ] **Step 1: 에이전트 서버 Dockerfile** + +```dockerfile +# Dockerfile - galaxis-agent 서버 +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl && \ + rm -rf /var/lib/apt/lists/* + +# uv 설치 +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" + +WORKDIR /app + +# 의존성 먼저 설치 (캐시 활용) +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev + +# 소스 코드 복사 +COPY agent/ ./agent/ +COPY langgraph.json ./ + +# non-root 사용자 +RUN useradd -m -u 1000 agent +USER agent + +EXPOSE 8000 + +CMD ["uv", "run", "uvicorn", "agent.webapp:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +- [ ] **Step 2: 샌드박스 Dockerfile** + +```dockerfile +# Dockerfile.sandbox - 작업 컨테이너 +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl postgresql-client && \ + rm -rf /var/lib/apt/lists/* + +# uv 설치 +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" + +# Node.js LTS +RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace + +# 컨테이너가 종료되지 않도록 대기 +CMD ["tail", "-f", "/dev/null"] +``` + +- [ ] **Step 3: 빌드 테스트 (로컬 아키텍처)** + +```bash +docker build -t galaxis-agent:latest . +docker build -f Dockerfile.sandbox -t galaxis-sandbox:latest . +``` + +Expected: 두 이미지 모두 빌드 성공 + +- [ ] **Step 4: 커밋** + +```bash +git add Dockerfile Dockerfile.sandbox +git commit -m "feat: add Dockerfiles for agent server and sandbox (ARM64)" +``` + +--- + +## Task 15: docker-compose.yml 작성 + +**Files:** +- Create: `docker-compose.yml` + +- [ ] **Step 1: docker-compose.yml 작성** + +```yaml +# docker-compose.yml - galaxis-agent 전체 서비스 구성 +services: + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:latest + environment: + - CONTAINERS=1 + - POST=1 + - EXEC=1 + - IMAGES=1 + - NETWORKS=0 + - VOLUMES=0 + - SERVICES=0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - galaxis-net + restart: unless-stopped + + galaxis-agent: + build: . + image: galaxis-agent:latest + restart: unless-stopped + user: "1000:1000" + ports: + - "8100:8000" + env_file: .env + environment: + - DOCKER_HOST=tcp://docker-socket-proxy:2375 + volumes: + - uv-cache:/cache/uv + - npm-cache:/cache/npm + - agent-data:/data + networks: + - galaxis-net + depends_on: + - docker-socket-proxy + +networks: + galaxis-net: + external: true + +volumes: + uv-cache: + npm-cache: + agent-data: +``` + +참고: LangGraph Server는 Phase 1에서 ARM64 호환성 확인 후 추가한다. 문제가 있으면 자체 상태 관리로 대체 (스펙 섹션 2 참조). + +- [ ] **Step 2: docker-compose 구문 검증** + +```bash +docker compose config +``` + +Expected: 구문 오류 없음 (네트워크가 없으면 경고 가능) + +- [ ] **Step 3: 커밋** + +```bash +git add docker-compose.yml +git commit -m "feat: add docker-compose with socket proxy and agent server" +``` + +--- + +## Task 16: ARM64 호환성 검증 + +**Files:** 없음 (검증만) + +- [ ] **Step 1: deepagents 패키지 ARM64 설치 확인** + +```bash +uv run python -c "import deepagents; print(deepagents.__version__)" +``` + +Expected: 버전 출력 성공. 실패 시 `langchain`의 `create_react_agent`로 대체 계획 수립. + +- [ ] **Step 2: langgraph 패키지 ARM64 설치 확인** + +```bash +uv run python -c "import langgraph; print(langgraph.__version__)" +``` + +Expected: 버전 출력 성공. + +- [ ] **Step 3: docker-py ARM64 설치 확인** + +```bash +uv run python -c "import docker; print(docker.__version__)" +``` + +Expected: 버전 출력 성공. + +- [ ] **Step 4: discord.py ARM64 설치 확인** + +```bash +uv run python -c "import discord; print(discord.__version__)" +``` + +Expected: 버전 출력 성공. + +- [ ] **Step 5: LangGraph Server ARM64 Docker 이미지 확인** + +```bash +docker pull langchain/langgraph-api:latest +docker run --rm langchain/langgraph-api:latest echo "OK" +``` + +Expected: 이미지 pull 및 실행 성공. ARM64 이미지가 없으면 스펙 섹션 2의 대안(SQLite 기반 자체 상태 관리)으로 전환 결정을 기록한다. + +- [ ] **Step 6: 결과 기록** + +모든 패키지 호환 시: Phase 2로 진행. +일부 실패 시: 대안 패키지로 교체하고 영향받는 코드 수정 계획 수립. + +--- + +## Task 17: 전체 빌드 & 기동 검증 + +**Files:** 없음 (검증만) + +- [ ] **Step 1: Docker 이미지 빌드** + +```bash +docker build -t galaxis-agent:latest . +docker build -f Dockerfile.sandbox -t galaxis-sandbox:latest . +``` + +Expected: 두 이미지 모두 빌드 성공 + +- [ ] **Step 2: .env 파일 생성 (테스트용)** + +```bash +cp .env.example .env +# 최소한의 값만 설정 +# ANTHROPIC_API_KEY, GITEA_TOKEN, GITEA_WEBHOOK_SECRET, DISCORD_TOKEN +``` + +- [ ] **Step 3: 에이전트 서버 기동 확인** + +```bash +docker compose up -d galaxis-agent +sleep 5 +curl http://localhost:8100/health +``` + +Expected: `{"status": "ok"}` + +- [ ] **Step 4: 전체 테스트 실행** + +```bash +uv run pytest tests/ -v +``` + +Expected: 모든 테스트 통과 + +- [ ] **Step 5: 정리 및 최종 커밋** + +```bash +docker compose down +git add -A +git commit -m "chore: Phase 1 complete - galaxis-agent base setup" +git push origin master +``` + +--- + +## Phase 1 완료 기준 + +- [ ] Gitea에 `quant/galaxis-agent` 리포 존재 +- [ ] 불필요한 코드 (Linear, Slack, GitHub, 클라우드 샌드박스) 모두 제거 +- [ ] `deepagents`, `langgraph`, `docker-py`, `discord.py` ARM64 설치 확인 +- [ ] `agent/config.py` + 테스트 통과 +- [ ] Phase 2 스텁 파일 생성 (DockerSandbox, GiteaClient, DiscordClient, 도구) +- [ ] Dockerfile (서버 + 샌드박스) 빌드 성공 +- [ ] docker-compose.yml 구성 완료 (docker-socket-proxy 포함) +- [ ] `/health` 엔드포인트 응답 확인 +- [ ] 모든 테스트 통과