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>
This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-05-24 18:50:19 +09:00
commit 45f96176a6
16 changed files with 2015 additions and 0 deletions

15
.claude/settings.json Normal file
View File

@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(git *)",
"Bash(python *)",
"Bash(pip *)",
"Bash(uvicorn *)",
"Bash(pytest *)",
"Bash(alembic *)",
"Bash(psql *)",
"Bash(ssh *)",
"Bash(sftp *)"
]
}
}

50
.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
*.egg
*.egg-info/
dist/
build/
.eggs/
# Virtual environments
venv/
.venv/
env/
ENV/
# Environment variables
.env
.env.*
!.env.example
# Database
*.db
*.sqlite3
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
desktop.ini
# Logs
*.log
logs/
# Claude Code local settings
.claude/settings.local.json
# Secrets
secrets/
*.pem
*.key
*.crt

88
CLAUDE.md Normal file
View File

@ -0,0 +1,88 @@
# GUARDiA — AI 기반 레거시 인프라 자율 운영 플랫폼
> **Claude Code용 프로젝트 마스터 컨텍스트 파일**
> 이 파일을 읽고 프로젝트의 전체 구조와 규칙을 파악한 뒤 작업을 시작하라.
---
## 프로젝트 비전
1,000개 이상의 다중 관공서(Multi-tenant) 레거시 인프라를 타겟으로 하는
**AI 기반 통합 ChatOps 오케스트레이션 플랫폼**.
- 메신저 한 줄 명령 → sLLM 파싱 → 에이전트리스(SSH/SFTP) 배포·운영 자동화
- 에이전트 설치 **불필요** — 표준 SSH/FTP 프로토콜만 활용
- 개발(Dev), SM 운영, PM 관리 세 역할의 워크플로우를 단일 메신저로 통합
---
## 디렉터리 구조
```
C:\GUARDiA\
├── CLAUDE.md # ← 지금 이 파일 (Claude Code 진입점)
├── docs/
│ ├── system_spec.md # 전체 시스템 아키텍처 명세
│ ├── ai_orchestration.md # AI 에이전트 워크플로우 규격
│ ├── db_schema.md # CMDB + SR + Audit 테이블 DDL
│ ├── messenger_integration.md # 메신저 웹훅 & sLLM 연동 명세
│ ├── deployment_engine.md # 에이전트리스 배포 엔진 명세
│ ├── security_governance.md # 보안·권한·감사 정책
│ └── shell_scripts_guide.md # SM용 원격 제어 쉘 스크립트 가이드
├── skills/
│ ├── guardia-deploy/SKILL.md # 배포 엔진 구현 스킬
│ ├── guardia-agent/SKILL.md # Python 역방향 에이전트 스킬
│ └── guardia-messenger/SKILL.md # 메신저 연동 스킬
└── src/ (생성 예정)
├── api/ # FastAPI 백엔드
├── agent/ # Python 역방향 에이전트
├── llm/ # sLLM 파서 모듈
├── deploy/ # SSH/SFTP 배포 엔진
└── db/ # DB 마이그레이션 & 모델
```
---
## 기술 스택 (변경 금지)
| 레이어 | 기술 | 비고 |
|--------|------|------|
| Backend API | Python 3.11+ / FastAPI | 비동기 WebSocket 처리 |
| LLM | 온프레미스 sLLM (Llama-3-8B / Solar-10.7B) | **외부 API 호출 절대 금지** |
| Infra 연결 | `paramiko` (SSH/SFTP) | 에이전트리스 |
| Database | PostgreSQL (CMDB + SR + Audit) | |
| 배포 자동화 | 쉘 스크립트 + Ansible (선택) | |
| 프론트 | React.js 또는 HTML5/Vanilla JS | |
---
## 핵심 구현 원칙
1. **에이전트리스**: 대상 서버에 어떤 소프트웨어도 설치하지 않는다.
2. **결정론적 파싱**: sLLM은 JSON만 출력한다. 자연어 부연 설명 금지.
3. **Fail-Safe**: 모든 배포는 백업 → 배포 → 헬스체크 → 롤백 시퀀스를 따른다.
4. **감사 추적**: 모든 명령과 결과는 `TB_AUDIT_LOG`에 기록한다.
5. **최소 권한**: 관제 전용 일반 계정(opsagent) 사용. root SSH 직접 접속 금지.
6. **보안 우선**: 서버 자격증명은 암호화 DB에만 저장. 메신저 응답에 노출 금지.
---
## 작업 시작 순서 (권장)
```
1단계: docs/db_schema.md 읽기 → PostgreSQL DDL 작성
2단계: docs/messenger_integration.md 읽기 → FastAPI 웹훅 서버 구현
3단계: docs/deployment_engine.md 읽기 → SSH/SFTP 배포 엔진 구현
4단계: docs/ai_orchestration.md 읽기 → sLLM 파서 & 워크플로우 연동
5단계: docs/security_governance.md 읽기 → 권한 검증 & 감사 모듈 구현
```
---
## 스킬 파일 참조 방법
특정 기능 구현 전 해당 스킬 파일을 반드시 먼저 읽어라.
- 배포 엔진 작업 시 → `skills/guardia-deploy/SKILL.md`
- 역방향 에이전트 작업 시 → `skills/guardia-agent/SKILL.md`
- 메신저 연동 작업 시 → `skills/guardia-messenger/SKILL.md`

162
docs/ai_orchestration.md Normal file
View File

@ -0,0 +1,162 @@
# [Specification] AI Agent Workflow & SR Task Execution
본 문서는 SR 접수부터 AI 에이전트(Claude Code)의 실제 작업 수행까지의 연동 규격이다.
---
## 1. SR Task Handling Logic
```
Reception → 메신저 SR을 파싱 서버가 TB_SR_REQUEST에 정형 데이터로 저장
Prioritization → PM 우선순위 부여 → 작업 관리 엔진이 큐에 적재
Execution Context → 작업 관리 엔진이 context.md를 프로젝트 루트에 생성
Claude Code → context.md 읽고 작업 범위 파악 → 수행
Result → result.json 저장 → DB 업데이트 → 메신저 완료 통보
```
### 상태 전이 (State Machine)
```
RECEIVED → PARSED → PENDING_APPROVAL → APPROVED → IN_PROGRESS → COMPLETED
FAILED_ROLLBACK
```
---
## 2. Claude Code Interaction Pattern
### 2.1. Context 파일 생성 규격 (`context.md`)
```markdown
## Task Context
- SR_NO: {sr_number}
- Institution: {institution_name}
- System: {system_name}
- Target Nodes: {node_list}
- Action: {action_type} # DEPLOY / LOG_ANALYSIS / RESTART / SSL_CHECK
- Artifacts: {file_paths}
- Constraints: {policy_rules}
```
### 2.2. Result 파일 규격 (`result.json`)
```json
{
"sr_no": "SR-2026-001",
"status": "SUCCESS", // SUCCESS | FAILED | PARTIAL
"changed_files": [],
"execution_log": "...",
"health_check": {"status": "200 OK"},
"rollback_available": true,
"completed_at": "2026-05-23T19:00:00Z"
}
```
---
## 3. sLLM Deterministic Parser
### 3.1. System Prompt (엄격 적용)
```
[SYSTEM PROMPT — 절대 변경 금지]
당신은 인프라 자동화 시스템의 핵심 파서입니다.
1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다.
2. 부연 설명, 인삿말, 안내 텍스트는 절대 포함하지 않습니다.
3. CMDB에 정의된 정확한 서버 ID와 매핑되지 않는 정보는 "UNKNOWN"으로 처리합니다.
4. 배포 명령 시 반드시 'requires_approval'(boolean) 값을 판단합니다.
```
### 3.2. 출력 JSON 스키마
```json
{
"intent_type": "DEPLOY_UPGRADE | LOG_ANALYSIS | SERVICE_RESTART | SSL_CHECK | DB_QUERY | CRON_MANAGE",
"institution": "기관명",
"system_name": "시스템명",
"infrastructure_layer": "WEB | WAS | DB | ESB",
"target_nodes": ["NODE_01", "NODE_02"],
"action_sequence": ["UPLOAD", "BACKUP", "REPLACE", "ROLLING_RESTART"],
"deploy_artifacts": ["class", "html", "js", "image"],
"requires_approval": false,
"priority": "HIGH | MEDIUM | LOW"
}
```
---
## 4. Deployment Integration
### 4.1. 정적 vs 동적 자원 처리
| 자원 타입 | WAS 재기동 필요 | 처리 방식 |
|-----------|----------------|-----------|
| html, js, css, image | ❌ | SFTP 카피 → 즉시 반영 |
| .class, .jar, .war | ✅ | 백업 → 롤링 재기동 |
| config 파일 | ✅ (서비스에 따라) | 확인 후 결정 |
### 4.2. 무중단 롤링 배포 시퀀스
```
WAS #1:
1. L4/Nginx에서 트래픽 차단
2. 현재 버전 백업 (mv app.jar app.jar.bak_YYYYMMDD)
3. SFTP로 신규 파일 전송 + md5sum 무결성 검증
4. shutdown.sh 실행 → startup.sh 실행
5. curl -I http://localhost:8080 → 200 OK 확인 (timeout 30s)
6. 실패 시 즉시 rollback.sh 실행
WAS #2: WAS #1 완료 확인 후 동일 시퀀스 반복
```
### 4.3. PM 승인 HITL 흐름
```python
# 작업 완료 → PM에게 Result Package 전송
def notify_pm_for_approval(sr_id, package):
summary = f"[{sr_id}] 작업 완료\n헬스체크: {package['health']['status']}"
messenger.send_approval_request(
target="PM_TEAM",
message=summary,
callback_url=f"/api/sr/approve/{sr_id}"
)
# PM 승인 시 고객 완료 통보
@app.post("/api/sr/approve/{sr_id}")
def finalize_sr(sr_id: str):
db.update_sr_status(sr_id, "COMPLETED")
messenger.send_message(requester, f"SR-{sr_id} 배포 완료")
```
---
## 5. 작업 관리 엔진 핵심 코드 구조
```python
class TaskManager:
def process_task_completion(self, sr_id, work_dir):
result_package = self._collect_result_package(work_dir)
self._notify_pm_for_approval(sr_id, result_package)
self.db.update_sr_status(sr_id, "PENDING_PM_VALIDATION")
def _collect_result_package(self, work_dir):
return {
"diff": open(f"{work_dir}/changes.diff").read(),
"logs": open(f"{work_dir}/execution.log").read(),
"health": json.load(open(f"{work_dir}/health_check.json"))
}
```
---
## 6. 로깅 및 감사 표준
- 모든 작업 이력: JSON Lines 포맷 → `audit_logs/` 디렉토리
- 감사 로그 SHA-256 해시 체이닝:
`Log[n].hash = SHA256(Log[n].data + Log[n-1].hash)`
- 위변조 탐지: 매시간 해시 체인 연결성 자동 검증

197
docs/db_schema.md Normal file
View File

@ -0,0 +1,197 @@
# [Specification] GUARDiA 데이터베이스 스키마 설계서
> **DBMS:** PostgreSQL 15+
> **원칙:** root 계정 금지. 전용 서비스 계정 사용. 비밀번호 컬럼 AES-256 암호화 필수.
---
## 1. 인프라 자산 영역 (CMDB)
### TB_INST_META (관공서 마스터)
```sql
CREATE TABLE TB_INST_META (
inst_id VARCHAR(20) PRIMARY KEY, -- 기관 코드 (예: MOF, MOIS)
inst_name VARCHAR(100) NOT NULL, -- 기관명 (예: 기획재정부)
pm_contact VARCHAR(50), -- 담당 PM 사번
phone VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### TB_SERVER_INFO (서버 노드 마스터)
```sql
CREATE TABLE TB_SERVER_INFO (
server_id VARCHAR(30) PRIMARY KEY, -- 서버 고유 ID (예: MOF-WAS-01)
inst_id VARCHAR(20) REFERENCES TB_INST_META(inst_id),
system_name VARCHAR(100) NOT NULL, -- 업무 시스템명
layer VARCHAR(10) NOT NULL, -- WEB | WAS | DB | ESB
hostname VARCHAR(100),
ip_address VARCHAR(45) NOT NULL,
ssh_port INT DEFAULT 22,
ftp_path VARCHAR(255), -- 배포 기본 경로
os_user VARCHAR(50), -- SSH 접속 계정
os_pw_enc TEXT, -- AES-256 암호화된 비밀번호
os_type VARCHAR(20) DEFAULT 'LINUX', -- LINUX | WINDOWS
status VARCHAR(20) DEFAULT 'ACTIVE',
last_check TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_server_inst ON TB_SERVER_INFO(inst_id, layer);
```
### TB_SYSTEM_OWNER (시스템 담당자 매핑)
```sql
CREATE TABLE TB_SYSTEM_OWNER (
system_id VARCHAR(30) PRIMARY KEY,
inst_id VARCHAR(20) REFERENCES TB_INST_META(inst_id),
owner_id VARCHAR(50) NOT NULL, -- 1순위 승인권자 사번
deputy_id VARCHAR(50), -- 2순위 대리 승인자
supervisor_id VARCHAR(50), -- 3순위 상위 관리자
sla_minutes INT DEFAULT 10, -- 승인 대기 SLA (분)
is_active CHAR(1) DEFAULT 'Y' -- Y: 근무중 / N: 부재중
);
```
---
## 2. 운영 작업 영역 (Ops Task)
### TB_OPS_TASK (운영 작업 헤더)
```sql
CREATE TABLE TB_OPS_TASK (
task_id VARCHAR(30) PRIMARY KEY, -- 예: OPS-20260523-001
task_category VARCHAR(50) NOT NULL, -- DEPLOY | INFRA | DATA | MAINTENANCE
task_sub_type VARCHAR(50), -- SSL_REPLACE | LOG_ANALYSIS | RESTART
raw_request TEXT NOT NULL, -- 사용자 원문 자연어
status VARCHAR(30) DEFAULT 'RECEIVED',
requester_id VARCHAR(50) NOT NULL,
inst_id VARCHAR(20) REFERENCES TB_INST_META(inst_id),
playbook_id VARCHAR(50), -- 실행 플레이북 ID
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 상태값: RECEIVED | PARSED | PENDING_APPROVAL | ESCALATED | APPROVED | IN_PROGRESS | PENDING_PM_VALIDATION | COMPLETED | FAILED_ROLLBACK
CREATE INDEX idx_task_status ON TB_OPS_TASK(status, created_at DESC);
```
### TB_DEPLOY_HISTORY (배포 상세 이력)
```sql
CREATE TABLE TB_DEPLOY_HISTORY (
deploy_id BIGSERIAL PRIMARY KEY,
task_id VARCHAR(30) REFERENCES TB_OPS_TASK(task_id),
server_id VARCHAR(30) REFERENCES TB_SERVER_INFO(server_id),
user_id VARCHAR(50) NOT NULL,
sr_no VARCHAR(30),
artifact_list JSONB, -- 배포된 파일 목록
deploy_status CHAR(1) DEFAULT 'P', -- P:대기 S:성공 F:실패
backup_path VARCHAR(255),
result_hash CHAR(64), -- SHA-256
deployed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
---
## 3. 거버넌스 승인 영역
### TB_APPROVAL_FLOW (다단계 승인 이력)
```sql
CREATE TABLE TB_APPROVAL_FLOW (
flow_id BIGSERIAL PRIMARY KEY,
task_id VARCHAR(30) REFERENCES TB_OPS_TASK(task_id),
step_order INT NOT NULL, -- 1, 2, 3...
approver_id VARCHAR(50) NOT NULL,
approval_status CHAR(1) DEFAULT 'P', -- P:대기 A:승인 R:반려
comments VARCHAR(500),
escalation_type VARCHAR(20), -- DIRECT | DEPUTY | SUPERVISOR
approved_at TIMESTAMP
);
CREATE INDEX idx_approval_task ON TB_APPROVAL_FLOW(task_id, step_order);
```
---
## 4. 감사 영역 (Audit — 위변조 방지)
### TB_AUDIT_LOG (SHA-256 해시 체인)
```sql
CREATE TABLE TB_AUDIT_LOG (
log_id BIGSERIAL PRIMARY KEY,
task_id VARCHAR(30) REFERENCES TB_OPS_TASK(task_id),
server_ip VARCHAR(45),
exec_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
command TEXT NOT NULL,
exit_code INT,
result_hash CHAR(64), -- SHA256(현재 결과 + 이전 hash)
operator_id VARCHAR(50)
);
-- 해시 체이닝: Log[n].result_hash = SHA256(Log[n].output || Log[n-1].result_hash)
```
### TB_SR_REQUEST (SR 티켓 — 레거시 호환)
```sql
CREATE TABLE TB_SR_REQUEST (
sr_no VARCHAR(30) PRIMARY KEY, -- SR-2026-001
requester VARCHAR(100),
system_id VARCHAR(30) REFERENCES TB_SERVER_INFO(server_id),
raw_text TEXT,
task_type VARCHAR(50), -- UI_FIX | DEPLOY | LOG_ANALYSIS
status VARCHAR(20) DEFAULT 'PENDING',
assigned_ai VARCHAR(50), -- 에이전트 실행 ID
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
---
## 5. 메신저 채팅 영역
### TB_MSG_ROOMS (대화방)
```sql
CREATE TABLE TB_MSG_ROOMS (
room_id VARCHAR(50) PRIMARY KEY,
room_name VARCHAR(100) NOT NULL,
inst_id VARCHAR(20) REFERENCES TB_INST_META(inst_id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### TB_MSG_CHAT_LOG (채팅 이력)
```sql
CREATE TABLE TB_MSG_CHAT_LOG (
chat_id BIGSERIAL PRIMARY KEY,
room_id VARCHAR(50) REFERENCES TB_MSG_ROOMS(room_id),
sender_id VARCHAR(50) NOT NULL,
sender_name VARCHAR(100),
sender_type VARCHAR(20) NOT NULL, -- HUMAN | BOT | AGENT
message_text TEXT,
is_command BOOLEAN DEFAULT FALSE,
task_id VARCHAR(30),
itsm_ticket_id VARCHAR(50), -- 향후 ITSM 연동용 (null 허용)
asset_code VARCHAR(50), -- 인프라 자산 코드
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
---
## 6. 더미 데이터 (로컬 테스트용)
```sql
-- 관공서 기관
INSERT INTO TB_INST_META (inst_id, inst_name, pm_contact) VALUES
('MOF', '기획재정부', 'PM001'),
('MOIS', '행정안전부', 'PM002'),
('MSS', '중소벤처기업부', 'PM003');
-- 서버 노드
INSERT INTO TB_SERVER_INFO (server_id, inst_id, system_name, layer, ip_address, ssh_port, ftp_path, os_user) VALUES
('MOF-WAS-01', 'MOF', '예산시스템', 'WAS', '192.168.10.11', 22, '/app/mof/webapps/ROOT', 'mofadmin'),
('MOF-WAS-02', 'MOF', '예산시스템', 'WAS', '192.168.10.12', 22, '/app/mof/webapps/ROOT', 'mofadmin'),
('MOF-DB-01', 'MOF', '예산시스템', 'DB', '192.168.10.13', 5432, '/var/lib/postgresql', 'postgres');
-- 담당자 매핑
INSERT INTO TB_SYSTEM_OWNER (system_id, inst_id, owner_id, deputy_id, supervisor_id) VALUES
('MOF-WAS-01', 'MOF', 'EMP-0101', 'EMP-0102', 'EMP-0100'),
('MOF-DB-01', 'MOF', 'EMP-0201', NULL, 'EMP-0200');
```

215
docs/deployment_engine.md Normal file
View File

@ -0,0 +1,215 @@
# [Specification] 에이전트리스 배포 엔진 (Agentless Deployment Engine)
---
## 1. 개요
대상 서버에 어떤 소프트웨어도 설치하지 않고, 표준 **SSH/SFTP 프로토콜**만으로
파일 배포 · 서비스 재기동 · 헬스체크 · 자동 롤백을 수행하는 핵심 실행 엔진.
---
## 2. SSH Executor 모듈 (`src/deploy/ssh_executor.py`)
```python
import paramiko
import hashlib
class SSHExecutor:
def __init__(self, host: str, user: str, password: str, port: int = 22):
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.client.connect(host, port=port, username=user, password=password, timeout=10)
def execute_command(self, command: str, timeout: int = 300) -> dict:
"""원격 명령 실행 — exit_code 반드시 확인"""
stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
exit_code = stdout.channel.recv_exit_status()
return {
"exit_code": exit_code,
"stdout": stdout.read().decode("utf-8", errors="replace"),
"stderr": stderr.read().decode("utf-8", errors="replace")
}
def upload_file(self, local_path: str, remote_path: str) -> str:
"""SFTP 파일 업로드 + MD5 무결성 검증"""
sftp = self.client.open_sftp()
sftp.put(local_path, remote_path)
sftp.close()
# 원격지 MD5 검증
result = self.execute_command(f"md5sum {remote_path}")
return result["stdout"].split()[0] # 원격 MD5
def close(self):
self.client.close()
```
---
## 3. 롤링 배포 엔진 (`src/deploy/rolling_deployer.py`)
```python
import time
import requests
from .ssh_executor import SSHExecutor
from ..db.audit import log_execution
class RollingDeployer:
HEALTH_TIMEOUT = 30 # 헬스체크 최대 대기 (초)
def deploy(self, nodes: list, artifact_path: str, sr_id: str) -> bool:
"""무중단 롤링 배포 — 노드 순차 처리"""
for node in nodes:
success = self._deploy_single_node(node, artifact_path, sr_id)
if not success:
return False
return True
def _deploy_single_node(self, node: dict, artifact_path: str, sr_id: str) -> bool:
executor = SSHExecutor(node["ip"], node["user"], node["password"], node["ssh_port"])
try:
# 1. Pre-check: 디스크 용량 확인
result = executor.execute_command("df -h /app | awk 'NR==2{print $5}' | sed 's/%//'")
if int(result["stdout"].strip()) > 90:
raise Exception("디스크 사용량 90% 초과 — 배포 중단")
# 2. 백업 (타임스탬프 포함)
ts = time.strftime("%Y%m%d_%H%M%S")
backup_cmd = f"cp -rp {node['deploy_path']} {node['deploy_path']}.bak_{ts}"
executor.execute_command(backup_cmd)
# 3. SFTP 전송 + 무결성 검증
local_md5 = self._local_md5(artifact_path)
remote_path = f"{node['deploy_path']}/{artifact_path.split('/')[-1]}"
remote_md5 = executor.upload_file(artifact_path, remote_path)
if local_md5 != remote_md5:
raise Exception(f"MD5 불일치: {local_md5} vs {remote_md5}")
# 4. WAS 재기동 (동적 자원인 경우)
if node.get("requires_restart", True):
executor.execute_command("sh /app/scripts/shutdown.sh")
executor.execute_command(f"cp {remote_path} {node['deploy_path']}/")
result = executor.execute_command("sh /app/scripts/startup.sh")
if result["exit_code"] != 0:
raise Exception(f"startup.sh 실패: {result['stderr']}")
# 5. 헬스체크
if not self._health_check(node["ip"], node.get("health_port", 8080)):
raise Exception("헬스체크 실패 — 롤백 시작")
log_execution(sr_id, node["ip"], "DEPLOY_SUCCESS", 0, "")
return True
except Exception as e:
self._rollback(executor, node, ts)
log_execution(sr_id, node["ip"], "DEPLOY_FAILED", 1, str(e))
return False
finally:
executor.close()
def _health_check(self, ip: str, port: int) -> bool:
"""서비스 포트 응답 확인 — 최대 30초 대기"""
deadline = time.time() + self.HEALTH_TIMEOUT
while time.time() < deadline:
try:
r = requests.get(f"http://{ip}:{port}", timeout=3)
if r.status_code == 200:
return True
except Exception:
pass
time.sleep(2)
return False
def _rollback(self, executor: SSHExecutor, node: dict, ts: str):
"""백업본으로 자동 롤백"""
try:
executor.execute_command("sh /app/scripts/shutdown.sh")
executor.execute_command(f"cp -rp {node['deploy_path']}.bak_{ts} {node['deploy_path']}")
executor.execute_command("sh /app/scripts/startup.sh")
except Exception as e:
print(f"[ROLLBACK ERROR] {e}")
@staticmethod
def _local_md5(path: str) -> str:
with open(path, "rb") as f:
return hashlib.md5(f.read()).hexdigest()
```
---
## 4. Command Sanitizer (보안 필터)
```python
BLACKLIST_PATTERNS = [
r"rm\s+-[rRf].*\/", # rm -rf /
r"\bmkfs\b",
r"\bformat\b",
r"\bdrop\s+table\b",
r"\btruncate\b",
r">\s*/dev/sda",
]
def sanitize_command(command: str) -> None:
"""위험 명령어 사전 차단 — SecurityViolationError 발생"""
import re
for pattern in BLACKLIST_PATTERNS:
if re.search(pattern, command, re.IGNORECASE):
raise SecurityViolationError(f"금지 명령어 패턴 탐지: {pattern}")
```
---
## 5. 티어드 롤링 배포 (1,000+ 사이트 대응)
```python
def tiered_rolling_deployment(sites: list, playbook: dict) -> bool:
"""
Tier 1 (미션 크리티컬): 순차 배포, 엄격한 헬스체크
Tier 2 (표준 사이트): 50개 단위 병렬
Tier 3 (테스트/개발): 전량 병렬
"""
tiers = _categorize_by_tier(sites)
for tier_name, tier_sites in tiers.items():
batch_size = 1 if tier_name == "TIER1" else 50
results = []
for i in range(0, len(tier_sites), batch_size):
batch = tier_sites[i:i+batch_size]
# 병렬 처리 (asyncio 또는 ThreadPoolExecutor)
batch_results = _deploy_batch(batch, playbook)
results.extend(batch_results)
# Safety Gate: 실패율 > 10% 이면 중단
fail_rate = sum(1 for r in results if not r) / len(results)
if fail_rate > 0.1:
print(f"[ABORT] {tier_name} 실패율 {fail_rate:.0%} > 10% — 전체 배포 중단")
return False
return True
```
---
## 6. 배포 파이프라인 API 엔드포인트
```python
@app.post("/api/deploy/trigger")
async def trigger_deployment(sr_id: str, approver_id: str):
"""승인 완료 후 배포 엔진 가동"""
task = db.get_task(sr_id)
nodes = db.get_server_nodes(task["inst_id"], task["system_name"])
artifacts = db.get_staged_artifacts(sr_id)
deployer = RollingDeployer()
success = deployer.deploy(nodes, artifacts[0], sr_id)
if success:
db.update_task_status(sr_id, "PENDING_PM_VALIDATION")
notify_pm_for_hitl_review(sr_id)
else:
db.update_task_status(sr_id, "FAILED_ROLLBACK")
notify_failure(sr_id)
return {"sr_id": sr_id, "success": success}
```

View File

@ -0,0 +1,254 @@
# [Specification] 메신저 연동 & sLLM 통합 명세
---
## 1. 아키텍처 개요
```
[메신저 (슬랙/잔디/자체앱)]
│ Webhook (HTTPS POST)
[FastAPI Webhook Server :80]
│ Nginx → proxy_pass → :3000 (Node.js/NestJS) 또는 :8000 (FastAPI)
[sLLM Parser :11434 (Ollama) or :8000 (vLLM)]
│ JSON 정형 데이터
[CMDB 조회 → 권한 검증 → 승인 라우팅]
[SSH/SFTP 배포 엔진]
```
---
## 2. sLLM 서버 구성
### 2.1. 모델 선택
```
권장 1순위: Solar-10.7B-Instruct (한국어 특화)
권장 2순위: Llama-3-8B-Instruct (경량)
실행 엔진: Ollama (빠른 구축) 또는 vLLM (고성능)
양자화: 4-bit AWQ 또는 GGUF (GPU VRAM 절감)
```
### 2.2. Ollama 기반 로컬 실행
```bash
# 설치 및 모델 다운로드
ollama pull llama3:8b-instruct-q4_K_M
# API 호출 (OpenAI 호환)
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "llama3:8b-instruct-q4_K_M", "messages": [...]}'
```
### 2.3. Python 클라이언트 모듈 (`src/llm/parser.py`)
```python
import openai
client = openai.OpenAI(
base_url="http://localhost:11434/v1", # 온프레미스
api_key="local" # 더미 키
)
SYSTEM_PROMPT = """
당신은 인프라 자동화 시스템의 핵심 파서입니다.
1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다.
2. 부연 설명, 인삿말은 절대 포함하지 않습니다.
3. CMDB에 없는 기관/서버는 "UNKNOWN"으로 처리합니다.
4. 배포 명령 시 requires_approval(boolean)을 반드시 판단합니다.
출력 포맷:
{"intent_type": "...", "institution": "...", "system_name": "...",
"infrastructure_layer": "WAS|WEB|DB", "target_nodes": [],
"action_sequence": [], "deploy_artifacts": [], "requires_approval": false}
"""
def parse_natural_language(user_text: str) -> dict:
response = client.chat.completions.create(
model="llama3:8b-instruct-q4_K_M",
response_format={"type": "json_object"},
temperature=0.1,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_text}
]
)
return json.loads(response.choices[0].message.content)
```
---
## 3. FastAPI Webhook 서버
### 3.1. 메신저 수신 엔드포인트 (`src/api/webhook.py`)
```python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import Dict, List
import json
app = FastAPI()
# 연결된 세션 관리
room_channels: Dict[str, List[WebSocket]] = {}
connected_agents: Dict[str, WebSocket] = {}
@app.post("/messenger/webhook")
async def receive_message(payload: dict):
"""메신저 Webhook 수신 — 메시지 파싱 & SR 생성"""
user_id = payload.get("user_id")
text = payload.get("text", "")
files = payload.get("files", [])
room_id = payload.get("room_id")
# 파일 스테이징
staged_files = await stage_files(files)
# sLLM 파싱
parsed = parse_natural_language(text)
# CMDB 조회 → 서버 정보 매핑
server_list = db.query_servers(parsed["institution"], parsed["system_name"])
if not server_list:
return {"status": "ERROR", "message": "CMDB에 해당 서버 정보 없음"}
# SR 생성
sr_id = create_ops_task(parsed, user_id, staged_files)
# 승인 라우팅
if parsed.get("requires_approval"):
initiate_approval_process(sr_id)
return {"status": "PENDING_APPROVAL", "sr_id": sr_id}
# 즉시 실행
trigger_execution_engine(sr_id)
return {"status": "IN_PROGRESS", "sr_id": sr_id}
```
### 3.2. WebSocket 실시간 중계 (`src/api/ws_relay.py`)
```python
@app.websocket("/ws/chat/{room_id}/{client_id}/{client_type}")
async def chat_endpoint(ws: WebSocket, room_id: str, client_id: str, client_type: str):
await ws.accept()
if client_type == "HUMAN":
room_channels.setdefault(room_id, []).append(ws)
elif client_type == "AGENT":
connected_agents[client_id] = ws
try:
while True:
data = json.loads(await ws.receive_text())
if client_type == "HUMAN" and data.get("is_command"):
# AI 봇 멘션 → 파싱 → 역방향 에이전트로 라우팅
target_agent = data.get("target_agent_id", "pc-01")
if target_agent in connected_agents:
await connected_agents[target_agent].send_text(
json.dumps({"task_id": str(uuid4()), "action": data["command_code"]})
)
elif client_type == "AGENT" and data.get("event") == "TASK_FINISHED":
# 에이전트 결과 → 방 전체에 브로드캐스트
for ws in room_channels.get(room_id, []):
await ws.send_text(json.dumps({"sender": "BOT", "result": data["payload"]}))
except WebSocketDisconnect:
if client_type == "HUMAN":
room_channels.get(room_id, []).remove(ws)
else:
connected_agents.pop(client_id, None)
```
---
## 4. Nginx 웹소켓 프록시 설정
```nginx
upstream was_backend {
server 127.0.0.1:8000;
keepalive 32;
}
server {
listen 80;
server_name localhost;
location /ws {
proxy_pass http://was_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_buffering off;
client_max_body_size 100M; # 배포 파일 업로드
}
location / {
proxy_pass http://was_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}
```
---
## 5. Proactive AI 챗봇 (대화 맥락 분석)
```python
CONTEXT_RULES = {
"db_delay": {
"keywords": ["DB", "디비", "쿼리", "느리", "타임아웃", "락"],
"action": "FETCH_DB_LOCKS",
"message": "DB 지연 징후가 감지되었습니다. 대기 쿼리를 조회할까요?"
},
"disk_full": {
"keywords": ["용량", "디스크", "로그", "쌓였"],
"action": "CHECK_DISK_SPACE",
"message": "디스크 공간 관련 문제가 감지되었습니다. 용량을 확인할까요?"
}
}
async def analyze_context(room_id: str, sender: str, text: str):
"""대화 맥락 분석 → 선제적 조언 Push"""
if any(kw in text for kw in ["@bot", "AI", "조회", "확인"]):
return # 이미 명시적 명령
for rule_name, rule in CONTEXT_RULES.items():
if any(kw in text for kw in rule["keywords"]):
await asyncio.sleep(1)
await broadcast_to_room(room_id, {
"sender": "GUARDiA-Bot",
"sender_type": "BOT",
"message_content": f"🤖 {rule['message']}",
"interactive_action": {
"type": "BUTTON",
"label": "즉시 확인",
"command_code": rule["action"]
}
})
break # 첫 매칭만
```
---
## 6. 메시지 프로토콜 스키마 (ITSM 확장 호환)
```json
{
"message_id": "MSG-20260523-0001",
"timestamp": "2026-05-23T19:00:00Z",
"sender": "ENGINEER_01",
"sender_type": "HUMAN",
"msg_type": "CHAT",
"content": "기재부 예산시스템 WAS 재기동해줘",
"is_widget": false,
"itsm_metadata": {
"itsm_ticket_id": null,
"asset_code": "MOF-WAS-01",
"severity": "MEDIUM"
}
}
```

183
docs/security_governance.md Normal file
View File

@ -0,0 +1,183 @@
# [Specification] 보안 & 거버넌스 정책
---
## 1. RBAC 권한 체계
### 1.1. 역할 정의
| 역할 | 권한 | 담당 |
|------|------|------|
| Developer | 소관 사이트 배포 요청 | 파일 전송 + 재기동 |
| SM 운영자 | 로그 조회, 자원 정비, 크론 관리 | 운영 유지보수 |
| 팀장 (Owner) | 1차 배포 승인 | 소관 시스템 |
| 대리 (Deputy) | 팀장 부재 시 대리 승인 | |
| PM | 최종 HITL 검증, 거버넌스 규칙 수정 | 전체 통제 |
| 보안책임자 | 인프라 변경/보안 작업 3차 승인 | |
### 1.2. 다단계 승인 워크플로우
```python
APPROVAL_POLICY = {
"MAINTENANCE": {"steps": 1, "approvers": ["OWNER"]},
"DEPLOY_NORMAL": {"steps": 2, "approvers": ["OWNER", "PM"]},
"DEPLOY_SECURE": {"steps": 3, "approvers": ["OWNER", "PM", "SECURITY"]},
}
def get_approval_policy(task_category: str) -> dict:
if task_category in ["LOG_ANALYSIS", "CRON_MANAGE"]:
return APPROVAL_POLICY["MAINTENANCE"]
elif task_category in ["DEPLOY", "DATA_CLEAN"]:
return APPROVAL_POLICY["DEPLOY_NORMAL"]
else: # SSL, INFRA_CHANGE
return APPROVAL_POLICY["DEPLOY_SECURE"]
```
---
## 2. 계층적 승인 엔진 (에스컬레이션)
```python
def get_approver_chain(system_id: str) -> str:
"""SLA 초과 시 자동 에스컬레이션"""
info = db.execute(
"SELECT owner_id, deputy_id, supervisor_id, sla_minutes, is_active "
"FROM TB_SYSTEM_OWNER WHERE system_id = %s", (system_id,)
).fetchone()
if info["is_active"] == "Y":
return info["owner_id"] # 1순위: 담당 팀장
elif info["deputy_id"]:
return info["deputy_id"] # 2순위: 대리 승인자
else:
return info["supervisor_id"] # 3순위: 상위 관리자
def check_escalation(task_id: str):
"""SLA 초과 여부 체크 → 자동 이관"""
task = db.get_task(task_id)
owner_info = db.get_system_owner(task["system_id"])
elapsed = (datetime.now() - task["created_at"]).seconds / 60
if elapsed > owner_info["sla_minutes"] and task["status"] == "PENDING_APPROVAL":
next_approver = get_approver_chain(task["system_id"])
db.update_task_approver(task_id, next_approver, "ESCALATED")
messenger.send_urgent_notification(next_approver, f"[긴급] {task_id} 승인 대기")
audit.log_action(task_id, f"Escalated to {next_approver}")
```
---
## 3. 자격증명 보안
### 3.1. 암호화 저장 정책
```
- SSH/FTP 비밀번호: AES-256 암호화 → TB_SERVER_INFO.os_pw_enc
- 실행 시점에만 메모리 로드, 로그 출력 금지
- 메신저 응답창 노출 절대 금지
- 주기적 비밀번호 갱신 시 DB 암호화 업데이트
```
### 3.2. SSH 접속 보안
```bash
# opsagent 계정 sudoers 설정 (대상 서버에 1회 설정)
# /etc/sudoers.d/opsagent
opsagent ALL=(ALL) NOPASSWD: /usr/bin/kill, /usr/bin/systemctl restart *, /app/scripts/shutdown.sh, /app/scripts/startup.sh
```
---
## 4. 감사 로그 해시 체이닝
```python
import hashlib, json
def compute_audit_hash(log_data: dict, prev_hash: str) -> str:
"""SHA-256 해시 체인 생성"""
content = json.dumps(log_data, ensure_ascii=False, sort_keys=True)
combined = f"{content}{prev_hash}"
return hashlib.sha256(combined.encode()).hexdigest()
def log_execution(task_id: str, server_ip: str, command: str, exit_code: int, result: str):
"""감사 로그 저장 — 이전 로그의 해시를 체인에 포함"""
prev = db.get_last_audit_log()
prev_hash = prev["result_hash"] if prev else "GENESIS"
log_data = {
"task_id": task_id,
"server_ip": server_ip,
"command": command,
"exit_code": exit_code,
"exec_time": datetime.utcnow().isoformat()
}
new_hash = compute_audit_hash(log_data, prev_hash)
db.insert_audit_log({**log_data, "result": result, "result_hash": new_hash})
def verify_audit_integrity() -> bool:
"""해시 체인 무결성 검증 — 위변조 탐지"""
logs = db.get_all_audit_logs_ordered()
prev_hash = "GENESIS"
for log in logs:
expected_hash = compute_audit_hash(
{k: v for k, v in log.items() if k != "result_hash"}, prev_hash
)
if expected_hash != log["result_hash"]:
alert_security_breach(log["log_id"])
return False
prev_hash = log["result_hash"]
return True
```
---
## 5. Kill-Switch (비상 정지)
```python
@app.post("/api/emergency/kill")
async def emergency_kill(operator_id: str, reason: str):
"""메신저 '정지' 명령 → 전체 배포 프로세스 즉시 중단"""
# 진행 중인 모든 IN_PROGRESS 태스크 중단
active_tasks = db.get_tasks_by_status("IN_PROGRESS")
for task in active_tasks:
task_manager.abort(task["task_id"])
trigger_rollback(task["task_id"])
# 감사 로그
log_execution("EMERGENCY", "ALL", f"KILL_SWITCH by {operator_id}", 0, reason)
# PM에게 알림
messenger.send_to_all_pm(f"🚨 비상 정지 실행: {operator_id} — {reason}")
return {"status": "KILLED", "aborted_tasks": len(active_tasks)}
```
---
## 6. 보안 감사 체크리스트 (GS 인증 대응)
| 항목 | 검증 방법 | 주기 |
|------|-----------|------|
| 로그 무결성 | 해시 체인 전체 역추적 | 매일 00:00 |
| 계정 잠금 탐지 | faillock 명령어 원격 스캔 | 1시간마다 |
| 설정 파일 변조 | SHA-256 Checksum 비교 | 1시간마다 |
| 인증서 만료 | openssl enddate 파싱 | 매일 04:00 |
| 디스크 임계치 | df -h 자동 스캔 | 5분마다 |
| 비인가 포트 | ss -tlnp 스캔 | 1시간마다 |
---
## 7. 데이터 무결성 보호 (배포 중 롤백)
```python
class IntegrityShield:
def protect_rollback(self, task_id: str, executor: SSHExecutor):
"""롤백 수행 시에도 감사 기록 보존"""
# 롤백 직전 현재 해시 상태 기록
log_execution(task_id, executor.host, "ROLLBACK_START", 0, "롤백 시작")
# 실제 롤백
result = executor.execute_command("sh /app/scripts/rollback.sh")
if result["exit_code"] != 0:
# 롤백 실패 → 수동 점검 필요 상태로 변경 (다음 배포 차단)
db.update_task_status(task_id, "MANUAL_INTERVENTION_REQUIRED")
messenger.send_critical_alert(f"🔴 {task_id} 롤백 실패 — 수동 점검 필요")
```

185
docs/shell_scripts_guide.md Normal file
View File

@ -0,0 +1,185 @@
# [Guide] SM 운영용 원격 제어 쉘 스크립트
> 중계 PC에서 내부망 WEB/WAS/DB 서버를 원격 제어하는 표준 스크립트 모음.
> 모든 스크립트는 **JSON 출력** → AI 에이전트가 파싱하여 메신저로 전달.
---
## 스크립트 목록
| 번호 | 파일명 | 역할 |
|------|--------|------|
| 1 | `check_infra_health.sh` | WEB/WAS/DB 포트·Ping 일괄 헬스체크 |
| 2 | `manage_service.sh` | 서비스 데몬 상태/기동/중지/재기동 |
| 3 | `monitor_resources.sh` | CPU/메모리/디스크 임계치 감시 |
| 4 | `collect_target_logs.sh` | 에러 로그 원격 추출 (grep + tail) |
| 5 | `check_db_status.sh` | DB 커넥션·Lock 세션 진단/해제 |
| 6 | `backup_and_clear.sh` | 로그 로테이션 및 오래된 파일 정비 |
| 7 | `security_audit.sh` | 계정 잠금·포트 스캔·파일 위변조 감사 |
| 8 | `ssl_expiry_detector.sh` | SSL 인증서 만료일 사전 감지 |
| 9 | `self_healing_defense.sh` | 디스크 임계치 초과 시 자동 정비 트리거 |
| 10 | `config_integrity_check.sh` | 설정 파일 SHA-256 무결성 검증 |
| 11 | `cron_health_checker.sh` | 크론탭 배치 가동성 진단 |
| 12 | `esb_channel_monitor.sh` | ESB 연계 채널 상태 및 큐 임계치 감시 |
| 13 | `audit_session_logger.sh` | 원격 명령 실행 이력 감사 기록 |
---
## 공통 규칙
```bash
# 모든 스크립트 공통 패턴
#!/bin/bash
declare -A SERVER_HOSTS=([WEB]="192.168.10.11" [WAS]="192.168.10.12" [DB]="192.168.10.13")
declare -A SERVER_USERS=([WEB]="webadmin" [WAS]="wasadmin" [DB]="postgres")
# SSH 실행 공통 함수
execute_remote_cmd() {
local host=$1; local user=$2; local cmd=$3
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 "${user}@${host}" "${cmd}" 2>/dev/null
}
# 출력: JSON (AI 파싱용)
# 로그: /app/agent/logs/YYYYMMDD.log
```
---
## 1. check_infra_health.sh
```bash
#!/bin/bash
# 사용법: ./check_infra_health.sh
# 출력: {"timestamp":"...","results":[{"server":"WEB","ping":"UP","port_status":"OPEN"}, ...]}
check_node() {
local name=$1 ip=$2 port=$3
ping -c 1 -W 1 "${ip}" > /dev/null 2>&1 && ping_s="UP" || ping_s="DOWN"
timeout 2 bash -c "cat < /dev/null > /dev/tcp/${ip}/${port}" 2>/dev/null && port_s="OPEN" || port_s="CLOSED"
echo "{\"server\":\"${name}\",\"ip\":\"${ip}\",\"ping\":\"${ping_s}\",\"port_status\":\"${port_s}\"}"
}
WEB_J=$(check_node "WEB" "192.168.10.11" "80")
WAS_J=$(check_node "WAS" "192.168.10.12" "8080")
DB_J=$(check_node "DB" "192.168.10.13" "5432")
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"results\":[${WEB_J},${WAS_J},${DB_J}]}"
```
---
## 2. manage_service.sh
```bash
#!/bin/bash
# 사용법: ./manage_service.sh [WEB|WAS|DB] [status|start|stop|restart]
TARGET=$1; ACTION=$2
declare -A SERVICE_NAMES=([WEB]="nginx" [WAS]="tomcat" [DB]="postgresql")
HOST=${SERVER_HOSTS[$TARGET]}; USER=${SERVER_USERS[$TARGET]}; SVC=${SERVICE_NAMES[$TARGET]}
case ${ACTION} in
status)
RAW=$(execute_remote_cmd "$HOST" "$USER" "systemctl is-active ${SVC}")
[ "$RAW" = "active" ] && STATUS="RUNNING" || STATUS="STOPPED"
echo "{\"server\":\"${TARGET}\",\"service\":\"${SVC}\",\"status\":\"${STATUS}\"}"
;;
start|stop|restart)
RAW=$(execute_remote_cmd "$HOST" "$USER" "sudo systemctl ${ACTION} ${SVC}")
[ $? -eq 0 ] && RESULT="SUCCESS" || RESULT="FAILED"
echo "{\"server\":\"${TARGET}\",\"service\":\"${SVC}\",\"result\":\"${RESULT}\",\"action\":\"${ACTION}\"}"
;;
esac
```
---
## 3. collect_target_logs.sh
```bash
#!/bin/bash
# 사용법: ./collect_target_logs.sh [WEB|WAS|DB] [라인수(기본100)]
TARGET=$1; LINES=${2:-100}
declare -A LOG_PATHS=([WEB]="/var/log/nginx/error.log" [WAS]="/app/tomcat/logs/catalina.out" [DB]="/var/log/postgresql/postgresql.log")
HOST=${SERVER_HOSTS[$TARGET]}; USER=${SERVER_USERS[$TARGET]}; LOG=${LOG_PATHS[$TARGET]}
REMOTE_CMD="tail -n ${LINES} ${LOG} | awk '/Exception|ERROR|Error|CRITICAL|FATAL/{print \"[ALERT] \" \$0; next}{print \$0}'"
OUTPUT=$(execute_remote_cmd "$HOST" "$USER" "$REMOTE_CMD")
CLEAN=$(echo "$OUTPUT" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
echo "{\"server\":\"${TARGET}\",\"log_path\":\"${LOG}\",\"lines\":${LINES},\"log_data\":\"${CLEAN}\"}"
```
---
## 5. check_db_status.sh
```bash
#!/bin/bash
# 사용법: ./check_db_status.sh DB check (Lock 세션 조회)
# ./check_db_status.sh DB kill 1234 (PID 강제 해제)
TARGET=$1; ACTION=$2; PID=$3
HOST=${SERVER_HOSTS[$TARGET]}; USER=${SERVER_USERS[$TARGET]}
case $ACTION in
check)
SQL="SELECT json_build_object('total_connections',(SELECT count(*) FROM pg_stat_activity),'locked_sessions',(SELECT coalesce(json_agg(json_build_object('blocked_pid',pid,'age',clock_timestamp()-query_start,'query',query)),'{}'::json) FROM pg_stat_activity WHERE wait_event_type='Lock'));"
execute_remote_cmd "$HOST" "$USER" "psql -d mes_db -t -c \"${SQL}\"" | tr -d '\n' | sed 's/ //g'
;;
kill)
[ -z "$PID" ] && echo '{"status":"ERROR","message":"PID 필요"}' && exit 1
OUT=$(execute_remote_cmd "$HOST" "$USER" "psql -d mes_db -t -c \"SELECT pg_terminate_backend(${PID});\"")
[[ "$OUT" == *"t"* ]] && RESULT="SUCCESS" || RESULT="FAILED"
echo "{\"server\":\"${TARGET}\",\"action\":\"KILL\",\"pid\":${PID},\"result\":\"${RESULT}\"}"
;;
esac
```
---
## 크론탭 스케줄러 등록
```bash
#!/bin/bash
# agent_self_scheduler.sh install — 모든 감시 스크립트 자동 등록
cat << EOF > /tmp/agent_cron.tmp
# 5분마다: 자원 감시 + 자가 치유
*/5 * * * * /app/agent/bin/self_healing_defense.sh WEB > /dev/null 2>&1
*/5 * * * * /app/agent/bin/self_healing_defense.sh WAS > /dev/null 2>&1
*/5 * * * * /app/agent/bin/self_healing_defense.sh DB > /dev/null 2>&1
# 1시간마다: 보안 감사
0 */1 * * * /app/agent/bin/security_audit.sh WEB > /dev/null 2>&1
0 */1 * * * /app/agent/bin/security_audit.sh WAS > /dev/null 2>&1
0 */1 * * * /app/agent/bin/security_audit.sh DB > /dev/null 2>&1
# 매일 03:00: 로그 정비
0 3 * * * /app/agent/bin/backup_and_clear.sh WAS > /dev/null 2>&1
# 매일 04:00: SSL 인증서 만료 스캔
0 4 * * * /app/agent/bin/ssl_expiry_detector.sh WEB > /dev/null 2>&1
# 10분마다: ESB 연계 감시
*/10 * * * * /app/agent/bin/esb_channel_monitor.sh 192.168.10.21 > /dev/null 2>&1
EOF
crontab /tmp/agent_cron.tmp
rm /tmp/agent_cron.tmp
echo '{"status":"SUCCESS","message":"크론탭 스케줄 등록 완료"}'
```
---
## 알림 웹훅 전송 (`alert_webhook_sender.sh`)
```bash
#!/bin/bash
# 사용법: ./alert_webhook_sender.sh "WAS" "CRITICAL" "디스크 95% 초과"
SERVER=$1; LEVEL=$2; MSG=$3
WEBHOOK="http://192.168.10.50:8080/api/v1/infrastructure/alerts"
PAYLOAD="{\"system_source\":\"PC_MIDDLEWARE_AGENT\",\"target_node\":\"${SERVER}\",\"severity_level\":\"${LEVEL}\",\"incident_message\":\"${MSG}\",\"occurred_at\":\"$(date '+%Y-%m-%d %H:%M:%S')\"}"
CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -d "${PAYLOAD}" --connect-timeout 3 "${WEBHOOK}")
[ "$CODE" -eq 200 ] || [ "$CODE" -eq 201 ] \
&& echo "{\"status\":\"SUCCESS\",\"code\":\"${CODE}\"}" \
|| echo "{\"status\":\"FAILED\",\"code\":\"${CODE}\"}"
```

96
docs/system_spec.md Normal file
View File

@ -0,0 +1,96 @@
# [Specification] GUARDiA 시스템 아키텍처 명세
## 1. 프로젝트 개요
### 1.1. 구축 배경
- 1,000개 이상의 관공서 SM 사이트, 10,000+ 운영 환경 관리
- 레거시 서버: 에이전트 설치 불가, 수동 FTP/SSH 반복 작업 만연
- 목표: 메신저 자연어 명령 → AI → 에이전트리스 자동 배포·운영
### 1.2. 핵심 가치
- **ChatOps**: 메신저가 유일한 터미널
- **Agentless**: SSH/FTP 표준 프로토콜만 사용
- **Human-in-the-loop**: PM 최종 승인 후 실행
---
## 2. 전체 아키텍처 (3-Layer)
```
┌─────────────────────────────────────────────────────────┐
│ INTAKE LAYER (현장 접수) │
│ 메신저 앱/웹 → Webhook → 입력 검증 모듈 │
└───────────────────────┬─────────────────────────────────┘
┌───────────────────────▼─────────────────────────────────┐
│ CONTROL LAYER (지능형 제어) │
│ ① SR/Ops 작업 관리 엔진 (상태: RECEIVED→COMPLETED) │
│ ② RBAC 보안 승인 엔진 (담당자→대리→상위 에스컬레이션)│
│ ③ sLLM Parser (자연어→JSON 정형화) │
│ ④ Claude Code 에이전트 (코드수정·스크립트 생성) │
│ ⑤ HITL 모듈 (PM 최종 검증 대기) │
└───────────────────────┬─────────────────────────────────┘
┌───────────────────────▼─────────────────────────────────┐
│ EXECUTION LAYER (에이전트리스 실행) │
│ SSH Executor / SFTP Client → 대상 서버(1,000+) │
│ Command Sanitizer → Audit Logger (Hash Chain) │
└─────────────────────────────────────────────────────────┘
```
---
## 3. 기술 스택
| 구분 | 기술 | 버전/비고 |
|------|------|-----------|
| Backend | Python / FastAPI | 3.11+ |
| LLM | Llama-3-8B 또는 Solar-10.7B | 온프레미스, 4-bit 양자화 |
| Infra 제어 | paramiko (SSH/SFTP) | 에이전트리스 |
| DB | PostgreSQL | CMDB + SR + Audit |
| 메신저 연동 | Webhook (REST) | 슬랙/잔디/자체앱 |
| 보안 | AES-256 (자격증명), SHA-256 (감사로그 해시체인) | |
---
## 4. 사용자 역할별 지원 기능
### 4.1. Developer (배포)
- 로컬 빌드 파일(class, html, js, img) 메신저 첨부 → 자동 배포
- 정적 파일: 카피 즉시 반영 (WAS 재기동 불필요)
- 동적 파일(class): 무중단 롤링 재기동 (WAS #1 → 헬스체크 → WAS #2)
### 4.2. SM 운영자
- 원격 로그 분석: 에러 키워드 grep, 타임라인 추출
- 인프라 점검: 디스크/CPU/메모리/포트 임계치 감시
- 자원 정비: 로그 로테이션, 오래된 아카이브 삭제
- SSL 인증서 만료일 사전 감지
### 4.3. PM 관리자
- SR 진척도 및 배포 이력 타임라인 조회
- 민감 시스템 배포 시 2차 승인 워크플로우 제어
- 배포 결과 HITL 최종 확인 후 고객 완료 통보
---
## 5. 핵심 보안 정책
| 정책 | 내용 |
|------|------|
| 자격증명 저장 | AES-256 암호화 DB 저장, 실행 시 메모리 로드 |
| SSH 접근 | 관제 전용 계정(opsagent) + sudoers 화이트리스트 |
| 명령어 필터 | Blacklist: `rm -rf /`, `mkfs`, `drop`, `truncate` |
| 감사 로그 | SHA-256 해시 체이닝 — 위변조 즉시 탐지 |
| Kill-Switch | 메신저 '정지' 명령 → 즉시 중단 + 롤백 |
| 폐쇄망 | 외부 API 호출 금지, sLLM 온프레미스만 허용 |
---
## 6. 개발 로드맵
| Phase | 내용 | 목표 |
|-------|------|------|
| P1 | 메신저 웹훅 + DB 티켓팅 시스템 | SR 접수 → DB 저장 |
| P2 | sLLM 파서 + 승인 워크플로우 | 자연어 → JSON → HITL |
| P3 | SSH/SFTP 배포 엔진 + 롤링 재기동 | 에이전트리스 실행 |
| P4 | End-to-End 자동화 루프 완성 | SR→배포→알림 전 과정 |

146
guardia_setup.ps1 Normal file
View File

@ -0,0 +1,146 @@
# ============================================================
# GUARDiA - Claude Code + MCP Filesystem 자동 설치 스크립트
# PowerShell을 관리자 권한으로 실행 후 붙여넣기
# ============================================================
$ErrorActionPreference = "Stop"
function Write-Step($msg) {
Write-Host "`n>>> $msg" -ForegroundColor Cyan
}
function Write-OK($msg) {
Write-Host " [OK] $msg" -ForegroundColor Green
}
function Write-Warn($msg) {
Write-Host " [!!] $msg" -ForegroundColor Yellow
}
# ──────────────────────────────────────────
# 1. C:\GUARDiA 디렉토리 생성
# ──────────────────────────────────────────
Write-Step "1/5 C:\GUARDiA 디렉토리 확인"
if (-not (Test-Path "C:\GUARDiA")) {
New-Item -ItemType Directory -Path "C:\GUARDiA" | Out-Null
Write-OK "C:\GUARDiA 생성 완료"
} else {
Write-OK "C:\GUARDiA 이미 존재"
}
# ──────────────────────────────────────────
# 2. Claude Code 설치
# ──────────────────────────────────────────
Write-Step "2/5 Claude Code 설치"
$claudePath = "$env:USERPROFILE\.local\bin\claude.exe"
$claudeInstalled = $false
if (Get-Command claude -ErrorAction SilentlyContinue) {
Write-OK "claude 명령어 이미 사용 가능"
$claudeInstalled = $true
} elseif (Test-Path $claudePath) {
Write-OK "claude.exe 존재 - PATH만 추가 필요"
} else {
Write-Warn "Claude Code 설치 중..."
try {
Invoke-RestMethod https://claude.ai/install.ps1 | Invoke-Expression
Write-OK "Claude Code 설치 완료"
} catch {
Write-Warn "공식 설치 실패 - npm으로 재시도"
if (Get-Command npm -ErrorAction SilentlyContinue) {
npm install -g @anthropic-ai/claude-code
Write-OK "npm 설치 완료"
} else {
Write-Host "`n[오류] npm도 없습니다. Node.js를 먼저 설치해 주세요:" -ForegroundColor Red
Write-Host " https://nodejs.org/en/download" -ForegroundColor Yellow
exit 1
}
}
}
# ──────────────────────────────────────────
# 3. PATH 등록
# ──────────────────────────────────────────
Write-Step "3/5 PATH 환경변수 등록"
$localBin = "$env:USERPROFILE\.local\bin"
$npmGlobal = (npm config get prefix 2>$null)
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User")
$pathsToAdd = @($localBin)
if ($npmGlobal) { $pathsToAdd += "$npmGlobal" }
foreach ($p in $pathsToAdd) {
if ($currentPath -notlike "*$p*") {
[Environment]::SetEnvironmentVariable("PATH", "$currentPath;$p", [EnvironmentVariableTarget]::User)
$env:PATH = "$env:PATH;$p"
Write-OK "PATH 추가: $p"
} else {
Write-OK "이미 PATH에 있음: $p"
}
}
# ──────────────────────────────────────────
# 4. .mcp.json 생성 (Claude Code 프로젝트 설정)
# ──────────────────────────────────────────
Write-Step "4/5 C:\GUARDiA\.mcp.json 생성"
$mcpConfig = @{
mcpServers = @{
filesystem = @{
command = "cmd"
args = @("/c", "npx", "-y", "@modelcontextprotocol/server-filesystem", "C:/GUARDiA")
}
}
} | ConvertTo-Json -Depth 5
$mcpPath = "C:\GUARDiA\.mcp.json"
Set-Content -Path $mcpPath -Value $mcpConfig -Encoding UTF8
Write-OK ".mcp.json 저장 완료: $mcpPath"
# ──────────────────────────────────────────
# 5. Claude Desktop config 업데이트
# ──────────────────────────────────────────
Write-Step "5/5 Claude Desktop 설정 업데이트"
$desktopConfigDir = "$env:APPDATA\Claude"
$desktopConfigPath = "$desktopConfigDir\claude_desktop_config.json"
if (-not (Test-Path $desktopConfigDir)) {
New-Item -ItemType Directory -Path $desktopConfigDir | Out-Null
}
$desktopConfig = @{
mcpServers = @{
filesystem = @{
command = "npx"
args = @("-y", "@modelcontextprotocol/server-filesystem", "C:\\GUARDiA")
}
}
} | ConvertTo-Json -Depth 5
Set-Content -Path $desktopConfigPath -Value $desktopConfig -Encoding UTF8
Write-OK "claude_desktop_config.json 저장 완료"
# ──────────────────────────────────────────
# 완료 메시지
# ──────────────────────────────────────────
Write-Host "`n============================================" -ForegroundColor Magenta
Write-Host " 설치 완료!" -ForegroundColor Magenta
Write-Host "============================================" -ForegroundColor Magenta
Write-Host @"
다음 단계:
1. PowerShell 창을 닫고 새로 여세요 (PATH 적용)
2. Claude Desktop을 완전히 종료 재시작하세요
3. Claude Code 실행:
cd C:\GUARDiA
claude
4. Claude Code 안에서 확인:
/mcp (MCP 서버 상태)
/doctor (전체 진단)
"@ -ForegroundColor White

View File

@ -0,0 +1,145 @@
---
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
```

View File

@ -0,0 +1,93 @@
---
name: guardia-deploy
description: >
GUARDiA 프로젝트에서 SSH/SFTP 기반 에이전트리스 배포 엔진 코드를 작성하거나
수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라:
- paramiko 관련 코드 작성/수정
- 롤링 배포 로직 구현
- 헬스체크 / 롤백 로직 작성
- 배포 파이프라인 API 엔드포인트 작성
- 파일 무결성(MD5/SHA-256) 검증 코드
---
# GUARDiA 배포 엔진 스킬
## 핵심 원칙
1. **백업 우선**: 배포 전 반드시 타임스탬프 포함 백업 (`app.jar.bak_YYYYMMDD_HHMMSS`)
2. **무결성 검증**: SFTP 전송 후 MD5 로컬 vs 원격 비교 필수
3. **헬스체크 강제**: 재기동 후 30초 이내 HTTP 200 미확인 시 즉시 롤백
4. **롤링 배포**: WAS #1 완료·검증 후 WAS #2 시작 (동시 배포 금지)
5. **감사 로그**: 성공/실패 모두 `log_execution()` 호출 필수
## 환경 변수
```python
SSH_TIMEOUT = 300 # SSH 명령 실행 타임아웃 (초)
HEALTH_TIMEOUT = 30 # 헬스체크 최대 대기 (초)
HEALTH_INTERVAL = 2 # 헬스체크 폴링 간격 (초)
DISK_THRESHOLD = 90 # 배포 전 디스크 임계치 (%)
MAX_BATCH_SIZE = 50 # 티어2 병렬 배포 단위
```
## 금지 사항
- root 계정으로 SSH 접속 금지 → opsagent + sudoers 사용
- `rm -rf`, `mkfs`, `format`, `drop table` 등 Blacklist 명령 차단
- 자격증명(비밀번호) 로그 출력 금지
- 헬스체크 없이 다음 노드 진행 금지
## 코드 패턴
### SSH 실행
```python
result = executor.execute_command("sh /app/scripts/startup.sh")
if result["exit_code"] != 0:
raise DeploymentError(f"startup 실패: {result['stderr']}")
```
### 파일 전송 + 무결성
```python
remote_md5 = executor.upload_file(local_path, remote_path)
local_md5 = hashlib.md5(open(local_path, 'rb').read()).hexdigest()
assert local_md5 == remote_md5, "MD5 불일치"
```
### 헬스체크
```python
deadline = time.time() + HEALTH_TIMEOUT
while time.time() < deadline:
try:
r = requests.get(f"http://{ip}:8080", timeout=3)
if r.status_code == 200:
return True
except:
pass
time.sleep(HEALTH_INTERVAL)
return False # 실패 → 즉시 rollback 트리거
```
## 에러 처리 표준
```python
try:
# 배포 로직
except DeploymentError as e:
self._rollback(executor, node, backup_ts)
log_execution(sr_id, node["ip"], "DEPLOY_FAILED", 1, str(e))
messenger.notify_failure(sr_id, str(e))
return False
finally:
executor.close()
```
## 정적 vs 동적 자원 판별
```python
STATIC_EXTENSIONS = {".html", ".js", ".css", ".png", ".jpg", ".gif", ".svg"}
DYNAMIC_EXTENSIONS = {".class", ".jar", ".war", ".properties"}
def requires_restart(filename: str) -> bool:
ext = Path(filename).suffix.lower()
return ext in DYNAMIC_EXTENSIONS
```

View File

@ -0,0 +1,186 @@
---
name: guardia-messenger
description: >
GUARDiA 프로젝트에서 메신저 연동 및 sLLM 파서 코드를 작성하거나
수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라:
- FastAPI Webhook 엔드포인트 구현
- WebSocket 실시간 중계 서버 코드
- sLLM 자연어 파서 프롬프트 엔지니어링
- 메신저 응답 포맷 및 인터랙티브 버튼 구현
- Proactive 챗봇 대화 맥락 분석
---
# GUARDiA 메신저 연동 스킬
## sLLM 파서 원칙
### 출력 포맷 강제 (절대 준수)
```
- JSON 외 텍스트 출력 금지
- 부연 설명, 인삿말 포함 금지
- temperature: 0.1 (결정론적 출력)
- response_format: {"type": "json_object"}
```
### 시스템 프롬프트 기본 구조
```python
SYSTEM_PROMPT = """
당신은 인프라 자동화 시스템의 핵심 파서입니다.
1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다.
2. 부연 설명, 인삿말, 안내 텍스트는 절대 포함하지 않습니다.
3. CMDB에 정의된 정확한 서버 ID와 매핑되지 않는 정보는 "UNKNOWN"으로 처리합니다.
4. 배포 명령 시 반드시 'requires_approval'(boolean) 값을 판단합니다.
출력 포맷 (반드시 이 구조만 사용):
{
"intent_type": "DEPLOY_UPGRADE|LOG_ANALYSIS|SERVICE_RESTART|SSL_CHECK|DB_QUERY|CRON_MANAGE",
"institution": "기관명 또는 UNKNOWN",
"system_name": "시스템명 또는 UNKNOWN",
"infrastructure_layer": "WEB|WAS|DB|ESB",
"target_nodes": [],
"action_sequence": [],
"deploy_artifacts": [],
"requires_approval": false,
"priority": "HIGH|MEDIUM|LOW"
}
"""
```
## Webhook 수신 표준
```python
@app.post("/messenger/webhook")
async def receive_message(request: Request):
# 1. 시크릿 키 검증
signature = request.headers.get("X-Messenger-Signature")
if not verify_signature(signature, await request.body()):
raise HTTPException(status_code=403)
payload = await request.json()
user_id = payload.get("user_id")
text = payload.get("text", "")
files = payload.get("files", [])
# 2. 봇 자신의 메시지는 무시 (무한 루프 방지)
if payload.get("bot") or not text.strip():
return {"status": "IGNORED"}
# 3. 파일 스테이징
staged = await stage_uploaded_files(files, user_id)
# 4. sLLM 파싱
parsed = parse_natural_language(text)
# 5. SR 생성 및 라우팅
sr_id = create_ops_task(parsed, user_id, staged)
route_to_engine(sr_id, parsed)
return {"status": "ACCEPTED", "sr_id": sr_id}
```
## 메신저 응답 포맷
### 텍스트 응답
```python
def send_text_response(room_id: str, text: str):
messenger.post_message(room_id, {
"sender": "GUARDiA-Bot",
"text": text,
"msg_type": "CHAT"
})
```
### 인터랙티브 버튼 응답
```python
def send_interactive_response(room_id: str, text: str, action_code: str, label: str):
messenger.post_message(room_id, {
"sender": "GUARDiA-Bot",
"text": text,
"is_widget": True,
"interactive_action": {
"type": "BUTTON",
"label": label,
"command_code": action_code
}
})
# 사용 예
send_interactive_response(
room_id,
"🤖 DB 지연 징후 감지. 대기 쿼리를 확인할까요?",
"FETCH_DB_LOCKS",
"대기 쿼리 확인"
)
```
### 승인 요청 응답
```python
def request_approval(approver_id: str, sr_id: str, summary: str):
messenger.post_direct_message(approver_id, {
"sender": "GUARDiA-Bot",
"text": f"[{sr_id}] 배포 승인 요청\n{summary}",
"is_widget": True,
"interactive_action": {
"type": "APPROVAL_BUTTONS",
"approve_url": f"/api/sr/approve/{sr_id}",
"reject_url": f"/api/sr/reject/{sr_id}"
}
})
```
## 금지 사항
- 메신저 응답에 서버 IP, 비밀번호, SSH 계정 노출 금지
- 봇 응답 메시지에 다시 파서 실행 금지 (무한 루프)
- 에러 메시지에 스택트레이스 전체 노출 금지 → SR ID와 요약만 전달
- 파일 스테이징 없이 바로 원격 서버 전송 금지 → 반드시 임시 디렉토리 거침
## 파일 스테이징 표준
```python
import aiofiles
from pathlib import Path
STAGING_BASE = Path("/app/guardia/staging")
async def stage_uploaded_files(files: list, user_id: str) -> list:
"""메신저 첨부 파일을 임시 디렉토리에 다운로드"""
staged = []
sr_dir = STAGING_BASE / user_id / datetime.now().strftime("%Y%m%d_%H%M%S")
sr_dir.mkdir(parents=True, exist_ok=True)
for f in files:
filename = f["name"]
url = f["url"]
dest = sr_dir / filename
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
async with aiofiles.open(dest, "wb") as fp:
await fp.write(await resp.read())
staged.append({"name": filename, "path": str(dest), "type": classify_artifact(filename)})
return staged
def classify_artifact(filename: str) -> str:
ext = Path(filename).suffix.lower()
if ext in {".class", ".jar", ".war"}: return "DYNAMIC"
if ext in {".html", ".js", ".css"}: return "STATIC"
if ext in {".png", ".jpg", ".gif"}: return "STATIC"
return "UNKNOWN"
```
## Nginx 업스트림 설정 (WebSocket)
```nginx
location /ws {
proxy_pass http://was_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 86400s;
proxy_buffering off;
client_max_body_size 100M;
}
```

BIN
메신져.docx Normal file

Binary file not shown.

BIN
특허출원.docx Normal file

Binary file not shown.