diff --git a/.env.example b/.env.example index b0428ea5..0f1575e3 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ GITEA_TOKEN= +ADMIN_API_TOKEN= diff --git a/.gitea/workflows/run-agent.yml b/.gitea/workflows/run-agent.yml index 4bf9e7ca..a5d7e4d0 100644 --- a/.gitea/workflows/run-agent.yml +++ b/.gitea/workflows/run-agent.yml @@ -7,18 +7,20 @@ # 요구사항: # - 저장소 설정에서 Actions 활성화 + act_runner 등록 (runs-on: ubuntu-latest 라벨) # - 러너가 백엔드에 내부 주소로 접근 가능해야 함 (아래 api_base 참고) +# - Gitea repo secret 'ADMIN_API_TOKEN' 등록 (백엔드 AuthGate 서비스 토큰) # # 인증 (중요): -# 공개 URL(https://legalpoc.eroomai.com)은 Google OAuth 프록시(auth.eroomai.com) -# 뒤에 있어서 CI 에서는 302 로 로그인 페이지로 튕긴다. localhost:8800 도 job 이 -# 컨테이너 안에서 돌면 컨테이너 자신을 가리켜 못 닿는다. -# → 해결: act_runner 의 job 컨테이너를 AgentBackend 와 같은 도커 네트워크에 붙이고 -# 내부 컨테이너 주소로 직접 호출한다(프록시 우회). 내부 백엔드는 /api 접두사 없이 -# 루트로 API 를 제공한다(/upload-agent, /sse/agent/...). -# 러너 설정 (서버, /opt/gitea-runner-scale/): -# config.yaml 의 container.network 를 백엔드 네트워크 이름으로 지정 -# (docker network ls 로 확인, 보통 agentbackend_agent-network). -# 그러면 api_base = http://agent-backend:8000 (container_name, 내부 포트 8000). +# (1) 네트워크: 공개 URL(legalpoc.eroomai.com)은 OAuth 프록시 뒤라 CI 에서 302. +# localhost:8800 도 job 컨테이너 자신을 가리켜 못 닿음. +# → act_runner job 컨테이너를 AgentBackend 도커 네트워크에 붙이고 내부 컨테이너 +# 주소(http://agent-backend:8000, /api 접두사 없음)로 직접 호출. +# 러너 config.yaml 의 container.network = 백엔드 네트워크(보통 +# agentbackend_agent-network, docker network ls 로 확인). +# (2) 앱 인증: 백엔드에 전역 AuthGate 가 있어 모든 데이터 라우트(workspaces/lookup, +# upload-agent, sse/... 전부)가 인증을 요구한다. CI 는 서비스 토큰을 +# X-Admin-Token 헤더로 보낸다(어느 user_id 로든 대리 호출 허용). 스크립트가 +# ADMIN_API_TOKEN 환경변수를 읽어 자동으로 헤더에 넣는다. 값은 Gitea secret +# 'ADMIN_API_TOKEN' 에서 주입(아래 env). 로컬 실행 시에는 .env 로 제공. # # 실행 흐름 (SKILL.md §0.6 경로 A): # workspaces/lookup(이름→UUID) → upload-agent 등록 → SSE start → @@ -98,6 +100,8 @@ jobs: - name: Run agent via API env: + # 백엔드 전역 AuthGate 통과용 서비스 토큰 (Gitea repo secret 에 등록 필요) + ADMIN_API_TOKEN: ${{ secrets.ADMIN_API_TOKEN }} YAML_PATH: ${{ inputs.yaml_path }} API_BASE: ${{ inputs.api_base }} AGENT_USER_ID: ${{ inputs.user_id }} diff --git a/TEST_WORKFLOW.md b/TEST_WORKFLOW.md index 798fc81c..6fa9f9ca 100644 --- a/TEST_WORKFLOW.md +++ b/TEST_WORKFLOW.md @@ -19,14 +19,21 @@ 프로젝트 루트에 `.env` 파일을 만들고 아래 형식으로 채운다 (값은 실제 토큰): ```dotenv -GITEA_TOKEN= +GITEA_TOKEN= # 워크플로우 dispatch / API 용 +ADMIN_API_TOKEN=<백엔드 AuthGate 서비스 토큰> # 백엔드 데이터 라우트 인증용 ``` -- 토큰 발급: Gitea → Settings → Applications → Generate New Token -- 필요한 스코프: **`write:repository`** -- 사용 후에는 **폐기(revoke)** 권장. 만료를 짧게 설정할 것. +- `GITEA_TOKEN` 발급: Gitea → Settings → Applications → Generate New Token, 스코프 **`write:repository`** +- `ADMIN_API_TOKEN`: 백엔드 컨테이너 env `ADMIN_API_TOKEN` 값 (전역 AuthGate 통과용 + 서비스 토큰). 스크립트가 이 값을 읽어 모든 백엔드 요청에 `X-Admin-Token` 헤더로 보낸다. + 없으면 `workspaces/lookup`·`upload-agent`·`sse` 전부 `401 authentication required`. +- 사용 후에는 **폐기(revoke)** 권장. - 템플릿: [.env.example](.env.example) 참고 (빈 값, 커밋되어도 안전). +> **CI(워크플로우)** 는 `.env` 를 쓰지 않는다. `ADMIN_API_TOKEN` 을 **Gitea repo +> secret** 으로 등록해야 하며(이미 등록됨), 워크플로우가 `${{ secrets.ADMIN_API_TOKEN }}` +> 로 주입한다. `GITEA_TOKEN` 은 dispatch(방법 A)에서만 필요. + --- ## 방법 A — Gitea API 로 트리거 (설계 의도: 러너가 실행) @@ -116,7 +123,8 @@ python scripts/run_agent_api.py \ 작업실 이름↔UUID 목록 확인: ```bash -curl -s "http://100.93.221.71:8800/workspaces/lookup?user_id=" | python3 -m json.tool +curl -s -H "X-Admin-Token: $ADMIN_API_TOKEN" \ + "http://100.93.221.71:8800/workspaces/lookup?user_id=" | python3 -m json.tool ``` --- diff --git a/scripts/run_agent_api.py b/scripts/run_agent_api.py index 2c8aeef3..3857417b 100644 --- a/scripts/run_agent_api.py +++ b/scripts/run_agent_api.py @@ -78,9 +78,17 @@ class AgentRunner: self.started_at = time.monotonic() self.deadline = self.started_at + self.max_runtime - self.sse_client = httpx.Client(timeout=httpx.Timeout(30.0)) + # 백엔드 전역 AuthGate 통과용 서비스 토큰 (모든 데이터 라우트에 필수). + # X-Admin-Token 은 어느 user_id 로든 대리 호출을 허용한다. 값은 환경변수로만 + # 받는다(코드/로그에 노출 금지) — 로컬은 .env, CI 는 Gitea secret. + admin_token = os.environ.get("ADMIN_API_TOKEN", "") + default_headers = {"X-Admin-Token": admin_token} if admin_token else {} + if not admin_token: + log("WARNING: ADMIN_API_TOKEN 미설정 — 인증이 필요한 백엔드는 401 로 거부됩니다") + + self.sse_client = httpx.Client(timeout=httpx.Timeout(30.0), headers=default_headers) # action/cancel 등 단발 요청은 SSE 스트림과 분리된 클라이언트로 보낸다 - self.api_client = httpx.Client(timeout=60.0) + self.api_client = httpx.Client(timeout=60.0, headers=default_headers) self.events_file = (self.output_dir / "events.jsonl").open("a", encoding="utf-8")