galaxis-po/docs/superpowers/plans/2026-03-20-galaxis-agent-phase1.md
머니페니 149560c083 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.
2026-03-20 14:28:14 +09:00

38 KiB

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 디렉토리 생성
mkdir -p ~/workspace/quant/galaxis-agent
cd ~/workspace/quant/galaxis-agent
git init
  • Step 3: open-swe 코드 복사 (git 히스토리 제외)
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에 푸시
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 전용 코드 삭제

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: 삭제 확인
# 삭제된 파일이 없는지 확인
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: 커밋
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.pyopen_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를 제거한다.

# agent/utils/git_utils.py
"""순수 git 명령 유틸리티.

agent/utils/github.py에서 GitHub API 의존성을 제거하고
git CLI 기반 유틸리티만 보존한 모듈.
"""
# 원본 github.py에서 git 관련 함수만 복사
# sandbox.execute()를 통해 git 명령을 실행하는 함수들

실제 구현은 원본 파일을 읽고 해당 함수들을 그대로 옮긴다.

  • Step 3: github.py 삭제
rm -f agent/utils/github.py
  • Step 4: 커밋
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 수정

[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-agentgalaxis-agent

  • requires-python: >=3.11>=3.12

  • target-version: py311py312

  • 삭제: PyJWT, langsmith, langchain-openai, langchain-daytona, langchain-modal, langchain-runloop

  • 추가: docker, pydantic-settings, slowapi, discord.py

  • Step 2: 의존성 설치 테스트

uv sync

Expected: 모든 의존성 설치 성공 (ARM64 호환성 확인)

  • Step 3: 커밋
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: 테스트 작성

# 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: 테스트 실행 — 실패 확인
uv run pytest tests/test_config.py -v

Expected: FAIL — agent.config 모듈 없음

  • Step 3: config.py 구현
# 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: 테스트 실행 — 성공 확인
uv run pytest tests/test_config.py -v

Expected: 4 passed

  • Step 5: .env.example 생성
# .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: 커밋
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 스텁

# 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 스텁
# 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 스텁
# 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 도구 스텁
# 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 도구 스텁
# agent/tools/discord_reply.py
"""Discord 채널/스레드 메시지 전송 도구.

Phase 2에서 구현 예정.
"""


def discord_reply(message: str) -> dict:
    """Discord 채널 또는 스레드에 메시지를 전송한다."""
    raise NotImplementedError("Phase 2에서 구현")
  • Step 6: 커밋
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를 추가:

# 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 스텁만 남긴다. 원본 파일을 읽고, 삭제된 프로바이더 참조를 제거한다. 최종 내용:

# agent/integrations/__init__.py
from agent.integrations.docker_sandbox import DockerSandbox

__all__ = ["DockerSandbox"]
  • Step 3: agent/utils/sandbox.py 수정

클라우드 프로바이더 팩토리를 제거하고 Docker 샌드박스만 사용하도록 변경:

# 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.pyFERNET_KEY로 통일:

# 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 토큰 기반 인증으로 교체. 원본 파일을 읽고 다음으로 교체:

# 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:

{
  "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: 커밋
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 제거
# 삭제:
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 추가
# 추가:
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() 함수 수정

  • 도구 목록 변경
# 변경 전:
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로 변경
# 변경 전: github.com 참조
# 변경 후:
clone_url = f"http://gitea:3000/{owner}/{repo}.git"
  • credential helper 패턴 적용
# 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 확인
uv run python -c "import agent.server; print('OK')"

Expected: import 성공. langgraph_sdk 관련 런타임 에러는 허용 (서버 기동 시에만 발생).

  • 커밋
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 앱으로 재작성:

# 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: 컴파일 확인
uv run python -c "from agent.webapp import app; print(app.title)"

Expected: galaxis-agent

  • Step 4: 커밋
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_commentgitea_comment, slack_thread_replydiscord_reply 변경

  • construct_system_prompt(): linear_project_id, linear_issue_number 매개변수 제거

  • Step 3: 컴파일 확인

uv run python -c "from agent.prompt import construct_system_prompt; print('OK')"
  • Step 4: 커밋
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 스텁으로 교체
# 변경 전:
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: 컴파일 확인

uv run python -c "from agent.tools.commit_and_open_pr import commit_and_open_pr; print('OK')"
  • Step 4: 커밋
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 제거
# 변경 전:
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: 커밋

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: 남아있는 테스트 파일 확인

ls tests/
  • Step 2: 각 테스트 파일에서 삭제된 모듈 import 수정

GitHub/Linear/Slack 관련 import가 있으면 제거하거나 Gitea/Discord로 교체.

  • Step 3: 테스트 실행
uv run pytest tests/ -v

Expected: 남은 테스트 전부 통과 (또는 Phase 2 의존성으로 인한 skip)

  • Step 4: 커밋
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 - 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.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: 빌드 테스트 (로컬 아키텍처)
docker build -t galaxis-agent:latest .
docker build -f Dockerfile.sandbox -t galaxis-sandbox:latest .

Expected: 두 이미지 모두 빌드 성공

  • Step 4: 커밋
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 작성

# 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 구문 검증
docker compose config

Expected: 구문 오류 없음 (네트워크가 없으면 경고 가능)

  • Step 3: 커밋
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 설치 확인
uv run python -c "import deepagents; print(deepagents.__version__)"

Expected: 버전 출력 성공. 실패 시 langchaincreate_react_agent로 대체 계획 수립.

  • Step 2: langgraph 패키지 ARM64 설치 확인
uv run python -c "import langgraph; print(langgraph.__version__)"

Expected: 버전 출력 성공.

  • Step 3: docker-py ARM64 설치 확인
uv run python -c "import docker; print(docker.__version__)"

Expected: 버전 출력 성공.

  • Step 4: discord.py ARM64 설치 확인
uv run python -c "import discord; print(discord.__version__)"

Expected: 버전 출력 성공.

  • Step 5: LangGraph Server ARM64 Docker 이미지 확인
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 이미지 빌드
docker build -t galaxis-agent:latest .
docker build -f Dockerfile.sandbox -t galaxis-sandbox:latest .

Expected: 두 이미지 모두 빌드 성공

  • Step 2: .env 파일 생성 (테스트용)
cp .env.example .env
# 최소한의 값만 설정
# ANTHROPIC_API_KEY, GITEA_TOKEN, GITEA_WEBHOOK_SECRET, DISCORD_TOKEN
  • Step 3: 에이전트 서버 기동 확인
docker compose up -d galaxis-agent
sleep 5
curl http://localhost:8100/health

Expected: {"status": "ok"}

  • Step 4: 전체 테스트 실행
uv run pytest tests/ -v

Expected: 모든 테스트 통과

  • Step 5: 정리 및 최종 커밋
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 엔드포인트 응답 확인
  • 모든 테스트 통과