Invenqor
요약. Invenqor: 자율 개선 회차 30회, 릴리즈 15건. 최근 릴리즈 v0.2.33 (자산 25개). 건강 D
- 30회차
- 1프로젝트
- 15배포 준비 완료
- 0릴리즈 진행 중
- 1병합 완료
- 1검토 대기
- 6검증 실패
- 7변경 없음
- 0실행 오류
- $94.30비용
- 5시간 30분에이전트 시간
현황
- 저장소
- https://github.com/hkjang/invenqor
- 마지막 회차
- 2026-09-12 08:28 KST — 🚀 릴리즈 merged PR #18, released v0.2.33
- 최근 릴리즈
- v0.2.33 — released · 자산 25개 (이전 v0.2.32: 25개) 전체 릴리즈 →
회차 이력
| 일시 | 프로젝트 | 결과 |
|---|---|---|
| 2026-09-12 08:28 | Invenqor | 배포 준비 완료 merged PR #18, released v0.2.33 |
| 2026-09-11 11:09 | Invenqor | 배포 준비 완료 merged PR #17, released v0.2.32 |
| 2026-09-10 22:37 | Invenqor | 검증 실패 verify failed: secrets in diff |
| 2026-09-10 17:58 | Invenqor | 배포 준비 완료 merged PR #16, released v0.2.31 |
| 2026-09-10 10:07 | Invenqor | 배포 준비 완료 merged PR #15, released v0.2.30 |
| 2026-09-09 13:23 | Invenqor | 검증 실패 CI failed, PR open PR #14 |
| 2026-09-09 05:05 | Invenqor | 배포 준비 완료 merged PR #13, released v0.2.29 |
| 2026-09-08 23:28 | Invenqor | 배포 준비 완료 merged PR #12, released v0.2.28 |
| 2026-09-08 18:28 | Invenqor | 배포 준비 완료 merged PR #11, released v0.2.27 |
| 2026-09-08 14:24 | Invenqor | 배포 준비 완료 release-only, released v0.2.26 |
| 2026-09-08 12:28 | Invenqor | 병합 완료 merged PR #10, release missing |
| 2026-09-07 07:15 | Invenqor | 검증 실패 verify failed: 실패한 검증: cargo test --quiet (exit 127) |
| 2026-09-06 21:57 | Invenqor | 검증 실패 verify failed: 실패한 검증: cargo test --quiet (exit 127) |
| 2026-09-06 00:29 | Invenqor | 검증 실패 verify failed: 실패한 검증: cargo test --quiet (exit 127) |
| 2026-09-05 17:27 | Invenqor | 검증 실패 verify failed: 실패한 검증: cargo test --quiet (exit 127) |
| 2026-09-05 11:20 | Invenqor | 검토 대기 guarded files, PR open PR #9 |
| 2026-09-04 22:36 | Invenqor | 배포 준비 완료 merged PR #8, released v0.2.25 +25 assets |
| 2026-09-04 13:30 | Invenqor | 변경 없음 assets-only v0.2.19, assets for v0.2.19 +25 assets |
| 2026-09-04 13:25 | Invenqor | 변경 없음 assets-only v0.2.20, assets for v0.2.20 +25 assets |
| 2026-09-04 12:27 | Invenqor | 변경 없음 assets-only v0.2.21, assets for v0.2.21 +25 assets |
| 2026-09-04 11:57 | Invenqor | 변경 없음 assets-only v0.2.22, assets for v0.2.22 +25 assets |
| 2026-09-04 11:43 | Invenqor | 변경 없음 assets-only v0.2.23, assets for v0.2.23 +25 assets |
| 2026-09-04 10:56 | Invenqor | 변경 없음 assets-only, assets for v0.2.24 +25 assets |
| 2026-09-04 07:06 | Invenqor | 변경 없음 assets-only, assets missing |
| 2026-09-04 04:01 | Invenqor | 배포 준비 완료 merged PR #7, released v0.2.24 |
| 2026-09-03 20:22 | Invenqor | 배포 준비 완료 merged PR #6, released v0.2.23 |
| 2026-09-03 12:20 | Invenqor | 배포 준비 완료 merged PR #5, released v0.2.22 |
| 2026-09-03 06:39 | Invenqor | 배포 준비 완료 merged PR #4, released v0.2.21 |
| 2026-09-03 00:39 | Invenqor | 배포 준비 완료 merged PR #3, released v0.2.20 |
| 2026-09-02 18:20 | Invenqor | 배포 준비 완료 merged PR #2, released v0.2.19 |
비용·사용량
| 시각 | 프로젝트 | 단계 | 시간 | 턴 | 비용 | 토큰 입력/출력 | 종료 |
|---|---|---|---|---|---|---|---|
| 08:22 | Invenqor | 릴리즈 | 7분 | 41 | $2.66 | 2.9M / 16K | success |
| 08:14 | Invenqor | review | 4분 | 21 | $1.63 | 1.2M / 15K | success |
| 08:10 | Invenqor | 개선 | 20분 | 92 | $9.97 | 11.6M / 84K | success |
| 11:02 | Invenqor | 릴리즈 | 20분 | 41 | $2.55 | 2.8M / 17K | success |
| 10:41 | Invenqor | review | 3분 | 32 | $1.71 | 1.7M / 10K | success |
| 10:38 | Invenqor | 개선 | 8분 | 59 | $3.70 | 4.1M / 23K | success |
| 22:36 | Invenqor | 개선 | 39분 | 162 | $15.14 | 21.6M / 76K | success |
| 17:52 | Invenqor | 릴리즈 | 9분 | 65 | $2.96 | 3.3M / 23K | success |
| 17:42 | Invenqor | review | 5분 | 25 | $1.28 | 1.0M / 15K | success |
| 17:37 | Invenqor | 개선 | 7분 | 46 | $2.35 | 2.2M / 23K | success |
| 10:01 | Invenqor | 릴리즈 | 9분 | 65 | $3.21 | 3.7M / 23K | success |
| 09:50 | Invenqor | review | 4분 | 24 | $1.20 | 1000K / 12K | success |
| 09:46 | Invenqor | 개선 | 6분 | 43 | $2.25 | 2.2M / 20K | success |
| 13:20 | Invenqor | review | 2분 | 22 | $0.75 | 538K / 7K | success |
| 13:18 | Invenqor | 개선 | 5분 | 38 | $1.99 | 1.8M / 20K | success |
| 05:00 | Invenqor | 릴리즈 | 9분 | 60 | $2.89 | 3.2M / 22K | success |
| 04:49 | Invenqor | review | 2분 | 19 | $0.67 | 457K / 8K | success |
| 04:46 | Invenqor | 개선 | 6분 | 50 | $2.73 | 2.8M / 23K | success |
| 23:21 | Invenqor | 릴리즈 | 9분 | 47 | $2.17 | 2.1M / 17K | success |
| 23:10 | Invenqor | review | 3분 | 18 | $0.88 | 628K / 10K | success |
| 23:07 | Invenqor | 개선 | 7분 | 36 | $1.95 | 1.7M / 22K | success |
| 18:22 | Invenqor | 릴리즈 | 20분 | 54 | $2.48 | 2.5M / 19K | success |
| 18:02 | Invenqor | review | 3분 | 20 | $0.95 | 706K / 10K | success |
| 17:58 | Invenqor | 개선 | 8분 | 48 | $2.65 | 2.4M / 28K | success |
| 14:18 | Invenqor | 릴리즈 | 1시간 10분 | 62 | $3.16 | 3.6M / 22K | success |
| 12:28 | Invenqor | 릴리즈 | 0분 | 0 | $0.00 | 0 / 0 | unknown |
| 11:29 | Invenqor | review | 2분 | 22 | $0.86 | 707K / 8K | success |
| 11:26 | Invenqor | 개선 | 6분 | 49 | $2.36 | 2.3M / 20K | success |
| 07:15 | Invenqor | 개선 | 5분 | 33 | $1.46 | 1.1M / 16K | success |
| 21:57 | Invenqor | 개선 | 7분 | 61 | $3.24 | 3.5M / 27K | success |
아이디어 백로그 — 대기 14 / 전체 15
| 아이디어 | 가치/위험/크기 | 상태 | 메모 | 갱신 |
|---|---|---|---|---|
| 여러 목록 핸들러가 rows.Err() 를 확인하지 않아 부분 결과를 200 으로 돌려줌 | 3/1/M | 대기 | 남은 곳: listAgents(agents.go:712), 설정 목록·이력(settings.go:42,198), 자산 상세의 sources·history·relations 루프. storage 테스트용 공개 생성자 추가가 선행 과제. | 2026-09-12 |
| Query DSL 에 attributes.<키> 존재/부재 연산자가 없음 — 지금은 >= "" 우회가 필요하다 | 3/2/M | 대기 | != 가 키 없는 자산을 포함하도록 바뀐 뒤로 '값을 보고한 자산만'을 표현할 수단이 더 필요해졌다. 문법(clausePattern)과 Describe() 를 함께 고쳐야 한다. | 2026-09-12 |
| MCP asset_relations 가 상대 자산의 이름·종류를 주지 않음 | 3/2/M | 대기 | 응답 행에 source_asset_id·target_asset_id 만 있어 모델이 상대마다 asset_get 을 부른다. assets 를 두 번 join 해 상대의 name·type 과 방향을 각 행에 넣으면 한 번의 호출로 답이 된다. | 2026-09-12 |
| MCP asset_search 가 0건일 때 실제 존재하는 type·status 값을 함께 돌려주지 않음 | 3/2/M | 대기 | type 은 열린 집합이라 고정 enum 은 정답이 아니다. 0건이고 필터가 걸렸을 때 실제 존재하는 값 목록을 함께 주어 '자산 없음'과 '필터 값 없음'을 구분하게 하는 것이 대안. 응답 모양이 바뀌므로 위험 2. | 2026-09-12 |
| 콘솔 Query DSL 화면이 새 total·offset 을 쓰지 않아 여전히 첫 페이지만 보여줌 | 3/2/M | 대기 | 서버는 total·has_more·next_offset 을 돌려주지만 operationsPages.tsx 는 items 와 truncated 만 읽는다. web 변경이므로 webui/dist 재빌드가 필요하다(이번 회차에서 dist 재빌드 절차가 그대로 통했다). | 2026-09-12 |
| attributes.* 의 배열·객체 값이 두 저장 모드에서 다른 텍스트로 렌더링됨 | 3/2/S | 대기 | SQLite json_extract 는 공백 없는 JSON, PostgreSQL #>> 는 {"k": "v"} 형태. 컨테이너를 가리키는 절을 아예 거절하는 편이 나을 수도 있다. | 2026-09-12 |
| 설정 → 방문 추적 화면 캡처를 캡처 스크립트에 추가해 가이드 20장에 싣기 | 2/1/S | 대기 | 새 아이디어(2026-09-12). TestGuideScreenshotsMatchCaptureScript 가 캡처 스크립트·docs/assets/guide·가이드 그림 세 곳을 대조하므로 셋을 함께 고쳐야 한다. 캡처 스크립트가 추적을 켠 상태(가짜 수집기)로 촬영하면 차단 목록도 찍힌다. | 2026-09-12 |
| MCP 도구 표 대조 테스트가 입력만 보고 Scope 열은 확인하지 않음 | 2/1/S | 대기 | TestMCPToolTableMatchesDeclaredSchemas 는 표의 세 번째 열만 대조한다. scope 가 틀리면 키를 잘못 발급받은 사용자가 tools/list 에서 도구를 못 본다. MCP 를 다시 손대는 회차에 묶는 편이 낫다. | 2026-09-12 |
| 두 가이드가 적은 API 경로·메서드를 라우터 등록과 대조하는 테스트가 없음 | 2/1/S | 대기 | USER_GUIDE.md·ADMIN_GUIDE.md 의 `GET /api/v1/...` 꼴 문구를 뽑아 openapi.yaml 의 paths 에 있는지 보는 테스트면 충분하다. 이번 회차에 ADMIN_GUIDE 20.5 절이 API 표를 더해 대조 대상이 늘었다. | 2026-09-12 |
| 추적 정책을 페이지 요청마다 DB 에서 읽음 — 짧은 TTL 캐시 검토 | 2/2/S | 대기 | 새 아이디어(2026-09-12). securityHeaders 가 화면 경로마다 server_metadata 한 행을 읽는다. 페이지 로드는 드물어 지금은 문제 없지만, 트래픽이 큰 설치에서는 2~5초 TTL 캐시가 필요할 수 있다. 다중 Pod 에서 반영 지연이 생기므로 위험 2. | 2026-09-12 |
| 콘솔이 마지막 scope 체크박스를 비활성화하지 않음 | 2/2/S | 대기 | 서버가 scope 없는 키를 거절하므로 이제 400 을 받고서야 알게 된다. web 변경 시 webui/dist 재빌드 필요. | 2026-09-12 |
| Query DSL 절 값의 빈 문자열 처리가 세 갈래로 갈림 | 2/2/S | 대기 | Parse 는 match[3]/match[4]/match[5] 를 순서대로 보며 빈 값을 다음 후보로 넘긴다. 지금은 결과가 같아 무해하지만 인용 그룹을 구분해 두는 편이 안전하다. | 2026-09-12 |
| API key 로 한 행위도 감사 기록의 actor_type 이 'user' | 2/3/S | 대기 | recordAdminAudit 이 actor_type 을 'user' 로 고정해 키가 한 일이 소유자가 콘솔에서 한 일과 구분되지 않는다. 콘솔 감사 필터의 facet 값이 바뀌므로 위험이 조금 있다. | 2026-09-12 |
| 잘못된 API key 는 rate limit 을 전혀 소비하지 않음 | 2/3/M | 대기 | apiRateLimit.Allow 가 Authenticate 성공 뒤에야 호출된다. 비밀이 256비트 난수라 현실적 위협은 낮고, 막으려면 소스 주소 기반 limiter 가 필요해 프록시 뒤 배포에서 정상 트래픽을 막을 수 있다. | 2026-09-12 |
| 캠페인 tracking-2026-09 — 관리자가 화면에서 방문 추적 스크립트를 붙이는 체계(nonce CSP · Momento 같은 오리진 프록시 · 차단 출처 기록) | 4/2/M | 완료 | server/internal/tracking 패키지, /api/v1/admin/settings/tracking(+violations, allowed-hosts), 무인증 csp-report, /momento/* 프록시, 콘솔 설정 → 방문 추적 탭, ADMIN_GUIDE 20장·PDF. 기본 꺼짐이고 꺼져 있으면 정책 문자열이 이전과 같음. include_admin 은 SPA 셸 하나라 구분 불가해 두지 않음. headless Chrome 으로 nonce 실행·차단 신고까지 확인. | 2026-09-12 |
원장 (에이전트가 남긴 기록)
2026-09-02
- 선택: Query DSL의 시간 값을 DB에 넘기기 전에 시각으로 해석 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
querydsl은"now - 24h"상대 시간만time.Time으로 바꾸고 나머지 시간 값은 문자열 그대로 SQL 파라미터로 넘겼다. PostgreSQL에서는last_seen_at < "2026-13-45"가 statement를 실패시켜 HTTP 500(실제로 재현 확인)이 되고, SQLite fallback에서는"2026-01-31T09:00:00Z"가 컬럼에 저장된"2026-01-31 09:00:00 +0000 UTC"와 바이트 비교되어 같은 날짜의 모든 행이 경계 반대편으로 넘어갔다. 이제now,now - <duration>, 드라이버가 만들어내는 절대 시각 레이아웃만 받아time.Time으로 컴파일하고, 해석되지 않는 값은 값 자체를 인용한 400INVALID_QUERY로 거절한다./query/validate도 파싱만 하지 않고 컴파일까지 하도록 맞춰 실행 결과와 판정이 갈리지 않게 했다. 검증: 신규 querydsl 단위 테스트와/api/v1/query/execute·/validate통합 테스트(엔드포인트에 기존 테스트가 전무했음)를 추가하고 수정 전에 실패·수정 후 통과를 SQLite와 실제 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 확인, 전체go test ./...(두 모드),go vet,go build,gofmt통과. - 보류 아이디어:
- Query DSL에 OR/괄호 지원 추가 (가치 3 / 위험 4 / L — 문법·평가기 변경 범위가 큼)
attributes.*경로의 숫자 비교가 텍스트 비교로 처리되는 문제 (가치 2 / 위험 3 / M)- 콘솔 쿼리 패널의 문법 참조 렌더링 테스트 보강 (가치 2 / 위험 1 / S)
/api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)
2026-09-02
- 선택: 요청 한도 카운터가 도착한 모든 주소를 영구히 기억하던 문제 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
agentRateLimiter.entries맵은 항목을 한 번도 지우지 않았다. 자격 증명이 필요 없는/v1/agent/enroll·/v1/agent/preflight의 한도는 출처 주소를 키로 쓰므로, 오래 떠 있는 Server는 지금까지 접속한 모든 주소의 레코드를 그대로 들고 있었고 IPv6 /64를 가진 호스트 하나가 운영자가 눈치채기 전에 메모리를 소진시킬 수 있었다. 창(window)이 지난 카운터는 아무것도 결정하지 않으므로Allow에서 만료 항목을 쓸어내되, 서로 다른 키가 몰릴 때 매 요청마다 맵 전체를 훑지 않도록 창당 최대 1회만 수행한다. 테스트가 실제 창을 sleep으로 기다리지 않도록 시계를 필드로 주입했다. 검증: 신규rate_limiter_test.go3개(만료 항목 제거, 창당 1회 sweep, 한도 자체 규칙 — 한도 규칙에 대한 테스트도 기존에 없었음)를 추가해 수정 전 실패·수정 후 통과를 확인하고,go test ./...,go test -race,go vet,go build,gofmt통과. - 보류 아이디어:
/api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)- API key 생성 시 잘못된 scope가 400 INVALID_SCOPES 대신 403 SCOPE_ESCALATION으로 반환됨 (가치 2 / 위험 1 / S)
- 마지막 scope를 제거하면 scope가 하나도 없는 키가 남음(생성은 1개 이상을 요구) (가치 2 / 위험 2 / S)
attributes.*경로의 숫자 비교가 텍스트 비교로 처리되는 문제 (가치 2 / 위험 3 / M)
- 릴리즈: v0.2.19 (2026-09-02)
2026-09-03
- 선택: 속성 조회가 찾으려는 키를 소문자로 바꿔버리던 문제 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
querydsl.Parse가 컬럼을 대소문자 구분 없이 받으려고 필드명 전체를strings.ToLower로 접었다. 그런데attributes.뒤는 컬럼이 아니라 저장된 JSON 문서의 키이고 JSON 키는 대소문자를 구분한다. 자산 API·MCP 도구·타 시스템 임포트로 만든 자산은 호출자가 쓴 키를 그대로 갖고 있어 대문자가 흔한데,attributes.assetTag는'{assettag}'(PostgreSQL#>>)와'$.assettag'(SQLitejson_extract)로 컴파일되어 어느 문서에도 없는 경로를 물었다. 결과는 오류 없는 HTTP 200 빈 목록이었고/query/validate는 그 표현식을 이미 valid라고 답한 뒤였다. 이제attributes.접두사만 접고 키는 입력된 대소문자를 유지하며(컬럼명은 그대로 대소문자 무시), 콘솔이 렌더링하는 문법 참조에도 대소문자 구분을 명시했다. 검증: querydsl 단위 테스트 2개와/api/v1/query/execute·/validate통합 테스트 2개를 추가해 수정 전 실패·수정 후 통과를 확인하고,go test ./...를 SQLite fallback과 실제 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 통과,go vet,go build,gofmt통과. - 보류 아이디어:
attributes.*경로의 숫자 비교가 텍스트 비교로 처리되는 문제 (가치 3 / 위험 3 / M)- 잘못된 scope로 키를 만들면 400 INVALID_SCOPES가 아니라 403 SCOPE_ESCALATION이 반환됨 (가치 2 / 위험 1 / S)
- 마지막 scope를 제거하면 scope가 하나도 없는 키가 남음(생성은 1개 이상을 요구) (가치 2 / 위험 2 / S)
/api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)
- 릴리즈: v0.2.20 (2026-09-03)
2026-09-03
- 선택: 속성 경로의 크기 비교가 숫자를 문자열로 비교하던 문제 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
attributes.*경로는 저장된 JSON 문서에서 텍스트로 추출되므로<,<=,>,>=가 문자 단위 비교였다.'1' < '2'라서"16000000000"이"2000000000"보다 앞서고, 결과적으로 16 GB 호스트가attributes.memory_bytes >= 2000000000에서 빠졌으며attributes.cpu_count > 9는 10인 장비를 제외했다. 오류 없이 HTTP 200과 짧은 목록만 돌아왔고/query/validate는 그 표현식을 이미 valid라고 답한 뒤였다. 이제 값이 숫자로 읽히는 크기 비교는 CASE로 컴파일되어 JSON 숫자로 저장된 값은 숫자로(PostgreSQL은::double precision, SQLite fallback은json_extract가 이미 돌려주는 수치 타입) 비교하고 그 외에는 지금처럼 텍스트로 비교한다. 덕분에attributes.os_version > "20.04"처럼 현재 텍스트로 정렬되는 절이 행을 잃지 않는다. 등호·부등호(=,!=)는 그대로 두었다. 텍스트 비교가 이미 옳고, 거기서"1.10"을 숫자로 읽으면 버전을 담은 속성이 자기 자신과 일치하지 않게 된다. 검증: querydsl 단위 테스트 2개(컴파일 결과·비대상 연산자)와/api/v1/query/execute통합 테스트 2개(숫자 비교 교정·텍스트 정렬 무회귀)를 추가해 수정 전 실패·수정 후 통과를 확인하고,go test ./...를 SQLite fallback과 실제 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 16개 패키지 모두 통과,go vet,go build,gofmt통과. - 보류 아이디어:
- 잘못된 scope로 키를 만들면 400 INVALID_SCOPES가 아니라 403 SCOPE_ESCALATION이 반환됨(생성·추가 경로가 서로 다른 코드를 돌려줌) (가치 2 / 위험 1 / S)
- 마지막 scope를 제거하거나
PATCH {"scopes":[]}를 보내면 scope가 하나도 없는 키가 남음(생성은 1개 이상을 요구) (가치 2 / 위험 2 / S) /api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)- Query DSL에 OR/괄호 지원 추가 (가치 3 / 위험 4 / L)
- 릴리즈: v0.2.21 (2026-09-03)
2026-09-03
- 선택:
id절이 입력된 값을 그대로 DB에 넘기던 문제 (가치 4 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
assets.id는 PostgreSQL에서UUID컬럼인데querydsl은id절의 값을 타이핑된 그대로 파라미터로 넘겼다.id = "web-01"은 statement 자체를 실패시켜 HTTP 500(실제로 재현 확인)이 되고 값이 무엇이었는지는 응답 어디에도 남지 않았으며, SQLite fallback에서는 텍스트 비교라 오류 없이 200 + 빈 목록이라 “그런 자산이 없다”처럼 보였다. 두 모드는 표기만 다른 UUID에서도 갈렸다. PostgreSQL은 대문자·하이픈 없는 표기를 같은 값으로 읽고 fallback의 텍스트 비교는 그렇지 않아서, UUID를 대문자로 출력하는 보고서에서 복사한 식별자가 운영에서는 자산을 찾고 fallback에서는 아무것도 찾지 못했다. 이제id절은 컴파일 시점에uuid.Parse로 해석해 UUID가 아니면 값을 인용한 400INVALID_QUERY로 거절하고, UUID면 저장된 정규 표기로 접어서 두 모드가 같은 답을 준다. 콘솔이 렌더링하는 문법 참조의id항목도 거절 대상이던 생략형 예시("0d0f…") 대신 완전한 UUID 예시와uuid종류로 바꿨다. 검증: querydsl 단위 테스트 2개와/api/v1/query/execute·/validate통합 테스트 2개를 추가해 수정 전 실패(PostgreSQL 500, SQLite 빈 목록)·수정 후 통과를 확인하고,go test ./...를 SQLite fallback과 실제 PostgreSQL 양쪽에서 전 패키지 통과,go vet,go build,gofmt통과. - 보류 아이디어:
/api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)- 잘못된 scope로 키를 만들면 400 INVALID_SCOPES가 아니라 403 SCOPE_ESCALATION이 반환됨(생성·추가 경로만 검증 순서가 PATCH와 다름) (가치 2 / 위험 1 / S)
attributes.*에 대한!=가 해당 키가 아예 없는 자산을 제외함(SQL NULL 의미) (가치 2 / 위험 3 / M)/api/v1/admin/api-keysHTTP 계층에 테스트가 전무함(서비스 계층에만 존재) (가치 3 / 위험 1 / M)
- 릴리즈: v0.2.22 (2026-09-03)
2026-09-03
- 선택: 입력된 검색어를 DB에 패턴으로 넘기던 문제 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 운영자가 타이핑하는 것은 호스트명·패키지명·이벤트 코드지 패턴이 아닌데,
모든 검색이 그 문자열을 그대로
%로 감싸 LIKE에 넘겼다._는 임의의 한 글자와 맞으므로 자산 목록에서db_prod를 찾으면db-prod도 나왔고, 감사 로그에서api_key.create를 찾으면 다른 action이 남긴 행도 함께 나왔다.%는 모든 행과 맞아서 아무것도 없어야 할 검색이 전체 인벤토리·전체 감사 로그를 돌려주었고 응답 어디에도 그런 표시가 없었다. 역슬래시는 두 엔진이 서로 다르게 읽어 (PostgreSQL은 LIKE 기본 escape, SQLite fallback은 기본 escape 자체가 없음)C:\Program검색이 한 모드에서는 호스트를 찾고 다른 모드에서는 아무것도 찾지 못했다 — PostgreSQL에서 실제로 빈 목록을 재현 확인. 소프트웨어 인벤토리 검색만 이미 escape와ESCAPE '!'를 쓰고 있었으므로 그 규칙을storage.LikePattern/LikeContains/LikeEscapeClause로 올리고, 빠져 있던 여섯 곳(자산 목록 검색과 owner 필터 — 콘솔·REST·API key 경로·CSV export가 공유, MCPasset_search, 감사 actor·전문 검색, 서버 로그 검색)에 적용했다. 검증: storage 단위 테스트 2개와 통합 테스트 6개(_·%·역슬래시·owner 필터·MCP·감사· 서버 로그)를 추가해 escape를 무력화하면 전부 실패하고 수정 후 통과함을 확인,go test ./...를 SQLite fallback과 실제 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet,go build,gofmt통과. - 보류 아이디어:
PATCH {"scopes":[]}나 마지막 scope 삭제로 scope가 하나도 없는 키가 남음(생성은 1개 이상을 요구) (가치 2 / 위험 2 / S)- 잘못된 scope 이름이 400 INVALID_SCOPES가 아니라 403 SCOPE_ESCALATION으로 돌아옴(super admin과 일반 admin의 응답이 갈림) (가치 2 / 위험 1 / S)
/api/v1/admin/api-keysHTTP 계층에 테스트가 전무함(서비스 계층에만 존재) (가치 3 / 위험 1 / M)attributes.*에 대한!=가 해당 키가 아예 없는 자산을 제외함(SQL NULL 의미) (가치 2 / 위험 3 / M)
- 릴리즈: v0.2.23 (2026-09-03)
2026-09-04
- 선택: scope가 하나도 없는 API 키가 남을 수 있던 문제 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 키 생성은 처음부터 scope를 1개 이상 요구했다. scope가 없는 키는 폐기된
키가 아니라 인증에 성공해
last_used_at을 남기고 한도 검사를 통과한 뒤 모든 엔드포인트의 scope 검사에서 거절되는 키이기 때문이다. 그런데 기존 키를 바꾸는 두 경로가 그 규칙을 건너뛰었다.PATCH {"scopes":[]}는 목록을 빈 배열로 바꾸고 200을, 마지막 남은 scope에 대한DELETE .../scopes/{scope}도 마찬가지로 200을 돌려주었다(실제로"scopes":[]재현 확인). 콘솔은 scope마다 체크박스 하나를 그리고 그 DELETE로 토글하므로, 마지막 체크를 해제하는 클릭 한 번이면 생성 폼이 거부하는 상태에 도달했고 키 목록에는 그대로 활성으로 표시되었다. 이제 목록 전체를 다루는 호출자가RequireScopes하나를 공유해 빈 결과를 거절하며(Create·Update·낙관적 scope 변경 모두),AddScopes·RemoveScope는 결과가 아니라 조각을 넘기므로ValidScopes는 그대로 빈 목록을 허용한다. 세 경로 모두 400INVALID_SCOPES로 답해 클라이언트가 한 규칙에 대한 세 가지 응답을 구분할 필요가 없다. 검증: apikeys 서비스 테스트 1개와/api/v1/admin/ api-keysHTTP 통합 테스트 1개(이 엔드포인트에 HTTP 계층 테스트가 전무했음)를 추가해 수정 전 실패·수정 후 통과를 확인하고,go test ./...를 SQLite fallback과 실제 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet,go build,gofmt,redocly lint openapi.yaml통과. - 보류 아이디어:
/api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)attributes.*에 대한!=가 해당 키가 아예 없는 자산을 제외함(SQL NULL 의미) (가치 2 / 위험 3 / M)- 콘솔이 마지막 scope 체크박스를 비활성화하지 않아 이제 400을 받고서야 알게 됨(web 변경 시
webui/dist재빌드 필요) (가치 2 / 위험 2 / S) - Query DSL에 OR/괄호 지원 추가 (가치 3 / 위험 4 / L)
- 기각: “잘못된 scope 이름이 403 SCOPE_ESCALATION으로 돌아옴”은 실제로 도달
불가능하다.
api_keys.manage는 마이그레이션에서 super_admin 역할에만 부여되고 역할 생성 API가 없으므로(/api/v1/admin/roles는 조회 전용), 이 핸들러에 도달하는 호출자는 항상 super admin이고HasPermission이 무조건 true를 돌려준다. 즉 오타 난 scope는 이미 400을 받는다. - 릴리즈: v0.2.24 (2026-09-04)
2026-09-04
- 선택:
attributes.*에 대한!=가 그 키가 없는 자산을 전부 제외하던 문제 (가치 4 / 위험 2 / 작업량 M) - 결과: 성공
- 요약:
attributes.*경로는 저장된 JSON 문서에서 값을 꺼내므로 그 키를 한 번도 보고하지 않은 자산에서는 SQLNULL이 나오고,NULL != 'prod'는 참이 아니라 미지(unknown)라 행이 탈락했다. 그래서attributes.env != "prod"는 env를 수집한 적 없는 자산 — “운영이 아닌 것”을 묻는 운영자가 가장 보고 싶어 하는 미표시 자산 — 을 하나도 돌려주지 않았고, 응답은 HTTP 200과 짧아진 목록뿐이라 무엇이 빠졌는지 알 길이 없었으며/query/validate는 그 표현식을 이미 valid라고 답한 뒤였다. PostgreSQL의#>>와 SQLite의json_extract가 똑같이 NULL을 주므로 두 모드를 비교해도 힌트가 없었다. 이제 속성 경로의 부등호는(<경로> IS NULL OR <경로> != $n)으로 컴파일된다. 등호는 그대로 두어 값을 묻는 절이 보고한 적 없는 자산을 끌어오지 않게 했고, 질의 가능한 컬럼은 전부NOT NULL이라 컬럼 절은 단순 비교를 유지한다. 콘솔이 렌더링하는 문법 참조에도!=가 해당 속성이 없는 자산을 포함한다고 적었다(문구는 서버Describe()가 내려주므로webui/dist재빌드 불필요). 검증: querydsl 단위 테스트 2개(두 모드의 컴파일 결과·컬럼 절 무회귀)와/api/v1/query/execute통합 테스트 1개(prod· staging·env 없음 세 자산으로!=와=양쪽 확인)를 추가해 수정 전 실패 (!=가 staging 하나만 반환)·수정 후 통과를 확인하고,go test ./...를 SQLite fallback과 실제 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet,go build,gofmt,npm test(130개),npm run build,redocly lint openapi.yaml통과. - 보류 아이디어:
/api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)- 콘솔이 마지막 scope 체크박스를 비활성화하지 않아 이제 400을 받고서야 알게 됨(web 변경 시
webui/dist재빌드 필요) (가치 2 / 위험 2 / S) - Query DSL에 OR/괄호 지원 추가 (가치 3 / 위험 4 / L)
- Query DSL이
attributes.<키>의 존재/부재 자체를 묻는 연산자를 제공하지 않음(지금은>= ""같은 우회가 필요) (가치 3 / 위험 2 / M)
- 릴리즈: v0.2.25 (2026-09-04)
- 릴리즈: v0.2.25 (2026-09-04)
2026-09-05
- 선택: 중간에 실패한 행 읽기가 완전한 결과로 응답되던 문제 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약:
*sql.Rows반복문은 “끝까지 읽었다”와 “읽다가 실패했다”를 구분하지 못한다 —Next()가 둘 다false를 돌려주고rows.Err()만이 둘을 가른다. 그런데 아홉 곳이 그것을 호출하지 않아, 연결이 끊기거나 statement가 취소되면 그때까지 도착한 행이 그대로 답이 되어 HTTP 200으로 나갔고 조각이라는 표시는 응답 어디에도 없었다. 가장 무거운 자리는/api/v1/query/execute로, 3,000대 규모 질의가 3대만 돌려줄 수 있었고 바로 다음 줄의 감사 레코드가 그 짧아진 수를 “표현식에 일치한 자산 수”로 남겼다. Agent 목록, 설정 목록·변경 이력, 자산의 수집 원천·변경 이력·관계도 같은 모양이었고,mergeAssets는 scan 오류까지 버려 실패한 scan이 빈 문자열을 “옮긴 source id” 목록에 넣었으며 그 목록은asset_changes에 영구 기록으로 들어간다. 이제 아홉 곳 모두 요청을 실패시키고(진단 pruner만 읽은 행을 유지하되 그 회차를 버림), 나머지 반복문은 이미 확인하고 있었다는 차이가 다시 생기지 않도록 Server 트리의 모든 행 반복문을 소스로 읽어 커서를 연 함수가 오류를 확인하는지 요구하는 테스트를 추가했다(CSV 컬럼의spreadsheetSafe검사와 같은 방식 — 이 저장소에 이미 AST 규약 테스트가 4개 있다). 검증: 새 테스트가 수정 전 아홉 곳을 파일·함수· 변수 이름과 함께 나열하며 실패하고 수정 후 통과함을 확인,go test ./...를 SQLite fallback과 실제 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt,npm test(130개)·npm run build(webui/dist무변경 확인),redocly lint openapi.yaml,cargo build --locked통과. - 보류 아이디어:
exportAssetsCSV가 limit(기본 10,000)에서 조용히 잘림. 목록 API는total·has_more를 주는데 CSV에는 아무 표시가 없음 (가치 3 / 위험 2 / M)- Query DSL이
attributes.<키>의 존재/부재 자체를 묻는 연산자를 제공하지 않음(지금은>= ""같은 우회가 필요) (가치 3 / 위험 2 / M) /api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)- 콘솔이 마지막 scope 체크박스를 비활성화하지 않아 이제 400을 받고서야 알게 됨(web 변경 시
webui/dist재빌드 필요) (가치 2 / 위험 2 / S)
- 릴리즈: v0.2.26 (2026-09-05)
2026-09-05
- 선택: SQLite 모드에서
attributes.*절이 문자열로 저장된 값만 찾던 문제 (가치 4 / 위험 2 / 작업량 M) - 결과: 성공
- 요약:
attributes.*경로는 Agent가 보고한 JSON 문서에서 값을 꺼내는데, 호스트가 자기 자신에 대해 보고하는 값 대부분은 문자열이 아니라 숫자나 참거짓이다. PostgreSQL의#>>는 저장된 값을 텍스트로 렌더링해= 10이"10"을,= true가"true"를 만나지만, SQLite의json_extract는 SQL 값(INTEGER·REAL, 참거짓은 1과 0)을 그대로 돌려주고 SQLite는 숫자를 그 자릿수 텍스트와 같다고 보지 않고 모든 텍스트보다 작다고 본다. 절의 값은 항상 텍스트로 바인딩되므로 SQLite 모드에서는attributes.cpu_count = 10이 무엇을 보고했든 모든 자산에 대해 거짓이었고,!=쪽은 정확히 10코어인 호스트까지 포함해 전부 참이었다. 문자열로 저장된 값만 매칭됐고 둘 다 HTTP 200에 그럴듯한 목록이라 표시가 없었으며/query/validate는 이미 valid라고 답한 뒤였다 — PostgreSQL은 정상 동작하므로 두 모드를 비교해야만 보였다(실제로 같은 데이터로 두 모드를 돌려 확인). 이제 fallback은 속성 경로를 PostgreSQL과 같게 렌더링하고(참거짓은'true'/'false', 나머지는CAST(... AS TEXT)), 정렬 비교의 숫자 쪽만 추출값 그대로 두어 기존 숫자 정렬 수정을 되돌리지 않게 했다. 콘솔이 렌더링하는 문법 참조에 숫자·참거짓 표기법을 적었다(서버Describe()가 내려주므로webui/dist재빌드 불필요). 검증: querydsl 단위 테스트 2개(두 모드 컴파일 결과 고정)와/api/v1/query/execute통합 테스트 1개(숫자·참거짓·문자열·키 없음 자산으로=/!=/정렬 7가지)를 추가, 기존 컴파일 기대문자열 2건 갱신. 수정 전 동일 질의가 SQLite에서 전부 0건이던 것을 확인한 뒤 수정 후 통과.go test ./...를 SQLite fallback과 실제 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt,npm test(130개)·npm run build(webui/dist무변경 확인),redocly lint openapi.yaml,cargo build --locked통과. 버전 범프와 릴리즈 노트는 하지 않았다: 병렬 브랜치auto/2026-09-05-1110이 이미 v0.2.26을 만들어 두어 같은 번호를 쓰면 병합 시 버전 파일 전체가 충돌한다. - 보류 아이디어:
exportAssetsCSV가 limit(기본 10,000)에서 조용히 잘림. 목록 API는total·has_more를 주는데 CSV에는 아무 표시가 없음 (가치 3 / 위험 2 / M)- Query DSL이
attributes.<키>의 존재/부재 자체를 묻는 연산자를 제공하지 않음(지금은>= ""같은 우회가 필요) (가치 3 / 위험 2 / M) /api/v1/external/query/*API key 경로의 한도·감사 로그 커버리지 (가치 3 / 위험 2 / M)attributes.*경로가 배열·객체를 두 모드에서 다른 텍스트로 렌더링함(SQLite는 공백 없는 JSON, PostgreSQL은{"k": "v"}) (가치 2 / 위험 2 / S)- 콘솔이 마지막 scope 체크박스를 비활성화하지 않아 이제 400을 받고서야 알게 됨(web 변경 시
webui/dist재빌드 필요) (가치 2 / 위험 2 / S)
2026-09-06
- 선택: CSV 내보내기가 행 상한에서 조용히 잘려 부분 추출이 완전한 추출과 구분되지 않던 문제 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 두 CSV 내보내기는 행 상한에서 멈춘다 — 자산 10,000행, 감사 5,000행 —
그런데 상한에 닿은 실행은 완전한 추출과 구별할 수 없었다. 같은
invenqor-assets.csv, 같은 헤더 행, HTTP 200. 목록 API 는total·has_more로 잘림을 말하지만 CSV 에는 그런 자리가 없고, 콘솔은 URL 로 이동해 내려받으므로 응답 헤더를 볼 수 있는 것조차 없다. 검토에 첨부된 인벤토리 추출과 감사인에게 건넨 증적 추출이 상한 너머의 행만큼 짧은 채로 전체인 양 읽혔다. 이제 두 내보내기 모두 조건 일치 총수를 세어(자산은listAssets가 이미 쓰던 count 를assetTotal로 뽑아 공유, 감사는 기존auditTotal재사용), 부분 추출이면 파일 이름 자체가invenqor-assets-10000-of-12345.csv가 되고 — 저장·전달 뒤에도 파일을 따라가는 유일한 통로다 —X-Invenqor-Truncated,X-Invenqor-Row-Count,X-Invenqor-Total-Count헤더가 API 로 읽는 프로그램에 같은 숫자를 준다. 감사 추출을 남기는audit.export기록에도matched와truncated를 넣었다: 누가 무슨 증적을 가져갔는지의 기록이야말로 완전한 추출로 읽히면 안 되는 자리다. 검증: 두 내보내기의 부분·완전 추출을 각각 확인하는 테스트 2개 추가(파일 이름·헤더·행 수와 감사 기록의truncated). PostgreSQL 은 JSONB 를 콜론 뒤 공백과 함께 렌더링해 첫 시도가 실 PostgreSQL 에서만 실패했고, 감사 기록 검사를 텍스트 매칭에서 JSON 디코딩으로 바꿔 두 모드에서 같게 만들었다.go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt,npm test(130개)·npm run build(webui/dist무변경 확인),redocly lint openapi.yaml(경고 6개, 모두 이전과 동일한 무관 경로) 통과. Rust 쪽 파일은 건드리지 않아cargo는 돌리지 않았다. 버전 범프·릴리즈 노트는 하지 않았다: 병렬 브랜치가 이미 v0.2.26 을 만들어 두어 같은 번호를 쓰면 병합 시 버전 파일이 통째로 충돌한다. - 보류 아이디어:
- Query DSL 에
attributes.<키>존재/부재 연산자가 없어>= ""같은 우회가 필요함 (가치 3 / 위험 2 / M) /api/v1/external/query/*API key 경로의 rate limit·감사 기록에 테스트가 하나도 없음 (가치 3 / 위험 2 / M)attributes.*의 배열·객체 값이 두 저장 모드에서 다른 텍스트로 렌더링됨 (가치 2 / 위험 2 / S)- 콘솔이 마지막 scope 체크박스를 비활성화하지 않아 400 을 받고서야 알게 됨(web 변경 시
webui/dist재빌드 필요) (가치 2 / 위험 2 / S) /api/v1/query/execute가has_more없이 limit(기본 100·최대 500)에서 자름 — 콘솔은 추측으로 알리지만 API key 로 붙는 프로그램에는 표시가 없음 (가치 2 / 위험 1 / S)
- Query DSL 에
2026-09-06
- 선택:
/api/v1/query/execute가 행 상한에서 잘린 결과를 전체인 양 돌려주던 문제 (가치 2 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
/api/v1/query/execute는 행 상한(기본 100·최대 500)에서 멈추면서 그 사실을 말하지 않았다. 400건이 맞는 질의가 100건과 HTTP 200,"count": 100으로 돌아왔고, 정확히 100건이 맞는 질의와 구분할 방법이 응답에 없었다. 이 엔드포인트는 프로그램용이다 — API key 가 닿을 수 있는 유일한 Query DSL 입구라, “패치가 빠진 호스트”를 묻고 4분의 1만 받아 전부인 줄 알고 조치할 수 있었다. 감사 기록의result_count도 잘린 수를 그대로 남겨 무엇을 묻고 무엇을 받아갔는지의 기록까지 완전한 것처럼 읽혔다. 콘솔은result.length === limit으로 추측했는데 그 추측은 반대 방향으로도 틀린다: limit 100 에 정확히 100건인 완전한 답을 “잘렸을 수 있음”이라고 표시해 없는 행을 찾게 만들었다. 이제 상한보다 한 행 더 읽고 버려has_more를 확정하고, 적용된limit을 함께 응답·감사 기록에 넣는다(limit 을 보내지 않은 호출자가 기본값을 알아야 더 큰 값을 요청할 수 있던 것도 없앤다). 콘솔은 추측 대신 서버의has_more를 쓴다. 같은 루프에서rows.Err()를 확인하도록 했다 — 서버의 다른 목록 질의 30여 곳이 모두 그렇게 하고, 반복 중 오류로 짧아진 목록을has_more: false로 보고하는 것은 행 상한이 하던 거짓말과 같기 때문이다. 검증: 5개 자산에 대해 limit 3(잘림)·limit 5(정확히 채운 완전한 답)·limit 미지정(기본 100 보고)을 응답과 감사 metadata 양쪽에서 확인하는 통합 테스트 1개와 콘솔 라벨 단위 테스트 2개 추가. 감사 metadata 는 텍스트가 아니라 JSON 으로 디코딩해 비교했다(PostgreSQL 의 JSONB 렌더링 공백이 다르다).go test ./...를 SQLite fallback 과 실 PostgreSQL (scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt,npm test(132개)·npm run build(webui/dist재빌드 후 커밋),redocly lint openapi.yaml(경고 6개, 모두 이전과 동일한 무관 경로) 통과. Rust 쪽 파일은 건드리지 않아cargo는 돌리지 않았다. 문서.md는 손대지 않았다: 이 저장소에서docs/*.md는 릴리즈 커밋에서 PDF 와 함께만 갱신된다. 버전 범프·릴리즈 노트도 하지 않았다: 병렬 브랜치가 이미 v0.2.26 을 만들어 두어 같은 번호를 쓰면 병합 시 버전 파일이 통째로 충돌한다. - 보류 아이디어:
- Query DSL 에
attributes.<키>존재/부재 연산자가 없어>= ""같은 우회가 필요함 (가치 3 / 위험 2 / M) /api/v1/external/query/*API key 경로의 rate limit·감사 기록에 테스트가 하나도 없음 (가치 3 / 위험 2 / M)listAgents·설정 목록/이력·자산 상세의 sources/history/relations 루프가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 (가치 3 / 위험 1 / M)- MCP
asset_search·agents의has_more가len(items)==limit추측이라 마지막 페이지를 “더 있음”으로 알림 (가치 2 / 위험 1 / S) - API key 로 한 행위도 감사 기록의
actor_type이user라 소유자가 콘솔에서 한 일과 구분되지 않음 (가치 2 / 위험 3 / S)
- Query DSL 에
2026-09-07
- 선택: MCP
asset_search·agents_list의has_more가 마지막 페이지를 “더 있음”으로 알림 (가치 2 / 위험 1 / 작업량 S) - 결과: 성공
- 요약: 두 MCP 도구는
has_more를len(items) == limit로 계산했다. 정확히 limit 개에서 끝나는 페이지는 크기만으로는 잘린 페이지와 구분되지 않으므로, 완전한 답이 잘린 것으로 보고됐다 — 모델은 빈 페이지를 한 번 더 읽고 재고가 일관되지 않다고 말하거나 아무것도 돌려주지 않을 offset 을 계속 넘긴다.agents_list는 offset 인자 자체가 없어서 “더 있다”는 호출자가 해소할 수 없는 주장이었다. 이제 두 도구 모두 한 행을 더 읽고 버려has_more를 확정한다 (software_inventory가 이미 실제 COUNT 로 확정하던 것과 같은 성질의 답을 lookahead 로 얻는다). 같은 자리에서rows.Err()를 응답 전에 확인하도록 바꿔, 반복 중 오류로 짧아진 목록이has_more: false인 성공 응답으로 나가지 않게 했다. 검증: 자산 3건·에이전트 3건에 대해 limit 2(잘림)·limit 3(정확히 채운 마지막 페이지)·limit 10(부분 페이지)·offset 2 로 도달한 마지막 페이지를 확인하는 테스트 2개 추가(mcp_paging_test.go,next_offset과count일관성 포함).go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt통과. web·Rust·openapi.yaml 은 건드리지 않아(MCP 도구 출력은 openapi 에 기술돼 있지 않다)npm·cargo·redocly는 돌리지 않았다. 문서.md와 버전 범프·릴리즈 노트는 하지 않았다:docs/*.md는 릴리즈 커밋에서 PDF 와 함께만 갱신되고, 병렬 브랜치가 이미 다음 버전 번호를 만들어 두어 같은 번호를 쓰면 병합 시 버전 파일이 통째로 충돌한다. - 보류 아이디어:
/api/v1/external/query/*API key 경로의 rate limit(429)·감사 기록 principal 에 테스트가 하나도 없음 (가치 3 / 위험 1 / M) · Query DSL 에attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) ·listAgents·설정 목록/이력·자산 상세의 sources/history/relations 루프가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 (가치 3 / 위험 1 / M) · MCPasset_search의type·status가 검증 없이 통과해 오타가 “해당 자산 없음”으로 답해짐(software_inventory는mustBeOneOf로 거절) (가치 3 / 위험 1 / S) · API key 로 한 행위도 감사 기록의actor_type이user라 소유자가 콘솔에서 한 일과 구분되지 않음 (가치 2 / 위험 3 / S)
2026-09-08
- 선택:
/api/v1/external/*API key 경로에 테스트가 하나도 없음 (가치 3 / 위험 1 / 작업량 M) - 결과: 성공
- 요약: 이 브랜치의
*_test.go어디에서도/api/v1/external/문자열이 나오지 않았다 — rate limit 뿐 아니라 API key 자격증명 경로 전체(401 과WWW-Authenticate챌린지, 429 와Retry-After, scope 거절, 감사 기록에 남는 이름)가 읽히기만 하고 한 번도 실행되지 않았다. 프로그램이 실제로 쓰는 유일한 입구인데 그렇다.server/internal/httpapi/external_api_test.go에 테스트 3개를 추가했다: (1) 한도를 넘긴 키는Retry-After: 60과API_RATE_LIMITED로 429 를 받고, 같은 순간 다른 키는 200 을 받으며(limiter 가 KeyID 로 나뉘어 있다는 것 — 공유 키였다면 재시도 루프에 빠진 통합 하나가 배포 전체를 429 로 만든다), 창이 지나면 다시 통과한다.server.apiRateLimit을newAgentRateLimiter(2, time.Minute)로 바꾸고now를 고정 시계로 교체해 600회를 두드리지 않는다. (2) 키 없음·우리 키가 아닌 값·우리 접두사에 모르는 비밀·폐기된 키 모두 빈 목록이 아니라 401 로 거절되고 챌린지 헤더가 붙는다. (3)assets.read만 가진 키는 쓰기에서 403FORBIDDEN을 받고, 쓰기가 허용된 키가 만든 자산의 감사 기록actor_name은 소유자가 아니라api-key:importer다. 검증: 새 테스트가 헛돌지 않는지 확인하려고Retry-After설정 줄을 지우고Allow(KeyID)를Allow("shared")로 바꿔 실패하는 것을 본 뒤 되돌렸다.go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt통과. Go 테스트 파일 하나만 추가했으므로 web·Rust·openapi.yaml은 손대지 않았고npm·cargo·redocly는 돌리지 않았다. 문서.md와 버전 범프·릴리즈 노트도 하지 않았다. -
보류 아이디어: MCP
asset_search의type·status오타가 “해당 자산 없음”으로 답해짐 — 다만type은 열린 집합이라 고정 enum 은 정답이 아님이 이번에 확인됐다 (가치 3 / 위험 2 / M) · Query DSL 에attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) ·listAgents·설정 목록/이력·자산 상세의 sources/history/relations 루프가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 (가치 3 / 위험 1 / M) · 잘못된 API key 는 rate limit 을 전혀 소비하지 않아(429 검사가Authenticate성공 뒤에 있음) 키 추측 시도만 무제한 (가치 2 / 위험 3 / M) · API key 로 한 행위도 감사 기록의actor_type이user라 소유자가 콘솔에서 한 일과 구분되지 않음 (가치 2 / 위험 3 / S) - 릴리즈: v0.2.26 (2026-09-08, run 2026-09-08-131347-Invenqor-release)
2026-09-08
- 선택: CSV 내보내기가 행 한도에 걸려도 완전한 파일처럼 보임 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
exportAssets(기본 10,000행)와exportAudit(기본 5,000행) 둘 다 한도만큼 읽은 결과를 전부인 것처럼 써 내려갔다 — 자산 12,000건이면invenqor-assets.csv가 10,000행으로 내려오고, 파일에도 이름에도 응답에도 나머지가 빠졌다는 표시가 없다. 콘솔 밖에서 대조할 것 없이 읽히라고 만든 파일(인수인계·티켓·감사 증거)이라 거절보다 나쁘다. 이제 둘 다 한도보다 한 행 더 읽어(정확히 한도에서 끝나는 결과는 여전히 ‘완전’으로 보고) 잘린 경우에만X-Invenqor-Truncated·X-Invenqor-Row-Limit헤더를 붙이고 첨부 파일 이름을...-partial.csv로 바꾼다(헤더가 사라진 뒤에도 남는 유일한 신호 — 브라우저가 그 이름으로 저장하고 스프레드시트 제목 표시줄에 뜬다).audit.export감사 기록에도truncated를 남겨, 부분 추출이 “전체 로그를 받아갔다”는 증거로 남지 않게 했다. 검증:csv_truncation_test.go추가 — 두 내보내기 각각에 대해 limit=3(5건 중 잘림)에서 행 수·두 헤더·partial 파일명을, limit=5(정확히 한도에서 끝나는 완전한 결과)에서 헤더 없음·평범한 파일명을 확인하고,audit_logs.after_json의truncated가 [true false] 로 남는지 확인한다. 헛돌지 않는지 보려고truncated계산을false로 바꿔 실패하는 것을 확인한 뒤 되돌렸다.go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt통과.openapi.yaml의 두 CSV 엔드포인트에limit파라미터와 새 응답 헤더를 기술하고 CI 와 같은@redocly/cli@2.47.0 lint통과(경고 6개는 모두 기존 것, 내 줄과 무관). web·Rust 는 손대지 않아npm·cargo는 돌리지 않았다. 문서.md와 버전 범프·릴리즈 노트는 하지 않았다:docs/*.md는 릴리즈 커밋에서 PDF 와 함께만 갱신된다. -
보류 아이디어: MCP
asset_search가 0건일 때 실제 존재하는 type·status 값을 함께 돌려주어 ‘자산 없음’과 ‘필터 값 없음’을 구분하게 함 (가치 3 / 위험 2 / M) · Query DSL 에attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) ·listAgents·설정 목록/이력·자산 상세 루프·executeQuery가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 — 드라이버 fault injection 없이는 테스트 불가 (가치 3 / 위험 1 / M) · 잘못된 API key 는 rate limit 을 전혀 소비하지 않아 키 추측만 무제한 (가치 2 / 위험 3 / M) · API key 로 한 행위도 감사 기록의actor_type이user라 소유자가 콘솔에서 한 일과 구분되지 않음 (가치 2 / 위험 3 / S) - 릴리즈: v0.2.27 (2026-09-08, run 2026-09-08-175105-Invenqor-improve)
2026-09-08
- 선택: attributes.* 의 등식이 에이전트가 숫자로 보고한 값과 절대 일치하지 않음 (SQLite 모드) (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
attributes.*절은 문서에서 뽑은 값을 사용자가 입력한 텍스트와 비교하므로 추출식이 두 저장 모드에서 값을 같은 텍스트로 렌더링해야 한다. PostgreSQL 의#>>는 그렇게 하지만 SQLite 의json_extract는 저장된 SQL 타입 그대로 돌려주고 SQLite 비교는 아무 변환도 하지 않는다 — INTEGER 는 TEXT 파라미터와 결코 같지 않고, JSON boolean 은'true'가 아니라 1/0 으로 온다. 그래서 SQLite fallback 에서attributes.cpu_count = 8은 그 값을 보고한 호스트가 있어도 HTTP 200 에 빈 목록을 돌려주고, 거울상인attributes.cpu_count != 8은 8 을 가진 자산까지 전부 남겼다(같은 질의가 PostgreSQL 에서는 정상 동작). 에이전트 payload 가 그대로 저장되어 cpu 수·메모리 바이트·포트 번호가 JSON number 이므로 수집 값의 상당 부분이 해당된다. SQLite 추출식이 PostgreSQL 과 같은 텍스트로 접도록 고쳤고(json_type으로 boolean 만'true'/'false'로, 나머지는CAST(... AS TEXT)),CAST가 SQL NULL 을 그대로 두므로 키를 보고하지 않은 자산은 여전히 NULL 을 뽑아!=절의 NULL 처리가 유지된다. 정렬 비교(<,<=,>,>=)는 숫자를 숫자로 비교해야 하므로attributeExpressions가 텍스트형과 원본 추출식을 함께 돌려주고 숫자 분기는 원본을 쓴다. 검증: 먼저 실패하는 테스트를 썼다 —server/internal/httpapi/query_attribute_types_test.go(숫자 등식·!=·정렬 회귀, boolean= "true"/= "false", 텍스트·버전 문자열이 그대로인지) 는 수정 전 SQLite 에서 실제로 빈 목록으로 실패했고 수정 후 통과한다.querydsl단위 테스트에 두 모드의 등식 SQL 모양을 고정하는 테스트를 추가하고, SQLite 기대값을 쓰던 기존 두 테스트(정렬·!=)를 새 렌더링에 맞게 갱신했다.go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt통과. 문법 응답(Describe())과 HTTP 계약이 바뀌지 않아openapi.yaml·web·Rust 는 손대지 않았고npm·cargo·redocly는 돌리지 않았다. 문서.md와 버전 범프·릴리즈 노트는 하지 않았다. -
보류 아이디어:
attributes.*의 배열·객체 값이 두 저장 모드에서 다른 텍스트로 렌더링됨(SQLite 은 공백 없는 JSON, PG#>>는{"k": "v"}) — 이번 수정으로 스칼라는 일치시켰으므로 남은 차이는 이것뿐 (가치 3 / 위험 2 / S) · Query DSL 에attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) ·listAgents·설정 목록/이력·자산 상세 루프·executeQuery가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 — 드라이버 fault injection 없이는 테스트 불가 (가치 3 / 위험 1 / M) · MCPasset_search가 0건일 때 실제 존재하는 type·status 값을 함께 돌려주어 ‘자산 없음’과 ‘필터 값 없음’을 구분하게 함 (가치 3 / 위험 2 / M) · API key 로 한 행위도 감사 기록의actor_type이user라 소유자가 콘솔에서 한 일과 구분되지 않음 (가치 2 / 위험 3 / S) - 릴리즈: v0.2.28 (2026-09-08, run 2026-09-08-230110-Invenqor-improve)
2026-09-09
- 선택: Query DSL 실행 결과가 limit 에 걸려도 완전한 답처럼 보임 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
executeQuery는 limit(기본 100, 상한 500)만큼 읽은 결과를 전부인 것처럼{items, count, ast}로 돌려줬다 —last_seen_at < "now - 720h"가 1,200건에 해당해도 100건과count100 이 오고, 나머지 1,100건이 있다는 표시가 어디에도 없다. 이 엔드포인트에는 offset 이 없어 두 번째 요청으로 드러날 여지조차 없고, 같은 핸들러가 API key 용/api/v1/external/query/execute에도 걸려 있어 스크립트는 콘솔처럼 “행 수가 limit 과 같네” 하고 눈치챌 방법도 없다. 2026-09-08 CSV 내보내기 수정과 같은 부류다. 이제 limit 보다 한 행 더 읽어(정확히 한도에서 끝나는 결과는 여전히 ‘완전’으로 보고)truncated와 실제 적용된limit을 응답에 넣고,query.execute감사 기록에도truncated를 남겨 부분 답이 전체 인벤토리를 본 증거로 남지 않게 했다. 콘솔은len(result) === limit추측으로 “잘렸을 수 있음”이라 적던 자리를 서버가 알려주는 사실(“잘림” + 조치 안내)로 바꿨다. 검증:server/internal/httpapi/query_truncation_test.go추가 — limit=3 (5건 중 잘림)에서 3행·truncatedtrue·limit3, limit=5(정확히 한도에서 끝나는 완전한 결과)와 limit 미지정(기본 100)에서truncatedfalse 를 확인하고,audit_logs.after_json에 3행 실행은 truncated=true, 5행 실행은 false 로 남는지 확인한다(타임스탬프가 같을 수 있어 순서 대신result_count로 구분). 헛돌지 않는지 보려고truncated계산과 응답 필드를 각각false로 바꿔 실패하는 것을 확인한 뒤 되돌렸다.go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt통과.npm test(130건)·npm run build통과하고server/internal/webui/dist를 재빌드해 커밋했다(빌드 전 dist 가 체크인된 것과 일치함을 먼저 확인해 CI 의git diff --exit-code대조가 안전함을 검증).openapi.yaml의 두 query/execute 엔드포인트에limit·truncated응답 필드를 기술하고 CI 와 같은@redocly/cli@2.47.0 lint통과(경고 6개는 모두 기존 것). Rust 는 손대지 않아cargo는 돌리지 않았다. 문서.md와 버전 범프·릴리즈 노트는 하지 않았다. -
보류 아이디어:
attributes.*의 배열·객체 값이 두 저장 모드에서 다른 텍스트로 렌더링됨(SQLite 은 공백 없는 JSON, PG#>>는{"k": "v"}) (가치 3 / 위험 2 / S) · Query DSL 에attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) · Query DSL 실행에 offset 이 없어 상한 500 을 넘는 나머지를 받아낼 방법이 아예 없음 —/api/v1/assets처럼 offset·total 을 주는 것이 대안 (가치 3 / 위험 2 / M) ·listAgents·설정 목록/이력·자산 상세 루프·executeQuery가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 — 드라이버 fault injection 없이는 테스트 불가 (가치 3 / 위험 1 / M) · MCP 의has_more가len(items) == limit추측이라 마지막 페이지에서 거짓말하고, offset 없는 agents 도구는 가져올 수 없는 페이지를 약속함 (가치 2 / 위험 1 / S) - 릴리즈: v0.2.29 (2026-09-09, run 2026-09-09-044107-Invenqor-improve)
2026-09-09
- 선택: MCP 의 has_more 가 페이지 크기 추측이라 마지막 페이지에서 거짓말함 (가치 2 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
asset_search와agents_list는has_more를len(items) == limit으로 계산해, 결과가 정확히 한도에서 끝나면(기본 50 인 상태에서 에이전트가 정확히 50대, 또는type필터가 요청한 만큼만 맞는 경우) 다음 페이지가 있다고 보고했고, 그 말을 믿은 클라이언트는 빈 페이지를 한 번 더 가져왔다.agents_list는 offset 자체가 없어has_more:true가 애초에 가져올 수 없는 페이지를 약속했다 — 더 있다는 말을 들은 호출자가 할 수 있는 일은 같은 요청을 반복하는 것뿐이었다. 둘 다 한 행을 더 읽고 버리도록 바꿔has_more가 실제로 다음 행이 존재하는지를 말하게 했고,agents_list에offset입력과offset·next_offset응답을 추가해 나머지에 도달할 수 있게 했다(/api/v1/assets와 query/execute 가 이미 쓰는 lookahead 와 같은 방식). 검증:server/internal/httpapi/mcp_pagination_test.go추가 — 3행을 limit 3 으로 읽으면has_morefalse, limit 2 면 2행·has_moretrue·next_offset2, 그next_offset으로 다시 물으면 남은 1행이 오고has_morefalse 인지, lookahead 행이 페이지에 섞여 나오지 않는지, 그리고agents_list스키마가offset을 실제로 광고하는지를 확인한다. 헛돌지 않는지 보려고 lookahead 와 비교식을 옛 방식으로 되돌려 두 테스트가 정확히 “경계에서 끝난 결과에 has_more true” 로 실패하는 것을 확인한 뒤 복구했다.go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt통과.docs/API_MCP_GUIDE.md의 도구 표에agents_list의offset을 넣고has_more가 추측이 아니라는 점과next_offset사용법을 한 문단 적었다. web·Rust·openapi.yaml(MCP 는 도구별 스키마를 기술하지 않음)은 손대지 않아npm·cargo·redocly는 돌리지 않았다. 버전 범프·릴리즈 노트는 하지 않았다. - 보류 아이디어:
attributes.*의 배열·객체 값이 두 저장 모드에서 다른 텍스트로 렌더링됨(SQLite 은 공백 없는 JSON, PG#>>는{"k": "v"}) — 컨테이너를 가리키는 절을 아예 거절하는 편이 나을 수도 (가치 3 / 위험 2 / S) · Query DSL 에attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) · Query DSL 실행에 offset 이 없어 상한 500 을 넘는 나머지를 받아낼 방법이 아예 없음 —/api/v1/assets처럼 offset·total 을 주는 것이 대안 (가치 3 / 위험 2 / M) ·listAgents·설정 목록/이력·자산 상세 루프가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 — 드라이버 fault injection 없이는 테스트 불가 (가치 3 / 위험 1 / M) · MCPasset_search가 0건일 때 실제 존재하는 type·status 값을 함께 돌려주어 ‘자산 없음’과 ‘필터 값 없음’을 구분하게 함 (가치 3 / 위험 2 / M)
2026-09-10
- 선택: MCP
asset_relations가 관계 목록에 상한을 두지 않아 무한정 큰 응답을 돌려줌 (가치 3 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
mcpAssetRelations만 유일하게 LIMIT 이 없어 자산의 활성 관계를 전부 돌려줬다 — process 상관관계가 돈 서버에서runs_on자식을 수천 개 가진 host 하나면 도구 호출 한 번의 응답이 모델 context 를 통째로 차지하고, 잘라내는 쪽은 클라이언트였다. 다른 읽기 도구와 같은limit(기본 50, 최대 100)·offset을 받고, 한 행을 더 읽어 버리는 lookahead 로has_more·next_offset을 사실로 보고하게 했다. 기존relations키를 그대로 유지해 응답 호환은 깨지지 않는다. 겸사겸사 가이드의 MCP 도구 표를mcpTools스키마와 대조하는 테스트를 추가했고 (openapi.yaml 대 라우터를 대조하는TestOpenAPIRouteCoverage와 같은 방식), 그 테스트가 실제로 기존 드리프트인software_inventory의vendor누락을 잡아내 문서를 함께 고쳤다. 검증:mcp_pagination_test.go에 두 테스트 추가 — 3개 관계를 limit 3 으로 읽으면has_morefalse, limit 2 면 2건·has_moretrue·next_offset2 이고 그 offset 으로 나머지 1건이 오며 세 페이지 합이 각 edge 정확히 1회(lookahead 행이 섞이지 않음), 관계 55개에서 limit 미지정이면 50건·has_moretrue. 헛돌지 않는지 보려고 lookahead 와 비교식을 옛 방식으로 되돌려 두 테스트가 정확히 그 지점에서 실패하는 것을 확인한 뒤 복구했다. 문서 대조 테스트도 표를 고치기 전에asset_relations·software_inventory두 드리프트를 차례로 잡는 것을 확인했다.go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt통과. web·Rust·openapi.yaml(MCP 는 도구별 스키마를 기술하지 않음)은 손대지 않아npm·cargo·redocly는 돌리지 않았다. 버전 범프·릴리즈 노트는 하지 않았다. -
보류 아이디어: Query DSL 실행에 offset 이 없어 상한 500 을 넘는 나머지를 받아낼 방법이 아예 없음 —
/api/v1/assets처럼 offset·total 을 주는 것이 대안 (가치 3 / 위험 2 / M) · Query DSL 에attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) · MCPasset_search가 0건일 때 실제 존재하는 type·status 값을 함께 돌려주어 ‘자산 없음’과 ‘필터 값 없음’을 구분하게 함 (가치 3 / 위험 2 / M) ·listAgents·설정 목록/이력·자산 상세 루프가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 — storage 에 테스트용 공개 생성자가 없어 fault injection 불가 (가치 3 / 위험 1 / M) ·attributes.*의 배열·객체 값이 두 저장 모드에서 다른 텍스트로 렌더링됨 — 컨테이너를 가리키는 절을 아예 거절하는 편이 나을 수도 (가치 3 / 위험 2 / S) - 릴리즈: v0.2.30 (2026-09-10, run 2026-09-10-094107-Invenqor-improve)
2026-09-10
- 선택: Query DSL 실행에 offset 이 없어 상한 500 을 넘는 나머지를 받아낼 방법이 아예 없음 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
query/execute는 최대 500행을 읽고 offset 이 없어, 조건에 맞는 자산이 그보다 많으면 나머지에 도달할 길이 전혀 없었다 — 조건을 잘게 쪼개 여러 번 물어 이어붙이는 것이 유일한 방법이었고, 같은 엔드포인트를 API key 로 읽는 스크립트에는truncated를 알아차릴 콘솔조차 없다./api/v1/assets와 같은offset입력을 받고offset·has_more·next_offset을 돌려주어has_more인 동안next_offset을 되먹여 전체를 순회하게 했고, 같은 WHERE 에COUNT(*)를 한 번 더 걸어total로 “몇 건이 맞았는지”를 돌려준다(감사 기록query.execute에도offset·total을 남긴다).truncated는 값과 의미를 그대로 두어(=has_more) 기존 콘솔·클라이언트는 깨지지 않는다. 페이징을 넣으면서 정렬에id타이브레이커를 더했다 — 한 번에 수집된 자산은last_seen_at이 초 단위로 같아, 그 컬럼만으로 정렬하면 순서가 엔진에 맡겨져 페이지를 넘기다 어떤 행은 두 번 나오고 어떤 행은 영영 안 나온다. 루프 직후rows.Err()검사도 함께 넣었다(보류 아이디어에 있던executeQuery부분). 검증:server/internal/httpapi/query_pagination_test.go추가 — 자산 5건을 limit 2 로 물으면total5·has_moretrue·next_offset2 이고,next_offset을 따라가는 순회가 5건을 정확히 1회씩 보며 마지막 페이지의next_offset이 5·has_morefalse 인지, 끝을 넘긴 offset 이 빈 페이지에total5 를 주는지, 그리고last_seen_at이 같은 자산 8건을 limit 3 으로 넘겨도 중복·누락이 없는지를 확인한다. 헛돌지 않는지 보려고 offset 입력을 무시하도록 되돌리자 두 테스트가 각각 “offset 2 를 물었는데 0”·”8건 중 3건만 보임”으로 실패했고,next_offset을offset + limit으로 바꾸자 마지막 페이지 단언이 6≠5 로 실패하는 것을 확인한 뒤 복구했다. 다만id타이브레이커를 빼는 변형은 SQLite 에서 테스트가 통과한다 — 두 엔진 모두 이 크기에서는 순서가 우연히 일정하다. 타이 테스트는 회귀 방어용으로 남겼다.go test ./...를 SQLite fallback 과 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt통과.openapi.yaml에QueryInput.offset과 두 execute 엔드포인트의 새 응답 필드를 기술하고@redocly/cli@2.47.0 lint로 경고 수가 변경 전과 같은 6개(모두 기존 것)임을 확인했다.docs/API_MCP_GUIDE.md에 페이징 설명과 curl 예시를 넣었다. web· Rust 는 손대지 않아npm·cargo는 돌리지 않았다(콘솔은items·truncated만 읽으므로 추가된 키는 무해하다). 버전 범프·릴리즈 노트는 하지 않았다. -
보류 아이디어: Query DSL 에
attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) · MCPasset_relations가 상대 자산의 이름·종류를 주지 않아 edge 마다asset_get을 한 번 더 부르게 만듦 (가치 3 / 위험 2 / M) · MCPasset_search가 0건일 때 실제 존재하는 type·status 값을 함께 돌려주어 ‘자산 없음’과 ‘필터 값 없음’을 구분하게 함 (가치 3 / 위험 2 / M) ·listAgents·설정 목록/이력·자산 상세 루프가rows.Err()를 확인하지 않아 부분 결과를 200 으로 돌려줌 — fault injection 불가, 이번에executeQuery만 해결 (가치 3 / 위험 1 / M) · 콘솔 Query DSL 화면이 새total·offset을 쓰지 않아 여전히 첫 페이지만 보여줌 (가치 3 / 위험 2 / M) - 릴리즈: v0.2.31 (2026-09-10, run 2026-09-10-173121-Invenqor-improve)
2026-09-10
- 선택: 사용자·관리자 가이드를 화면 캡처가 들어간 완성본으로 만든다 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약: 두 가이드는 분량은 충분했지만 캡처가 한 장도 없어 콘솔을 처음 여는 사람이
문장만 읽고 화면을 상상해야 했다.
scripts/capture-guide-screenshots.mjs를 새로 만들어 임시 디렉터리에 Server 를 빌드·기동하고 가공한 자산 6대·계정 3개·API key 2개를 채운 뒤 headless Chrome(CDP) 으로 1440x900 에서 21장을 촬영하게 했다. 대상 주소를 환경 변수로 받지 않아 실수로 운영 배포를 촬영·변경할 수 없고, Server 가os.Hostname()을 진단 기록·request ID 에 남기므로 UTS namespace 안에서 띄워 빌드 장비 이름이 감사 로그·Server 로그 캡처에 실리지 않게 했다(첫 촬영본에서 실제로DESKTOP-…가 찍혀 발견). 사용자 가이드에 “처음 5분”, 화면별 사용법 6절, 자주 하는 작업, 용어를 더하고 관리자 가이드에 코드(config.Load())에서 읽은 Server 기동 환경 변수 전수 표, migration 에서 읽은 내장 역할별 권한 표, 운영 설정 화면과 감사·Server 로그 절을 더했다. PDF 는 저장소 자체 변환기 대신 공용 도구를 쓰도록 두 문서를scripts/build-docs.sh에서 빼고scripts/build-guide-pdfs.sh로 옮겨 정본을 하나로 두었다. 검증: 캡처 스크립트를 4회 실제 실행해 21장이 모두 데이터가 찬 화면인지 눈으로 확인했고(빈 목록·스피너·실명 없음), 공용 md2pdf 로 구운 PDF 에 이미지 XObject 가 각각 12·9개 들어갔고/URI (file://가 남지 않았음을 확인했다. 링크 rewrite 를 빠뜨린 첫 빌드에서 실제로file:///tmp/guide-…/USER_GUIDE.md가 검출돼 raw HTMLhref=까지 rewrite 하도록 고쳤다.sh -n으로 두 셸 스크립트 구문을 확인했다. Go·Rust·web 코드는 손대지 않아 해당 테스트는 돌리지 않았다. 버전 범프·릴리즈 노트는 하지 않았다. - 보류 아이디어: Query DSL 에
attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) · MCPasset_relations가 상대 자산의 이름·종류를 주지 않아 edge 마다asset_get을 한 번 더 부르게 만듦 (가치 3 / 위험 2 / M) · 콘솔 Query DSL 화면이 새total·offset을 쓰지 않아 여전히 첫 페이지만 보여줌 (가치 3 / 위험 2 / M) · MCPasset_search가 0건일 때 실제 존재하는 type·status 값을 함께 돌려주어 ‘자산 없음’과 ‘필터 값 없음’을 구분하게 함 (가치 3 / 위험 2 / M) · 캡처 스크립트가 촬영한 화면 목록과 가이드가 싣는 그림 목록을 대조하는 테스트가 없어 화면 이름이 바뀌면 조용히 옛 그림이 남음 (가치 2 / 위험 1 / S)
2026-09-11
-
(원장 항목에 비밀/내부 정보 의심 문자열이 있어 비공개 기록으로 옮김 — run 2026-09-11-103112-Invenqor-improve)
- 릴리즈: v0.2.32 (2026-09-11, run 2026-09-11-103112-Invenqor-improve)
2026-09-12
- 선택: 캠페인 tracking-2026-09 — 관리자가 화면에서 방문 추적 스크립트를 붙이는 체계(nonce CSP · Momento 같은 오리진 프록시 · 차단 출처 기록) (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 콘솔은
script-src 'self'로 잠겨 있어 스니펫을 붙여도 브라우저가 조용히 버렸다.server/internal/tracking패키지(설정·검증·스니펫 렌더링·출처 추출·위반 기록)와httpapi/tracking.go(DBserver_metadata.tracking_policy저장, GET/PATCH/api/v1/admin/settings/tracking, 위반 목록·비우기·한 번에 허용, 무인증POST /api/v1/tracking/csp-report,/momento/*리버스 프록시)를 만들고securityHeaders가 페이지 요청마다 nonce 를 만들어 정책과 스니펫에 같이 넣게 했다.webui.Decorated가 임베드된 index.html 에 스니펫을 삽입한다(/index.html직접 요청도 같은 셸). provider 는 momento·ga4·gtm·matomo·custom 이고 Momento 는 기본으로 같은 오리진 프록시(세션 쿠키·Authorization 제거, Set-Cookie 폐기, 256KB, 10초)를 써 외부 출처가 정책에 안 들어간다.'unsafe-inline'은 어떤 입력으로도 나오지 않고(allowed_hosts 는 http(s) 출처만 통과), 꺼져 있으면 정책 문자열이 이전과 바이트 단위로 같으며 DB 에 아무것도 쓰지 않는다. 비화면 경로 (/api·/v1·/health·/mcp·/momento)는default-src 'none'으로 좁혔다.include_admin은 두지 않았다 — Invenqor 콘솔은 SPA 셸 하나에 해시 경로라 Server 가 관리 화면을 구분할 수 없고 콘솔 전체가 관리 화면이다(가이드에 이유를 적음). 콘솔에 설정 → 방문 추적 탭(provider 카드, 항목별 입력, 정책에 더해진 출처, 차단 목록과 “허용 목록에 추가”·”기록 비우기”)을 넣고webui/dist를 재빌드했다(이 저장소는 dist 를 추적하고 CI 가 대조한다). openapi.yaml 에 8개 오퍼레이션과 2개 스키마를 더했고, ADMIN_GUIDE.md 에 20장(설정·nonce/CSP 설명·프록시·차단 출처·API)을 넣고 PDF 를 다시 구웠다(51쪽,file://없음). 검증:tracking단위 테스트 8개(기본 꺼짐, 프록시 시 출처 0, 모든<script>에 nonce, 8KB·키워드 거절, 위반 100개 고리·와일드카드 허용 표시)와 httpapi 통합 테스트 4개 (기본 정책 불변·DB 무기록, 다른 Pod 에서 같은 스니펫과 요청마다 다른 nonce, placement=body, 프록시 쿠키 제거, 끄면 원래 정책, 붙여 넣은 스니펫 출처가 세 지시어에, 잘못된 입력 400, 신고→목록→허용→비우기)를 추가해go test ./...를 SQLite 와 실 PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과,go vet·go build·gofmt, vitest 134개 통과,@redocly/cli@2.47.0 lint경고 6개(모두 기존). 표준 검증 항목 “실제로 띄워 수집이 들어오는 것”은 임시 Server 와 가짜 수집기를 띄우고 headless Chrome 으로 열어 확인했다 — nonce 붙은 스니펫이 실행돼document.title을 바꿨고, 수집기에GET /t.js가 도착했으며, 정책에 없는 출처로의 fetch 는 브라우저가 막고csp-report로 신고돼 위반 목록에connect-src http://127.0.0.1:17173로 나타났다. 실제 Momento 수집기는 이 환경에 없어 프록시는 httptest 업스트림으로만 확인했다. 새 화면 캡처는 넣지 않았다(캡처 스크립트·PNG·가이드 세 곳을 대조하는 테스트가 있어 다음 회차 과제). 버전 범프·릴리즈 노트는 하지 않았다. -
보류 아이디어: 설정 → 방문 추적 화면 캡처를 캡처 스크립트에 추가해 가이드 20장에 싣기 (가치 2 / 위험 1 / S) · Query DSL 에
attributes.<키>존재/부재 연산자가 없어>= ""우회가 필요함 (가치 3 / 위험 2 / M) · MCPasset_relations가 상대 자산의 이름·종류를 주지 않아 edge 마다asset_get을 한 번 더 부르게 만듦 (가치 3 / 위험 2 / M) · 콘솔 Query DSL 화면이 새total·offset을 쓰지 않아 여전히 첫 페이지만 보여줌 (가치 3 / 위험 2 / M) · 추적 정책을 페이지 요청마다 DB 에서 읽음 — 트래픽이 많은 설치에서는 짧은 TTL 캐시가 필요할 수 있음 (가치 2 / 위험 2 / S) - 릴리즈: v0.2.33 (2026-09-12, run 2026-09-12-075116-Invenqor-improve)