fix: Phase 5 진단 핫픽스 + Phase 6 run_sql 안전 가드
진단 보고서(plans/...phase5-사용자체크리스트.md) 기반 7건 코드 이슈
수정 + Phase 6 잔여 항목 중 최우선인 run_sql 가드 구현.
핫픽스:
- nl2sql_worker.py: _list_drawings 파싱 버그(문자열 분리) HIGH
- nl2sql_worker.py: 5개 async 함수 blocking DB 연결 → to_thread MED
- ExperionDbContext.cs: KB DDL의 {} 문자가 String.Format placeholder로
오인되어 부팅 실패 → 별도 NpgsqlCommand 사용 HIGH
- KbIngestWorker: 단일 청크 임베딩 실패 시 전체 abort → 부분 인덱싱 LOW
- KbAuthService: 초기 비번 로그 평문 → 마스킹 + 콘솔 분리 출력 LOW
- KbQdrantClient: new HttpClient → IHttpClientFactory LOW
- OllamaController: plant_context.md 매 요청 파일 읽기 → mtime 캐시 LOW
Phase 6 — run_sql 가드:
- _validate_sql 강화: \b 단어 경계로 updated_at 오탐 제거, WITH 허용,
TRUNCATE/COPY 추가, 다중 세미콜론 차단
- _apply_sql_guards: LIMIT 미지정 시 SELECT * FROM (...) _capped LIMIT 1000
- _execute_sql_internal: 매 호출 SET statement_timeout = 30000
- SQL_MAX_ROWS / SQL_STATEMENT_TIMEOUT_MS 환경변수화
- 응답 JSON에 row_limit 필드 추가
- nl2sql_worker.py의 _run_sql / _query_with_nl에도 동일 적용
기타:
- .gitignore: storage/ 추가 (KB 업로드 원본 디렉토리)
- opencode.json: 모델 항목을 실제 서빙 모델(Qwen3.6-27B-FP8 / 256K)로 동기화
검증:
- dotnet build: 경고 0건, 에러 0건
- python3 -m py_compile: OK
- _apply_sql_guards / _validate_sql 스모크 테스트 통과
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
51
CLAUDE.md
51
CLAUDE.md
@@ -142,13 +142,62 @@
|
||||
- Qdrant 5개 컬렉션 생성 확인 — `curl http://localhost:6333/collections`
|
||||
|
||||
#### 잔여 작업
|
||||
- Phase 6 (보강 도구): `query_events`, `summarize_events`, `active_alarms`, `find_tags`, `generate_status_report`, `run_sql` LIMIT/timeout
|
||||
- Phase 6 (보강 도구): `query_events`, `summarize_events`, `active_alarms`, `find_tags`, `generate_status_report` (~~`run_sql` LIMIT/timeout~~ → 완료, 아래 참조)
|
||||
- Phase 7 (옵션): NL2SQL 의도 라우터, 대화 요약, 에이전트 모드, KB 청크 미리보기 UI
|
||||
- Phase 5 후순위: 시계열 미니 스파클라인, 툴 카드 메시지 영구 보존
|
||||
- 결정 보류: 현장 재고 데이터 출처, 임베딩 모델 BGE-M3 마이그레이션
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 후속 — 진단 보고서 핫픽스 + Phase 6 첫 항목 (2026-05-14)
|
||||
|
||||
#### 배경
|
||||
`plans/LLM채팅+지식증강-phase5-사용자체크리스트.md` 진단 보고서에서 도출된 6건의 코드 이슈와 Phase 6 잔여 항목 중 우선순위가 가장 높은 `run_sql` 안전 가드를 함께 처리.
|
||||
|
||||
#### 수정 파일
|
||||
|
||||
| 파일 | 수정 내용 |
|
||||
|------|----------|
|
||||
| `mcp-server/worker/nl2sql_worker.py` | (HIGH) `_list_drawings` `dict(zip(columns, row[0]))` → `[row[0] for row in rows]` 버그 수정 — 문자열이 문자 단위로 분리되어 잘못된 dict 생성되던 문제 |
|
||||
| `mcp-server/worker/nl2sql_worker.py` | (MED) `_run_sql`/`_query_pv_history`/`_get_tag_metadata`/`_list_drawings`/`_query_with_nl` 5개 함수가 async 안에서 `psycopg.connect()` blocking 호출 → `_aget_db_connection()` 헬퍼(`asyncio.to_thread`)로 격리 |
|
||||
| `mcp-server/worker/nl2sql_worker.py` | (Phase 6) `_validate_sql` + `_apply_sql_guards` 추가, `_run_sql`·`_query_with_nl`에 auto-LIMIT/statement_timeout 적용 |
|
||||
| `mcp-server/server.py` | (Phase 6) `_validate_sql` 강화 (단어 경계 매칭으로 `updated_at` 오탐 제거, `WITH` 허용, 세미콜론 다중문장 차단, `TRUNCATE`/`COPY` 추가). `SQL_MAX_ROWS=1000`, `SQL_STATEMENT_TIMEOUT_MS=30000` 환경변수화. `_apply_sql_guards`로 LIMIT 미지정 시 서브쿼리 wrap. `_execute_sql_internal`에서 매 호출 `SET statement_timeout` 적용. `run_sql` 도구 docstring 갱신 |
|
||||
| `src/Infrastructure/Kb/KbIngestWorker.cs` | (LOW) 단일 청크 임베딩 실패 시 전체 abort → skip 후 부분 인덱싱, `error_message`에 `"부분 인덱싱: N/M 청크"` 기록. 전 청크 실패 시에만 throw |
|
||||
| `src/Infrastructure/Kb/KbAuthService.cs` | (LOW) 초기 비번 로그 평문 → 마스킹(앞 4자만)으로 변경, 평문은 `Console.Out`으로 1회 분리 출력 (파일 로거 미포함) |
|
||||
| `src/Infrastructure/Kb/KbQdrantClient.cs` | (LOW) `new HttpClient` 직접 생성 → `IHttpClientFactory.CreateClient("KbQdrant")` |
|
||||
| `src/Web/Program.cs` | (LOW) `AddHttpClient("KbQdrant")` 팩토리 등록 (BaseAddress + 30s Timeout) |
|
||||
| `src/Web/Controllers/OllamaController.cs` | (LOW) `LoadPlantContext()` 매 요청 `File.ReadAllText` → mtime 기반 캐시 (lock 보호) |
|
||||
| `src/Infrastructure/Database/ExperionDbContext.cs` | (HIGH) KB DDL의 `ExecuteSqlRawAsync` 사용 → `JSONB '{}'` / `TEXT[] '{}'` / 시드 JSON의 중괄호가 `String.Format` placeholder로 오인되어 부팅 실패. 별도 `NpgsqlConnection` + `NpgsqlCommand.ExecuteNonQueryAsync`로 전환 |
|
||||
| `.gitignore` | KB 업로드 원본 디렉토리 `storage/` 추가 (런타임 데이터는 git 추적 제외) |
|
||||
|
||||
#### Phase 6 — `run_sql` 가드 동작
|
||||
|
||||
| 가드 | 동작 |
|
||||
|------|------|
|
||||
| 키워드 차단 | `\b{kw}\b` 단어 경계 매칭 — `INSERT/UPDATE/DELETE/DROP/ALTER/CREATE/GRANT/REVOKE/TRUNCATE/COPY/EXEC` |
|
||||
| 시작 키워드 | `SELECT` 또는 `WITH`만 허용 (선행 `(` 허용) |
|
||||
| 다중 문장 | 끝의 `;` 제외하고 `;` 포함 시 차단 |
|
||||
| 자동 LIMIT | 꼬리 `LIMIT N [OFFSET M]` 미지정 시 `SELECT * FROM ({sql}) _capped LIMIT 1000` wrap |
|
||||
| 타임아웃 | `SET statement_timeout = 30000` (커넥션별) |
|
||||
| 환경변수 | `SQL_MAX_ROWS`, `SQL_STATEMENT_TIMEOUT_MS`로 조정 가능 |
|
||||
|
||||
응답 JSON에 `row_limit` 필드 추가 — 클라이언트가 잘림 여부 판단 가능.
|
||||
|
||||
#### 빌드/검증 결과
|
||||
- `dotnet build src/Web/ExperionCrawler.csproj` — 경고 0건, **에러 0건**
|
||||
- `python3 -m py_compile mcp-server/server.py mcp-server/worker/nl2sql_worker.py` — OK
|
||||
- `_apply_sql_guards` / `_validate_sql` 스모크 테스트 통과
|
||||
- `SELECT *` → wrap 적용, `LIMIT 10` / `LIMIT 10 OFFSET 5` → 그대로
|
||||
- `SELECT 1; DROP TABLE x` → 차단, `WITH t AS ...` → 허용
|
||||
- `SELECT updated_at` → 허용 (단어 경계 매칭으로 `UPDATE` 키워드 오탐 제거)
|
||||
|
||||
#### 잔여 (이 커밋 이후)
|
||||
- Phase 6 나머지: `query_events`, `summarize_events`, `active_alarms`, `find_tags`, `generate_status_report`
|
||||
- Phase 7 옵션, Phase 5 후순위는 변동 없음
|
||||
- `appsettings.json`의 `AdminInitialPassword: "admin"`은 사용자 지시로 유지 (운영 전 제거 권장)
|
||||
|
||||
---
|
||||
|
||||
### 기능 추가 — OPC UA 서버 기능 (2026-04-15)
|
||||
|
||||
#### 배경
|
||||
|
||||
Reference in New Issue
Block a user