G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
5.2 KiB
Markdown
159 lines
5.2 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
|
|
```
|
|
|
|
## 라이선스 주의사항
|
|
|
|
역방향 에이전트 자체는 라이선스 검증 없이 동작하지만, 에이전트가 연결되는 GUARDiA ITSM 서버에 유효한 라이선스가 있어야 기관·서버 등록이 가능하다.
|
|
|
|
| 에디션 | 연결 가능 기관 수 | 등록 가능 서버 수 |
|
|
|--------|-------------|-------------|
|
|
| COMMUNITY | 1 | 20 |
|
|
| STANDARD | 50 | 500 |
|
|
| ENTERPRISE | 무제한 | 무제한 |
|
|
|
|
기관 수 초과 시 에이전트 등록 요청(`POST /api/institutions`)이 HTTP 403으로 거부된다.
|
|
갱신 전까지 기존 연결된 에이전트는 계속 동작한다.
|