zioinfo-mail/skills/guardia-agent/SKILL.md
DESKTOP-TKLFCPR\ython 45f96176a6 Initial commit: GUARDiA project setup
- CLAUDE.md: project context and architecture spec
- docs/: system specs, DB schema, messenger integration, deployment engine
- skills/: guardia-deploy, guardia-agent, guardia-messenger
- .claude/settings.json: project-level permissions
- .gitignore: Python/FastAPI project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 18:50:19 +09:00

146 lines
4.6 KiB
Markdown

---
name: guardia-agent
description: >
GUARDiA 프로젝트에서 Python 역방향 에이전트(Reverse Agent) 코드를 작성하거나
수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라:
- 내부망 중계 PC 에이전트 코드 작성
- asyncio / websockets 기반 역방향 연결 구현
- PostgreSQL psycopg2 DB 조회 코드
- SM 운영 쉘 스크립트 원격 실행 파이프라인
- 에이전트 자동 재연결(Backoff) 로직
---
# GUARDiA 역방향 에이전트 스킬
## 개념: 역방향 연결 (Reverse Connection)
공공기관 방화벽은 **인바운드 차단** → 에이전트가 먼저 **아웃바운드로 연결**
외부 서버로 전화를 걸어두면 서버가 그 터널을 통해 명령 전달 가능
```
[내부망 에이전트] ──(Outbound WS)──► [외부 중계 WAS]
◄──(명령 역전달)────
```
## 핵심 구현 패턴
### 무한 재연결 루프 (Backoff)
```python
async def agent_main_loop():
while True:
try:
async with websockets.connect(EXTERNAL_WS_URL) as ws:
print("연결 성공")
async for message in ws:
await handle_command(ws, json.loads(message))
except (websockets.ConnectionClosed, OSError):
print("연결 끊김 — 5초 후 재연결")
await asyncio.sleep(5)
except Exception as e:
print(f"예외: {e}")
await asyncio.sleep(5)
```
### 화이트리스트 기반 명령 분기 (필수)
```python
async def handle_command(ws, packet):
action = packet.get("action")
params = packet.get("params", {})
task_id = packet.get("task_id")
room_id = packet.get("room_id")
# 절대로 임의 셸 명령 직접 실행 금지
# 반드시 정의된 함수만 호출
if action == "FETCH_MES_QC":
data = fetch_mes_qc(params.get("date"))
status = "SUCCESS"
elif action == "CHECK_INTERNAL_WAS_STATUS":
data = check_was_health()
status = "SUCCESS"
elif action == "CHECK_DISK_SPACE":
data = check_disk_space()
status = "SUCCESS"
else:
data = {"error": f"허용되지 않은 action: {action}"}
status = "FAIL"
await ws.send(json.dumps({
"event": "TASK_FINISHED",
"room_id": room_id,
"task_id": task_id,
"payload": {"status": status, "data": data}
}))
```
## DB 조회 표준 (psycopg2)
```python
from psycopg2.extras import RealDictCursor
import datetime
class InfrastructureJsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.datetime, datetime.date)):
return obj.isoformat()
return super().default(obj)
def fetch_mes_qc(work_date: str) -> list:
"""고정 쿼리 + 파라미터 바인딩 (SQL Injection 방지)"""
conn = psycopg2.connect(**DB_CONFIG)
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
sql = """
SELECT work_date, prc_code, err_msg, reg_date
FROM tb_mes_wrk_prc_i
WHERE work_date = %s
ORDER BY reg_date DESC LIMIT 10
"""
cur.execute(sql, (work_date,))
return json.loads(json.dumps(cur.fetchall(), cls=InfrastructureJsonEncoder))
finally:
conn.close()
```
## WAS 헬스체크
```python
def check_was_health() -> dict:
try:
r = requests.get("http://10.100.10.10:8080/actuator/health", timeout=3)
return r.json()
except requests.exceptions.ConnectionError:
return {"status": "DOWN", "error": "연결 거부"}
except requests.exceptions.Timeout:
return {"status": "DOWN", "error": "타임아웃"}
```
## 금지 사항
- `subprocess.run(user_input)` 절대 금지
- 패스워드/API 키 하드코딩 금지 → 환경변수 또는 암호화 설정 파일
- 예외 발생 시 에이전트 프로세스 다운(exit) 금지 → try-except로 감싸고 계속 실행
- 메신저 응답에 내부망 IP 그대로 노출 금지
## 환경변수 참조 패턴
```python
import os
DB_CONFIG = {
"host": os.environ["DB_HOST"],
"port": int(os.environ.get("DB_PORT", 5432)),
"user": os.environ["DB_USER"],
"password": os.environ["DB_PASSWORD"],
"database": os.environ["DB_NAME"],
}
EXTERNAL_WS_URL = os.environ["EXTERNAL_WS_URL"]
```
## Windows 서비스 등록 (운영 배포용)
```
# NSSM 사용 (Non-Sucking Service Manager)
nssm install GUARDiA-Agent "python" "C:\GUARDiA\src\agent\main.py"
nssm set GUARDiA-Agent AppDirectory "C:\GUARDiA"
nssm start GUARDiA-Agent
```