자율 개선 일일 보고 — 2026-09-08
요약. 2026-09-08에 자율 개선 에이전트가 25개 프로젝트에서 53회차를 돌려 29건을 릴리즈하고 8건은 머지만 했으며 0건은 변경이 없었고 실패는 0건이다. 에이전트 시간 11시간 22분, 추정 비용 $227.63. 릴리즈: AgentHub v0.236.0, relio v1.11.19, Clustara v0.9.275, ReSSO v0.9.71, Invenqor v0.2.26, Vendra v0.7.46, ai-admin v1.2.11, dataworks v0.9.45….
- 53회차
- 25프로젝트
- 30배포 준비 완료
- 3릴리즈 진행 중
- 7병합 완료
- 9검토 대기
- 4검증 실패
- 0변경 없음
- 0실행 오류
- $227.63비용
- 11시간 22분에이전트 시간
회차
| 시각 | 프로젝트 | 결과 |
|---|---|---|
| 09:39 | AgentHub | 릴리즈 진행 중 release-only, released v0.236.0, ASSETS MISSING |
| 09:53 | relio | 배포 준비 완료 release-only, released v1.11.19 |
| 10:20 | AgentHub | 검토 대기 review held, PR open PR #13 |
| 10:56 | Clustara | 배포 준비 완료 merged PR #14, released v0.9.275, asset manifest failed |
| 11:15 | Clustara | 릴리즈 진행 중 assets-only, assets for v0.9.275, asset manifest failed, ASSETS MISSING |
| 11:18 | Clustara | 배포 준비 완료 assets-only, assets for v0.9.275 |
| 12:28 | Invenqor | 병합 완료 merged PR #10, release missing |
| 13:13 | ReSSO | 배포 준비 완료 merged PR #11, released v0.9.71 |
| 14:24 | Invenqor | 배포 준비 완료 release-only, released v0.2.26 |
| 14:52 | Vendra | 배포 준비 완료 merged PR #112, released v0.7.46 |
| 15:25 | ai-admin | 배포 준비 완료 merged PR #12, released v1.2.11 |
| 15:40 | ai-admin | 검토 대기 guarded files, PR open PR #13 |
| 15:57 | aiportal-front-admin | 병합 완료 merged PR #10, release skipped |
| 16:11 | aiportal-front | 검토 대기 needs approval (risk=medium, files=2), PR open PR #8 |
| 16:25 | aiportal-java | 검증 실패 verify failed: 실패한 검증: ./gradlew --quiet test (exit 126) |
| 16:41 | aiportal-py | 병합 완료 merged PR #11, release skipped |
| 17:01 | appstore | 검토 대기 review held, PR open PR #7 |
| 17:29 | dataworks | 배포 준비 완료 merged PR #10, released v0.9.45 |
| 17:43 | git-ctx | 검토 대기 review held, PR open PR #22 |
| 18:10 | Clustara | 배포 준비 완료 merged PR #15, released v0.9.276 |
| 18:12 | AgentHub | 배포 준비 완료 merged PR #14, released v0.237.0 |
| 18:28 | Invenqor | 배포 준비 완료 merged PR #11, released v0.2.27 |
| 18:54 | ai-admin | 배포 준비 완료 merged PR #14, released v1.2.12 |
| 18:55 | Vendra | 배포 준비 완료 merged PR #113, released v0.7.47 |
| 19:04 | ReSSO | 배포 준비 완료 merged PR #12, released v0.9.72 |
| 19:17 | aiportal-java | 검증 실패 verify failed: 실패한 검증: ./gradlew --quiet test (exit 1) |
| 19:20 | aiportal-front | 검토 대기 needs approval (risk=medium, files=5), PR open PR #9 |
| 19:47 | dataworks | 배포 준비 완료 merged PR #11, released v0.9.46 |
| 19:56 | appstore | 배포 준비 완료 merged PR #8, released v2.5.3 |
| 20:14 | git-ctx | 배포 준비 완료 merged PR #23, released v0.77.8 |
| 20:20 | aiportal-front-admin | 병합 완료 resumed: merged PR #11 |
| 20:31 | igame | 검증 실패 verify failed: 실패한 검증: cd web && npm run build --silent (exit 1) |
| 20:37 | jupiq | 배포 준비 완료 merged PR #6, released v1.4.8 |
| 20:41 | jikim | 배포 준비 완료 merged PR #17, released v0.2.4 |
| 20:53 | git-ctx | 배포 준비 완료 resumed: merged PR #23, released v0.77.8 |
| 21:06 | aiportal-front | 병합 완료 merged PR #8 (approved), release skipped |
| 21:07 | aiportal-front | 병합 완료 merged PR #9 (approved), release skipped |
| 21:20 | moyro | 검증 실패 verify failed: 실패한 검증: cd webapp && ([ -d node_modules ] || npm ci --no-audit --no-fund) && npm run typecheck --silent && npm |
| 21:26 | moina | 릴리즈 진행 중 merged PR #10, release tag held (failed) |
| 21:33 | kanpic | 배포 준비 완료 merged PR #11, released v0.238.0 |
| 21:39 | igame | 배포 준비 완료 assets-only, assets for v0.7.7, asset conflict |
| 21:58 | pii-masker | 배포 준비 완료 merged PR #12, released v1.0.15 |
| 22:07 | releasedock | 배포 준비 완료 merged PR #11, released v0.5.10 |
| 22:09 | ptium | 배포 준비 완료 merged PR #10, released v1.69.28 |
| 22:19 | relio | 병합 완료 merged PR #13 |
| 22:49 | umm | 배포 준비 완료 merged PR #145, released v0.71.3 |
| 22:50 | weekly | 검토 대기 CI timeout, PR open PR #10 |
| 23:21 | Clustara | 배포 준비 완료 merged PR #16, released v0.9.277 |
| 23:26 | AgentHub | 배포 준비 완료 merged PR #15, released v0.238.0 |
| 23:28 | Invenqor | 배포 준비 완료 merged PR #12, released v0.2.28 |
| 23:42 | ReSSO | 검토 대기 guarded files, PR open PR #13 |
| 23:46 | Vendra | 검토 대기 review held, PR open PR #114 |
| 23:54 | ai-admin | 배포 준비 완료 merged PR #15, released v1.2.14 |
비용·사용량
| 시각 | 프로젝트 | 단계 | 시간 | 턴 | 비용 | 토큰 입력/출력 | 종료 |
|---|---|---|---|---|---|---|---|
| 09:22 | AgentHub | 릴리즈 | 3분 | 24 | $1.07 | 866K / 7K | success |
| 09:46 | relio | 릴리즈 | 3분 | 23 | $1.13 | 855K / 12K | success |
| 10:13 | AgentHub | 개선 | 14분 | 75 | $6.69 | 7.7M / 56K | success |
| 10:20 | AgentHub | review | 6분 | 37 | $2.36 | 2.1M / 23K | success |
| 10:41 | Clustara | 개선 | 11분 | 55 | $4.19 | 4.1M / 44K | success |
| 10:49 | Clustara | review | 7분 | 24 | $1.51 | 805K / 20K | success |
| 10:54 | Clustara | 릴리즈 | 5분 | 28 | $1.51 | 1.1M / 13K | success |
| 11:01 | Clustara | 자산 | 2분 | 21 | $0.86 | 606K / 6K | success |
| 11:18 | Clustara | 자산 | 2분 | 21 | $0.87 | 544K / 7K | success |
| 11:26 | Invenqor | 개선 | 6분 | 49 | $2.36 | 2.3M / 20K | success |
| 11:29 | Invenqor | review | 2분 | 22 | $0.86 | 707K / 8K | success |
| 12:28 | Invenqor | 릴리즈 | 0분 | 0 | $0.00 | 0 / 0 | unknown |
| 12:48 | ReSSO | 개선 | 18분 | 48 | $3.43 | 3.5M / 29K | success |
| 12:53 | ReSSO | review | 5분 | 26 | $1.70 | 1.4M / 17K | success |
| 13:01 | ReSSO | 릴리즈 | 6분 | 28 | $1.47 | 1.3M / 10K | success |
| 14:18 | Invenqor | 릴리즈 | 1시간 10분 | 62 | $3.16 | 3.6M / 22K | success |
| 14:43 | Vendra | 개선 | 14분 | 81 | $5.84 | 6.2M / 55K | success |
| 14:48 | Vendra | review | 5분 | 43 | $2.48 | 2.6M / 18K | success |
| 14:51 | Vendra | 릴리즈 | 2분 | 15 | $0.53 | 322K / 5K | success |
| 15:18 | ai-admin | 릴리즈 | 3분 | 25 | $1.00 | 877K / 7K | success |
| 15:40 | ai-admin | 개선 | 10분 | 71 | $4.19 | 4.8M / 33K | success |
| 15:54 | aiportal-front-admin | 개선 | 4분 | 27 | $1.62 | 1.2M / 16K | success |
| 15:56 | aiportal-front-admin | review | 1분 | 9 | $0.44 | 256K / 4K | success |
| 15:57 | aiportal-front-admin | 릴리즈 | 1분 | 13 | $0.37 | 193K / 4K | success |
| 16:08 | aiportal-front | 개선 | 9분 | 29 | $2.48 | 1.7M / 35K | success |
| 16:11 | aiportal-front | review | 3분 | 14 | $0.97 | 582K / 10K | success |
| 16:25 | aiportal-java | 개선 | 4분 | 35 | $1.86 | 1.3M / 20K | success |
| 16:36 | aiportal-py | 개선 | 6분 | 35 | $2.22 | 1.8M / 25K | success |
| 16:39 | aiportal-py | review | 3분 | 17 | $1.11 | 709K / 13K | success |
| 16:41 | aiportal-py | 릴리즈 | 1분 | 12 | $0.42 | 207K / 4K | success |
| 16:58 | appstore | 개선 | 8분 | 46 | $2.95 | 2.6M / 34K | success |
| 17:01 | appstore | review | 3분 | 21 | $0.84 | 462K / 10K | success |
| 17:18 | dataworks | 개선 | 8분 | 62 | $3.83 | 4.2M / 30K | success |
| 17:22 | dataworks | review | 3분 | 20 | $1.00 | 787K / 9K | success |
| 17:26 | dataworks | 릴리즈 | 4분 | 27 | $1.38 | 1.3M / 9K | success |
| 17:37 | git-ctx | 개선 | 7분 | 22 | $1.56 | 847K / 23K | success |
| 17:43 | git-ctx | review | 5분 | 16 | $1.11 | 593K / 18K | success |
| 17:58 | Invenqor | 개선 | 8분 | 48 | $2.65 | 2.4M / 28K | success |
| 17:58 | AgentHub | 개선 | 8분 | 67 | $4.41 | 4.9M / 32K | success |
| 17:59 | Clustara | 개선 | 9분 | 43 | $2.92 | 2.3M / 34K | success |
| 18:02 | Invenqor | review | 3분 | 20 | $0.95 | 706K / 10K | success |
| 18:02 | AgentHub | review | 3분 | 16 | $0.98 | 653K / 8K | success |
| 18:03 | Clustara | review | 3분 | 13 | $0.71 | 437K / 8K | success |
| 18:05 | AgentHub | 릴리즈 | 2분 | 19 | $0.78 | 566K / 6K | success |
| 18:08 | Clustara | 릴리즈 | 5분 | 29 | $1.38 | 1.1M / 11K | success |
| 18:22 | Invenqor | 릴리즈 | 20분 | 54 | $2.48 | 2.5M / 19K | success |
| 18:39 | ai-admin | 개선 | 9분 | 49 | $3.58 | 3.5M / 34K | success |
| 18:39 | ReSSO | 개선 | 2분 | 7 | $4.76 | 795K / 5K | success |
| 18:42 | ai-admin | review | 3분 | 22 | $0.81 | 547K / 8K | success |
| 18:43 | ReSSO | review | 4분 | 20 | $1.00 | 794K / 9K | success |
| 18:46 | Vendra | 개선 | 17분 | 91 | $7.10 | 8.6M / 55K | success |
| 18:47 | ai-admin | 릴리즈 | 2분 | 19 | $0.81 | 592K / 5K | success |
| 18:52 | Vendra | review | 6분 | 40 | $2.59 | 2.6M / 20K | success |
| 18:52 | ReSSO | 릴리즈 | 5분 | 25 | $1.20 | 1.0M / 9K | success |
| 18:54 | Vendra | 릴리즈 | 2분 | 16 | $0.54 | 312K / 6K | success |
| 19:17 | aiportal-front-admin | 개선 | 7분 | 58 | $2.67 | 2.3M / 30K | success |
| 19:17 | aiportal-front | 개선 | 7분 | 36 | $2.17 | 1.6M / 29K | success |
| 19:17 | aiportal-java | 개선 | 7분 | 42 | $2.79 | 2.5M / 29K | success |
| 19:19 | aiportal-front-admin | review | 2분 | 11 | $0.76 | 401K / 8K | success |
| 19:20 | aiportal-front | review | 4분 | 17 | $1.09 | 671K / 14K | success |
| 19:21 | aiportal-front-admin | 릴리즈 | 1분 | 11 | $0.44 | 237K / 4K | success |
| 19:36 | git-ctx | 개선 | 6분 | 18 | $1.35 | 767K / 19K | success |
| 19:37 | dataworks | 개선 | 7분 | 47 | $2.86 | 2.8M / 27K | success |
| 19:40 | dataworks | review | 3분 | 11 | $0.82 | 422K / 9K | success |
| 19:41 | appstore | 개선 | 11분 | 84 | $5.51 | 6.6M / 40K | success |
| 19:42 | git-ctx | review | 4분 | 17 | $0.98 | 450K / 16K | success |
| 19:44 | appstore | review | 3분 | 17 | $0.98 | 669K / 11K | success |
| 19:44 | dataworks | 릴리즈 | 4분 | 26 | $1.38 | 1.1M / 10K | success |
| 19:49 | appstore | 릴리즈 | 4분 | 25 | $1.17 | 976K / 8K | success |
| 19:53 | git-ctx | 릴리즈 | 8분 | 28 | $1.55 | 1.3M / 12K | success |
| 20:25 | jupiq | 개선 | 5분 | 29 | $1.50 | 1.2M / 17K | success |
| 20:27 | jupiq | review | 1분 | 8 | $0.39 | 202K / 4K | success |
| 20:27 | jikim | 개선 | 7분 | 38 | $2.84 | 2.2M / 31K | success |
| 20:31 | igame | 개선 | 2분 | 8 | $6.66 | 809K / 9K | success |
| 20:31 | jupiq | 릴리즈 | 2분 | 17 | $0.86 | 673K / 5K | success |
| 20:31 | jikim | review | 4분 | 16 | $1.18 | 714K / 15K | success |
| 20:34 | jikim | 릴리즈 | 2분 | 21 | $1.08 | 810K / 7K | success |
| 20:53 | git-ctx | 릴리즈 | 3분 | 14 | $0.71 | 343K / 6K | success |
| 20:56 | ai-admin | 릴리즈 | 2분 | 22 | $1.11 | 968K / 7K | success |
| 21:06 | aiportal-front | 릴리즈 | 1분 | 11 | $0.38 | 197K / 3K | success |
| 21:07 | aiportal-front | 릴리즈 | 1분 | 9 | $0.29 | 149K / 3K | success |
| 21:12 | kanpic | 개선 | 4분 | 20 | $1.32 | 903K / 14K | success |
| 21:13 | moina | 개선 | 4분 | 43 | $2.04 | 1.9M / 17K | success |
| 21:15 | kanpic | review | 2분 | 11 | $0.70 | 382K / 8K | success |
| 21:16 | moina | review | 3분 | 22 | $0.96 | 782K / 9K | success |
| 21:19 | moyro | 개선 | 11분 | 60 | $3.37 | 3.3M / 33K | success |
| 21:23 | kanpic | 릴리즈 | 2분 | 21 | $0.74 | 484K / 7K | success |
| 21:24 | moina | 릴리즈 | 3분 | 25 | $1.16 | 903K / 9K | success |
| 21:39 | igame | 자산 | 2분 | 11 | $0.53 | 288K / 4K | success |
| 21:54 | pii-masker | 개선 | 3분 | 27 | $1.65 | 1.4M / 13K | success |
| 21:54 | ptium | 개선 | 4분 | 24 | $1.26 | 947K / 13K | success |
| 21:55 | pii-masker | review | 1분 | 13 | $0.50 | 273K / 5K | success |
| 21:56 | ptium | review | 1분 | 7 | $0.44 | 201K / 5K | success |
| 21:56 | releasedock | 개선 | 6분 | 33 | $1.88 | 1.4M / 23K | success |
| 21:58 | pii-masker | 릴리즈 | 2분 | 16 | $0.56 | 407K / 5K | success |
| 21:59 | releasedock | review | 3분 | 15 | $0.81 | 525K / 10K | success |
| 22:03 | releasedock | 릴리즈 | 4분 | 32 | $0.92 | 785K / 8K | success |
| 22:04 | ptium | 릴리즈 | 5분 | 35 | $1.65 | 1.6M / 12K | success |
| 22:16 | relio | 개선 | 6분 | 34 | $2.03 | 1.6M / 22K | success |
| 22:18 | relio | review | 2분 | 12 | $0.71 | 432K / 7K | success |
| 22:18 | umm | 개선 | 8분 | 42 | $2.33 | 2.1M / 27K | success |
| 22:21 | umm | review | 3분 | 18 | $0.86 | 430K / 11K | success |
| 22:26 | weekly | 개선 | 16분 | 60 | $5.39 | 5.8M / 39K | success |
| 22:31 | weekly | review | 4분 | 26 | $1.06 | 959K / 9K | success |
| 22:35 | umm | 릴리즈 | 4분 | 24 | $1.16 | 875K / 12K | success |
| 23:07 | Invenqor | 개선 | 7분 | 36 | $1.95 | 1.7M / 22K | success |
| 23:10 | Invenqor | review | 3분 | 18 | $0.88 | 628K / 10K | success |
| 23:10 | Clustara | 개선 | 11분 | 55 | $3.63 | 3.6M / 36K | success |
| 23:11 | AgentHub | 개선 | 12분 | 59 | $5.04 | 5.5M / 42K | success |
| 23:14 | Clustara | review | 3분 | 16 | $1.03 | 684K / 11K | success |
| 23:16 | AgentHub | review | 4분 | 21 | $1.41 | 1.1M / 13K | success |
| 23:19 | Clustara | 릴리즈 | 4분 | 24 | $1.12 | 779K / 11K | success |
| 23:19 | AgentHub | 릴리즈 | 3분 | 21 | $0.84 | 679K / 6K | success |
| 23:21 | Invenqor | 릴리즈 | 9분 | 47 | $2.17 | 2.1M / 17K | success |
| 23:37 | ai-admin | 개선 | 7분 | 57 | $3.69 | 4.0M / 28K | success |
| 23:42 | ReSSO | 개선 | 12분 | 82 | $6.50 | 8.2M / 43K | success |
| 23:42 | ai-admin | review | 5분 | 27 | $1.54 | 1.3M / 16K | success |
| 23:43 | Vendra | 개선 | 13분 | 88 | $7.23 | 9.0M / 52K | success |
| 23:45 | ai-admin | 릴리즈 | 2분 | 16 | $0.77 | 548K / 5K | success |
| 23:46 | Vendra | review | 3분 | 19 | $1.24 | 872K / 12K | success |
무엇을 왜 바꿨나 (원장 발췌)
AgentHub
- 선택: 데이터 등급 조건이 붙은 정책 규칙이 Pod까지 전달되지 않던 문제 수정 (가치 4 / 위험 3 / 작업량 M)
- 결과: 성공 — 커밋 47a7de9 (auto/2026-09-08-1000)
- 요약: policy 패키지 주석이 첫 문장으로 드는 예시의 뒷 절 — “누구도 주민등록번호를 모델로 보낼 수 없다” — 의 나머지 절반, 즉 “외부 도구로도 보낼 수 없다”를 쓰는 운영자는 데이터 등급 조건이 붙은 tool.call 규칙 하나를 씁니다. 그 규칙은 어느 Pod에도 도달하지 않았습니다. 컴파일은 “이 규칙이 이 에이전트·이 서버에 해당하는가”를 아무것도 스캔되지 않은 요청에 대해 묻는데, 등급 조건이 있는 규칙은 스캔되지 않은 요청에 매치하지 않으므로 그냥 탈락했고, 런타임은 그 규칙이 애초에 없는 것처럼 프로비저닝됐습니다. 대신 그 등급의 전역 조치가 결정했으므로, 주민등록번호를
기록만으로 두고 나머지를 규칙으로 막으려던 배포는 값을 MCP 서버로 그대로 보내면서 findings만 남겼습니다. 다른 화면은 전부 문서와 일치했습니다: 시뮬레이터는 데이터 등급을 입력받아 실제로 나가고 있던 그 호출에 계속차단이라고 답했고, 콘솔은 등급을 다른 조건과 나란히 두고 “도구 호출 시점에 강제합니다”라고 적어 두었으며, task.create·runtime.start는 API가 문서를 평가하므로 지켜졌습니다. 오직 tool.call만 Pod 안에서 판정되는데, 에이전트가 우회할 수 없는 그 게이트웨이에는 규칙이 없었습니다. 그리고 게이트웨이는 이 규칙을 답할 수 있는 유일한 곳이기도 합니다 — 도구 호출은 컨트롤 플레인을 지나가지 않으므로 무엇을 싣고 있는지는 스캔 뒤 거기에서만 알 수 있습니다. 이제 등급이 규칙에 실려 그대로 이동하고, 스캐너가 이미 도는 자리에서 판정됩니다: 거절하거나, 이름으로 걸린 규칙이 찾아가는 것과 같은 검토자에게 보냅니다. 스캔이 답을 바꾼 경우에만 작동하므로 이름으로 이미 게이트된 도구가 같은 사람에게 두 번 가지 않습니다. 검토자에게는 마스킹된 인자를 보냅니다 — 승인은 컨트롤 플레인에 저장되므로 원문을 보내면 이 규칙이 지키려던 바로 그 값을 승인 테이블에 복사하게 됩니다. 요약 목록은 이 조건을 표현할 수 없어(“이 도구”가 아니라 “이 도구에 고객정보가 실렸을 때”) 싣지 않았고, 그래서 요약만 읽는 옛 Pod는 지금과 완전히 같습니다. 같은 이유로 도구 목록에서 숨기지도 않습니다. 검증: 수정 전 양 끝에서 실패를 확인(규칙이 아무것도 컴파일되지 않음, 게이트웨이가 호출과 응답을 그대로 통과 — 게이트웨이 테스트 4개 실패), 새 스윕이 규칙 3개 조합 전체 × 기본값 3종 × 스캔이 보고할 수 있는 등급 집합 5종에 대해 “게이트웨이가 받은 것 == 문서의 판정”을 검사하고, reflection 스윕 2개가 컴파일된 규칙의 필드가 binding까지 가지 않거나 CRD에 선언되지 않으면 클러스터가 아니라 여기서 실패합니다. go vet ./…, go test -race ./cmd/… ./internal/…, web npm ci+lint+build,release-catalog-images.sh check-versions·validate모두 통과. internal/policy·cmd/runtime-proxy가 런타임 base 이미지 소스라 BASE_VERSION 0.24.0으로 상향(5곳). - 보류 아이디어: 정책 시뮬레이터가 ‘이 규칙이 Pod에서 어떻게 컴파일되는가’를 보여주지 않아 순서 실수를 저장 전에 볼 수 없음 (3/1/M) / 정책 문서가 알 수 없는 데이터 등급 이름을 그대로 저장해 오타 난 규칙이 조용히 아무것도 매치하지 않음 (3/1/S) / 컴파일된 규칙에 사유가 없어 Pod에서 거절된 호출이 운영자가 쓴 이유를 전하지 못함 (3/1/S) / 이름으로 승인 게이트가 걸린 호출은 스캔 전에 승인 요청이 나가 원문 값이 승인 테이블에 복사됨 (3/2/M)
relio
- 선택: 감사 이벤트가 그 이벤트를 만든 주소 때문에 통째로 사라지지 않게 하기 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
httpx.ClientIP은net.SplitHostPort가 돌려준 것을 그대로 반환했고, 이를 저장하는 6곳 전부가 그 문자열을NULLIF($n,'')::inet에 그대로 바인딩합니다. PostgreSQL 의inet은 zone 이 붙은 주소를 거절하므로 IPv6 link-local 로 접속한 클라이언트([fe80::1%eth0]:52000)는 ip 컬럼 하나를 잃는 것이 아니라 INSERT 문 전체가 실패했습니다 — 감사 이벤트는LOGIN_FAILED를 포함해 로그 한 줄만 남기고 사라지고,auth.Login은 같은 값으로 세션 행을 쓰므로 그 클라이언트는 아예 로그인할 수 없었습니다. 주소가 아닌 피어(Unix 소켓은@)도 같은 결과였습니다. 이제 zone 을 떼고, IPv4-mapped 형태(::ffff:192.0.2.10)를 unmap 해 dual-stack 리스너에서 한 클라이언트가 로그인 리미터 버킷과 감사 로그 표기를 하나로 유지하며, 주소가 아닌 값은""로 만들어 기존NULLIF가 NULL 로 쓰게 합니다(행을 함께 죽이지 않음). 같은 주제로audit.nullableJSON도 고쳤습니다 — json 이 거절하는 payload(NaN 으로 스캔된 numeric, 순환 참조)에서 오류를 버리고 nil 을 돌려주어 컬럼이 조용히 SQL NULL 이 되었고, 그 행은 “무엇이 바뀌었다”고만 말한 채 “어떻게” 를 잃어 payload 가 원래 없던 이벤트와 구분되지 않았습니다. 이제 인코딩 실패 자체를 기록합니다. 검증은 새 테스트 9개(두 파일,internal/audit의 첫 테스트)를 옛 구현으로 되돌리면 실제로 실패하는지 확인 — ClientIP 20건 실패(zone 4종, IPv4-mapped 2종, 주소 아님 5종, 계약 검사 6종), nullableJSON 5건 실패 — 한 뒤go test -race ./...,go vet ./...,gofmt -l, web typecheck·build,check-env-contract.sh,check-static-assets.sh,previous-release-tag-test.sh전체 통과. 커밋 d9ee0c8. - 보류 아이디어:
system.timezone설정을 읽는 Go 코드가 한 줄도 없어 관리자 화면의 선택이 순전히 장식이고 계약·견적 번호의 날짜까지 UTC 로 찍힘 (가치 4 / 위험 3 / M) · 네 intelligence 필터의Cursor필드는 어떤 질의도 쓰지 않고 어떤 핸들러도 채우지 않는 죽은 코드 — 상위 200건 뒤를 볼 방법이 없음 (가치 3 / 위험 1 / M) ·list_activities·get_contracts·list_quotations등 나머지 MCP 목록 도구는 페이징 자체가 없음 (가치 3 / 위험 2 / M) · 로컬 로그인이 꺼져 있을 때/auth/login이 403 과 401 을 갈라 돌려주는 두 번째 열거 통로 (가치 3 / 위험 2 / S) · CI 에gofmt -l검사 추가 — 지금은 포맷 위반이 CI 를 통과함 (가치 2 / 위험 1 / S)
Clustara
- 선택: 터미널 게이트가 읽는 명령 문자열과 executor 가 만드는 argv 의 파서 불일치 5종 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 하나의 명령 문자열을 두 파서가 읽는다 — 게이트(Command Risk Parser·터미널 denylist·access mode 분류기)와 Kubernetes exec argv 를 조립하는 executor. 게이트는 원본 바이트를 보면서 토큰의 바깥쪽 따옴표만 떼어냈고 executor 는 따옴표·백슬래시를 제대로 해석해 실제 프로그램을 실행했으므로, 셸에서 흔한 표기 하나로 모든 게이트를 동시에 통과했다 — ①
r"m" -rf /·\rm -rf /·re"boot"·shut'down' -h now·mkfs".ext4" /dev/sda가 전부critical이 아니라low로 채점됐다. critical 은 정책을 한 줄도 읽기 전에 걸리는 하드 블록(실행 핸들러에서 한 번 더)이고 low 는 승인이 필요 없는 read_only 티어라, 첫 토큰을 받아주는 allowlist 하나만 있으면 evaluateTerminalPolicy 가 루트 삭제에Allowed=true, RequireApproval=false, access_mode=read_only를 돌려줬다(신규 종단 테스트가 이 응답을 그대로 재현). ② denylist 도 같은 표기를 놓쳤다 —"rm -rf"는 원본 문자열 substring 검사인데r"m" -rf /data에는 그 부분문자열이 없다. 이제 deny 쪽만 executor 가 해석한 형태로도 대조한다(allow 쪽은 원본 유지 — 정규화하면 allowlist 를 더 쉽게 만족시키는 반대 방향이 된다). ③ full TTY 는 항상 승인을 강제하는 티어인데isInteractiveShell이 원본 Fields 를 써서"bash"·\bash·'sh'가 read_only 로 분류됐다. ④ executor 의 분해기 자체도 승인된 텍스트와 두 곳에서 어긋났다 — 명시적 빈 인자(sh -c "" ls)를 버려 뒤 인자가 한 칸씩 당겨지고(=sh -c ls실행), 작은따옴표 안의 백슬래시를 이스케이프로 처리해grep 'a\.b' f가a.b를 찾았다. ⑤podExecArgs가 호출자 argv 의 빈 요소를 버리고 각 요소를 trim 해서sh -c <script> <argv0> <path> <query> <n>의 위치 파라미터가 밀렸다(evidence search 경로가 이 형태). 따옴표 해석을analyzer.ShellWords한 곳에 POSIX 규칙으로 문서화하고 executor 분해기가 같은 규칙을 따르게 했다. 검증: 신규 테스트 8개(analyzer 5 + kube 2 + proxy 3, 정상 조회가 계속 low·미차단인지 지키는 오탐 회귀와 allow 쪽이 넓어지지 않았는지 지키는 테스트 포함)를 고치기 전 코드에 되돌려 붙여 각 결함을 지목하며 실패함을 확인했고(analyzer 16건·kube 5건·proxy 8건 실패),go build ./...·go vet ./...·go test ./...전부 통과. 이번 세션 규칙에 따라 버전·changelog·docs 마커는 건드리지 않았다. -
보류 아이디어: ①
.github에 CI 워크플로 없음 — build/vet/test 게이트 추가 (가치 3 / 위험 1 / S) ② PSS Restricted 검사에 seccompProfile 항목이 없어RuntimeDefault/Localhost미설정 Pod 가 Restricted 로 남음 (가치 3 / 위험 2 / S) ③ 취약점 import 가 파싱 못 한 아티팩트를 ‘취약점 0건 완료’ 로 저장 — 400 거절 또는parse_failed상태 재검토 (가치 3 / 위험 3 / S) ④capacity.podRequestGPU는 nvidia 만,node_monitoring.podGPURequests는 amd·intel 도 세어 화면마다 GPU 요청량이 다름 (가치 2 / 위험 1 / S) ⑤podControllerOwned가 라벨 휴리스틱만 봐서ownerReferences로만 소유된 Pod 를 standalone 으로 판정 (가치 2 / 위험 1 / S) - 릴리즈: v0.9.275 (2026-09-08, run 2026-09-08-103054-Clustara-improve)
Invenqor
- 선택:
/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)
ReSSO
- 선택: OIDC 엔드포인트들이 이쪽 장애를 “그런 Realm은 없다”로 답하던 문제 수정 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공 (커밋 0f2b4c8)
- 요약: 모든 OIDC 엔드포인트는 같은 조회 — 경로에 적힌 Realm — 로 시작하는데, 그 중 다섯이 “조회를 완료하지 못한 것”을 “Realm이 없는 것”과 같이 답했다. 그리고 그 답들은 전부 호출자가 보여주는 것이 아니라 실행하는 것이다. 404
realm_not_found는 RP에게 “설정된 issuer가 존재하지 않는다”는 말이라 라이브러리는 장애가 아니라 설정 오류로 읽고 캐시하기도 하는데, discovery와 JWKS는 RP가 무엇을 하기도 전에 먼저 가져오는 두 문서다. revoke는 더 나빴다 — RFC 7009이 “일치하는 Token이 없다”에 쓰라고 정해둔 200을 답해서, 유출된 Token을 폐기하러 온 사람에게 “그건 이미 죽었다”고 말하면서 Token은 그대로 살아 있었다. 근거는 같은 핸들러 안에 이미 있었다: 그 아래 모든 폐기 실패는 503 +TOKEN_REVOKEDFAILURE로 답하는데(docs/operations.md가 그렇게 적고 있다), 이 조회가 그 전부보다 앞에서 돌아 장애가 먼저 닿았고 뒤쪽 배려는 실행되지 않았다 — 게다가 감사 기록은 Realm을 해결한 뒤에 쓰이므로 트레일에는 아무것도 남지 않아 로그인 안 한 브라우저의 호출과 구별되지 않았다. 이제store.ErrNotFound만 옛 답을 유지하고(없거나 꺼진 Realm은 여전히 404, revoke는 200), 나머지는 500internal_error(revoke만 503 + FAILURE 기록) + 어느 엔드포인트가 만났는지 로그다. revoke 기록은 Client가 아니라 경로의 Realm 이름을 대상으로 남긴다 — 그 시점엔 Realm이 없어 Client를 인증할 수 없기 때문이고, 인증되지 않은 폼 값을 행위자로 적으면 트레일이 오염된다.token엔드포인트만 손대지 않았다: 같은 뭉개기(400invalid_grant)를 하지만 병합 대기 중인 브랜치 7753752가 이미 고쳤고 여기서 또 고치면 충돌만 만든다. 검증: 새 연동 테스트가realms를 RENAME으로 숨기고 다섯 엔드포인트를 모두 호출해, 수정 전 코드에서 여섯 건 모두 실제로 실패함을 확인했다(404 넷, revoke 200, FAILURE 감사 기록 0건). 정상 상태의 답 다섯 개와 테이블 복구 후 회복도 같은 테스트가 고정한다.make test전체 통과(exit 0) —go test -race ./...전 패키지 ok(httpserver 84s / store 80s), 연동 테스트 SKIP 0건,go vet,golangci-lint(0 issues),govulncheck(0),npm run lint,npm run test,npm run build. 빌드가 만든webui/dist/index.html변경은 되돌렸다.docs/operations.md의TOKEN_REVOKED항목에resolve the realm:상세의 읽는 법을,docs/compatibility.md의 Discovery 행에 404/500/503 구분을 적었다. -
보류 아이디어:
authorization의ClientByIdentifier실패가 400invalid_request“unknown client_id”로 나가 RP에 설정 오류로 보인다 — 이번에 고친 바로 윗줄이다(가치 4/위험 2/M) /authorization이id_token_hint·max_age를 저장하지 않아 로그인 폼을 거치면 hint가 지목한 계정과 다른 계정으로도 코드가 나간다(가치 3/위험 2/M) /authChallenge가 없는 요청에 404를 답해 같은 원인에 login의 400expired_request와 화면 문구가 갈린다(가치 2/위험 1/S) / UserInfo POST에서 form-encodedaccess_token수용, RFC 6750 §2.2(가치 2/위험 1/S) /oidcCORS가realmFromPath실패에 조용히 CORS 헤더를 빼고 지나가 브라우저 RP에는 원인 없는 CORS 오류로 보인다(가치 2/위험 1/S) - 릴리즈: v0.9.71 (2026-09-08, run 2026-09-08-123054-ReSSO-improve)
Vendra
- 선택: 공급업체에 닿는 두 값(전화번호·웹사이트)을 그 값이게 하고, 폼과 API가 서로 다른 모양을 요구하던 것을 하나로 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공 (commit eef34dc)
- 요약: 날짜·숫자·라벨·어휘·레코드id·이메일 스윕과 같은 기준(철자가 아니라 연산 — 요청 값이 phone/website 컬럼에 도달)으로 훑었다. 두 값은 라벨을 재는 목록에 들어 있었고 그게 전부였는데, 이 상자에 잘못 들어가는 값은 짧다 — “내선 3번”이 걸어야 할 번호로, 사내 위키 제목이 회사 홈페이지로, 다섯 문(등록·수정·포털 프로필·담당자 두 문) 모두에서 저장됐다. 빈 칸은 아무도 안 채웠다는 뜻이지만 문장이 들어 있으면 누가 채웠다는 뜻이고, 전화를 걸어야 할 때까지 아무도 모른다. 나쁜 쪽은 두 번째다: 구매자 폼은 아무 텍스트나 받아 등록부가 “www.acme.co.kr”(명함에 인쇄된 모양)로 가득한데, 포털의 같은 컬럼 폼은 type=”url”이라 브라우저가 스킴을 요구했다. 전화번호를 고치러 「회사 연락정보 수정」을 연 공급업체는 자기가 건드린 적 없는, 구매자가 써 넣은 웹사이트 칸에서 막혔고 — 남의 값을 고치는 것 말고는 길이 없었으며 — 게다가 submit 핸들러에 catch가 없어 API 거절은 핸들러 밖으로 던져지고 모달은 아무 말 없이 그대로 떠 있었다. rows.go에 validPhoneFields·validWebsiteFields를 기존 여섯 검사 옆에 두고 다섯 문 전부에 세웠다. 번호는 국가별 형식이 아니라 걸 수 있는 자릿수로 본다(해외 업체를 등록하는 제품이라 02-1234-5678도 +82 2 1234 5678도 1588-0000도 같은 것이다). 웹사이트는 사람들이 쓰는 대로의 호스트를 받아 스킴을 붙여 저장하므로 컬럼이 한 모양이 되고 두 폼이 같은 것을 제안한다. 검증: docker postgres:16-alpine으로 CI와 동일한 세 DSN을 새 DB에 걸고
go test ./internal/... ./cmd/...전체 통과, gofmt·go vet 무결, 웹 테스트(13파일 54개)·tsc·eslint·빌드 통과. 가드는 넷 — 패키지를 파싱해 요청 필드명이 아니라 statement의 컬럼 목록으로 문을 찾는 TestEveryStoredContactDetailIsOne, 거절이 상자를 지목하고 저장 모양이 하나임을 고정하는 TestARejectedContactDetailNamesTheBox, 다섯 문을 API로 걷고 포털이 막혀 있던 그 전화번호 수정까지 해보는 TestEveryContactDetailOnAWriteIsOne, 사내 호스트가 들어 있는 모달을 실제로 렌더링해 브라우저의 checkValidity를 읽는 portal-profile.test.tsx. portalUpdateProfile의 가드와 폼의 type=”url”을 각각 되돌리면 해당 테스트가 모두 실패하는 것까지 확인했다. -
보류 아이디어: 업무 객체 status는 여전히 임의 문자열 — 유형별 어휘가 어디에도 정의돼 있지 않아 오타 하나가 집계에서 조용히 빠짐 (3/3/M) / 통합 테스트가 하나의 DB를 공유해 순서에 따라 산발적으로 깨짐 — 초안 축출은 ORDER BY updated_at DESC의 동률에서 방금 저장한 키를 버릴 수 있음 (3/1/M) / 업무 객체의 data jsonb 블롭에 어떤 검증도 없음 (3/2/M) / 공급업체 수정(PATCH)은 tradingSince·annualSpend·businessNumber를 갱신하지 않아 오타난 사업자번호를 영영 고칠 수 없음 (3/2/S) / CI의 go job이 ./cmd/…를 빼놓아 Makefile·README와 불일치 (2/1/S)
- 릴리즈: v0.7.46 (2026-09-08, run 2026-09-08-143053-Vendra-improve)
ai-admin
- 선택: 확인하지 못한 자격 증명을 세션 만료로 보고하던 인증 미들웨어 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
auth.Authenticate가 session·API key 조회 실패를 자격 증명 오류와 같은 값으로 반환하고 미들웨어가 모두 401unauthenticated로 답해, DB 장애가 나면 아직 유효한 session이 만료된 것으로 보여 모든 운영자가 로그인 화면으로 밀려나고 API key client는 키가 폐기된 것으로 읽었다(프런트엔드는 이미 401이 아닌 인증 오류에 “서비스에 연결할 수 없습니다”+재시도를 띄우도록 되어 있었지만 서버가 그 상태를 만들지 않았다). v1.2.10 로그인 분류·990c7fb의 DB 오류 분류와 같은 원칙으로ErrUnauthenticatedsentinel을 도입해 “확인해서 거부한” 경우만 401로, 조회 실패는Retry-After와 함께 503auth_unavailable로 답하고 MCP의WWW-Authenticatechallenge도 실제 거부에만 붙게 했다. 연결 불가 pool을 향한 요청의 401/503 분기와, 정상 DB에서 폐기 session·비활성 계정·없는 key는 401이고 session·role table이 안 읽힐 때만 503인지를 통합 테스트로 검증했다(수정 전 코드에서는 모두 401로 실패). Docker PostgreSQL 16에TEST_POSTGRES_DSN을 걸어go test -race -count=1 ./...를 통과시켰고gofmt -l·go vet ./...·go build ./...·scripts/verify-version.sh·npm ci && npm test(62개)·npm run build도 통과했다. 이번 세션 규칙대로 버전·CHANGELOG는 건드리지 않았고docs/api.md에만 401/503 계약을 명시했다. - 보류 아이디어: CI에 정적 분석 단계(
gofmt -l,go vet) 추가 — eslint는 설정 자체가 없어 축소 범위 권장 (가치 3 / 위험 1 / M) ·loadGrants가 map 순회로 roles·permissions 순서를 무작위화해/api/v1/auth/me응답 순서가 요청마다 뒤바뀜 (가치 2 / 위험 1 / S) ·safeCSVCell이 OWASP가 함께 권고하는 tab(0x09)·CR(0x0D) 선행 문자를 중화하지 않음 (가치 2 / 위험 1 / S) ·listUsers의q에만 길이 상한이 없어 매우 긴 검색어가 세 컬럼 ILIKE 스캔으로 들어감 (가치 2 / 위험 1 / S) ·decideApproval이approval_action.comment에는 trim한 값을,approval_request.decision_comment에는 원문을 저장해 같은 결정의 두 기록이 달라짐 (가치 2 / 위험 1 / S)
aiportal-front-admin
- 선택: 자동 갱신이 운영 현황 표를 주기마다 비우는 문제 수정 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
OperationsView는 자동 갱신을 켜면pollIntervalMs(운영 기본 30초)마다load()를 부르는데,loadingref 하나로 표 전체를LoadingBlockskeleton으로 갈아끼워 사용자가 보던 POD 목록과 스크롤 위치가 30초마다 사라졌다(같은 이유로 수동 새로고침도 표를 비웠다). 결과가 아직 없는 첫 조회에서만 skeleton을 보여주는initialLoading과, 이미 결과가 있을 때만 참인refreshing으로 나눠 이후 갱신은 이전 결과를 유지한 채 헤더의 “갱신 중…” 표시로만 알리도록 했다(실패해도 이전 snapshot이 남으므로 기존 오류 배너 동작은 그대로다). 테스트가 없던 화면이라OperationsView.test.ts를 새로 만들어 첫 조회 skeleton·자동 갱신 중 목록 유지·수동 새로고침·unmount 시 타이머 정리 4개를 덮었고, 검증은npm run build(typecheck + vitest 87개 통과, 기존 83개 + 신규 4개 → vite build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 수정을 되돌려 신규 테스트 2개가 실제로 실패하는지 직접 확인했다. 커밋 e7b7527. - 보류 아이디어:
monitoringParser.toPod이 kubectl 스타일restarts(“3 (5d ago)”)에서 NaN을 표에 노출 (2/1/S) · CatalogView·DirectoryView·ContentAccessView가 두 탭이 공유하는loading·error를 써서 느린 탭의 응답이 다른 탭의 skeleton을 풀어버림 (2/2/S) · PaginationBar가 범위 밖 페이지에서 “31–25 / 25”를 표시하고 이전 버튼이move()가드에 막힘 (2/2/S) · AdvancedPolicyView가snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · 루트 앱의crypto-jslocal tarball 의존성 제거로 clean install 복구 (4/4/M)
aiportal-front
- 선택: OCR 사용 횟수 초기화 누락 및 상태 스토어 정합성 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 미테스트 상태였던
src/storage/ocrStatusCheckStore.js에서 8건을 고쳤다 — (1)checkOcrStatus가if (todayOcrList.length)가드 때문에 오늘 목록이 비면useCount갱신을 건너뛰어 어제 저장된 사용 횟수가 localStorage 에 그대로 남고 다음 날 첫 변환이 “동시 OCR 변환 사용 횟수를 초과했습니다” 로 막히던 문제(항상 재계산 + 저장 값에savedDate를 남겨 날짜가 바뀌면 로드 시점에 0 으로 복구), (2)loadStatus()가 스토어 객체를 통째로 교체하면서completedItems키를 빠뜨려 Header/Home 마운트 때마다undefined가 되고 SupportOcrList·SupportOcrListCurrent 의 watch 대상이 사라지던 문제, (3)getMaxUseOcr가res?.data?.body ?? 5로 비숫자 응답(객체 등)을 그대로 반환해useCount < NaN이 항상 false 가 되어 OCR 이 영구히 막히던 문제, (4)canUseOcr가 로그인 정보 확인 전에 최대 횟수 API 를 호출하던 문제, (5)startPolling(0)이 간격 0 의 setInterval 을 만들어 사실상 무한 루프가 되던 문제(최소 1초 하한 — 옛 코드로 테스트를 돌리면 실제로 힙 OOM 발생), (6)calculateUseCount가group_id없는 항목들을 한 그룹으로 묶어 여러 요청을 1회로 세던 문제, (7)checkOcrStatus가 API 실패 시 문서와 달리undefined를 반환하던 문제, (8)todayOcrList/저장 JSON 이 배열·객체가 아닐 때의 방어. 순수 헬퍼(todayKey,toCount,normalizeStoredStatus,calculateUseCount,extractCompletedItems)로 분리하고 테스트 43건을 추가했다(수정 전 코드에서 12건 실패 + 폴링 테스트는 OOM 으로 크래시함을 확인). 검증은npm test(총 245건 통과)와npm run build:dev(빌드 성공)로 수행했다. - 보류 아이디어:
mitt이eventBus.js에서 import 되는데 package.json 직접 의존성에 없어 전이 의존성에 기대는 문제 (가치 3 / 위험 1 / S) ·authStorage.readAuth가 localStorage 폴백 시 sessionStorage 를 복구하지 않고clearAuth도 통지가 없어 userStorage 와 동작이 어긋나는 문제 (가치 3 / 위험 2 / S) · Header.vue 가 OCR 사용 여부와 무관하게 마운트 즉시 3초 폴링을 시작해 statusOcr API 를 계속 호출하는 문제 (가치 3 / 위험 3 / M) ·globalLoading이 참조 카운트 없이 boolean 이라 병렬 요청 중 하나만 끝나도 스피너가 사라지는 문제 (가치 3 / 위험 3 / S) · 빌드 산출물 단일 청크 6.2MB 문제(manualChunks 코드 스플리팅) 개선 (가치 3 / 위험 3 / M)
aiportal-java
- 선택: 게시판·자료실 등록·수정·삭제에서도 조회수가 오르는 문제 수정 (가치 3 / 위험 2 / 작업량 S)
- 결과: 성공
- 요약:
BoardServiceImpl.get()(62행)과LibraryServiceImpl.get()(61행)이 상세 조회와view_cnt증가를 한 메소드에서 겸하고 있어서, 내부적으로 상세를 다시 읽는put()·modify()·delete()경로에서도 조회수가 올랐습니다 —modify()는 수정 전후로 상세를 두 번 읽으므로PUT한 번에 +2, 등록과 삭제도 각각 +1 이라, 아무도 읽지 않은 글이 작성·수정만으로 조회수 4 를 갖게 됩니다(게시판 조회수 통계가 편집이 잦은 글에서 계속 부풀려짐). 조회수 증가 여부를 인자로 받는 privatefind(id, countView)로 분리해 컨트롤러의 상세 조회 엔드포인트(get())에서만 증가시키고 나머지 내부 조회는 증가시키지 않도록 했으며, 없는 글에 대한BOARD_NOT_FOUND동작은 그대로 유지했습니다. 검증은BoardServiceImplViewCntTest·LibraryServiceImplViewCntTest(각 5: 상세 조회 시 정확히 1회 증가 / 없는 글은 예외 + 증가 없음 / 등록 증가 없음 / 수정 증가 없음(예전 +2 회귀 가드) / 삭제 증가 없음) 를 추가한 뒤sh gradlew check build실행으로 했고 26개 테스트 클래스 123개 테스트 전부 통과했습니다. 커밋 e10459a. (참고: 이 세션 환경에도 JDK 가 없고 JRE 만 있어 Temurin 21 을$HOME/jdks에 내려받아 빌드했습니다. 저장소에는 아무 변경도 하지 않았습니다. 또한 이 워크트리는 여전히 3ffa5ca 기준이라 직전 다섯 세션의 커밋이 아직 병합되지 않아, 해당 세션들이 건드린 파일은 충돌을 피하려고 이번 범위에서 제외했습니다.) - 보류 아이디어:
- 컨트롤러가
@RequestParam user_id를 인증 사용자와 대조 없이 신뢰 (IDOR) —ToolsController.preview는 매퍼에도 user_id 필터가 없어 id 증가만으로 남의 OCR 원본을 받을 수 있다 (가치 5 / 위험 4 / 작업량 L) SystemPolicyUtil.saveExtension/saveNote의!isEmpty()가드로 빈 배열 저장이 조용히 무시되는데 API 는 success 를 반환 —deleteExtension의 마지막 항목 삭제도 반영되지 않는다 (가치 3 / 위험 2 / 작업량 S)- 인증 필터의 요청 헤더 전량 INFO 로깅 축소 — 요청마다 모든 헤더를 남겨 운영 로그에서 실제 오류를 가린다 (가치 3 / 위험 2 / 작업량 S)
- 남은 외부 응답 무검증 접근에
JsonNodeUtil확대 적용 — 유틸을 만든 커밋 602dfc3 이 main 에 병합된 뒤 진행할 것 (가치 3 / 위험 2 / 작업량 M) ToolsServiceImpl.decFileDrm이DrmUtil을new로 직접 생성해 테스트 불가 — 무상태 객체이므로@Bean으로 주입하면 실패 처리를 단위 테스트할 수 있다 (가치 2 / 위험 2 / 작업량 S)
- 컨트롤러가
aiportal-py
- 선택: 검색 경로의 Milvus default alias 재사용 분리 (감사 A-007 후속) (가치 4 / 위험 3 / 작업량 M)
- 결과: 성공
- 요약:
util/search_module.py의hybrid_search가 진입할 때마다connections.disconnect("default")를 호출한 뒤, 컬렉션이COLLECTION_LIST_KAI에 있으면KAI_MILVUS_HOST, 아니면MILVUS_HOST로 같은defaultalias 를 다시 연결했다.default는util/milvus_collection.py·util/milvus_confluence.py·pipeline 이 공유하는 alias 이므로, 단일 프로세스에서 요청이 겹치면 (1) 무조건적인 진입 시disconnect가 다른 요청이 사용 중이던 연결을 끊고, (2) KAI 검색이default를 KAI 서버로 연결해 둔 사이 다른 요청이 기본 주소로 같은 alias 를 연결하면 pymilvus 가 주소 불일치로 실패하거나 앞선 요청이 엉뚱한 서버를 보게 된다. 설정·pymilvus 비의존 helperutil/milvus_connection.py를 추가해alias_for(host, port)가milvus_<주소>_<sha1 8자>형태의 결정적 alias 를 만들고(normalize_endpoint로 int19530과 str"19530"을 같은 alias 로 합치며, 해시 덕에 정규화 충돌이 없다),ensure_connection()이 그 alias 가 없을 때만 연결한 뒤 alias 를 돌려주도록 했다.search_module의 연결 지점 9곳을 전부 helper 로 바꾸고Collection(...)에using=alias를 명시했으며 진입 시disconnect는 제거했다.default를 그대로 쓰는milvus_collection.py등은 항상 기본 주소 하나만 보고 호출 직전에 스스로 연결하므로 동작이 바뀌지 않아 범위에서 제외했다. pymilvus 미설치라 helper 는 가짜connections로 단위 테스트(주소별 alias·default무접촉·재사용·두 서버 동시 연결 유지·빈 host 거부·실패 전파)하고,search_module은 AST 정적 검사(disconnect금지·default문자열 금지·using=누락 금지·helper import·KAI 주소 선택 유지)로 검증하는tests/unit/test_milvus_connection.py를 추가했다. 옛search_module.py를 되돌려 넣어 정적 검사 4건이 실제로 실패하는지 확인했다.python -m pytest875 passed(기존 848),python -m pyflakes .undefined name 0건. docs 4종(CURRENT_STATE_AUDIT/MILVUS_SEARCH/CODEBASE_MAP/TESTING) 갱신. 커밋5d67513. - 보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
- 보류 아이디어: A-003 후속 — 백그라운드 task 에
add_done_callback로그와/healthchecks 노출 — 가치 3 / 위험 2 / S - 보류 아이디어:
util/extract_minor.py의except Exception: return e정리(예외 객체가 Milvus 색인까지 흘러감) — 가치 3 / 위험 2 / S - 보류 아이디어:
util/milvus_collection.py·util/milvus_confluence.py·pipeline 의defaultalias 도ensure_connection으로 통일 — 가치 2 / 위험 2 / M - 보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용
requests.Session도입 — 가치 3 / 위험 3 / M
appstore
- 선택: 429 응답의 Retry-After를 현재 윈도우 잔여 초로 계산 (가치 2 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
apiPolicy와mcpRateLimit이 429에 항상Retry-After: 60(윈도우 전체 길이)을 실어, 헤더를 존중하는 클라이언트가 남은 윈도우에 더해 다음 윈도우의 거의 전부를 놀렸다 —fixedWindowLimiter.allow은 지난 윈도우에서 넘어온 bucket을 초기화하므로 거절은 항상 현재 분 안에서 일어나고, 한도는 보통 분이 끝나기 한참 전에 찬다. 새 순수 함수retryAfterSeconds가 남은 초를 올림해 돌려주도록 하고(광고한 시각이 리셋보다 앞서지 않도록) 두 미들웨어가 같은now로 한도 확인과 헤더 계산을 하게 했다.auth_handlers.go의 세 번째Retry-After: 60은 미머지 브랜치 ee70048이 바로 그 hunk를 건드리고 있어 이번에도 손대지 않았다(브라우저 로그인 폼만 읽는 경로라 영향도 없다). 함께internal/httpapi의 DB 없이 테스트 가능한 경로에 단위 테스트를 채웠다 — SPAHandler(캐시 헤더·content type·HEAD·404·traversal, Alpine 런타임에 /etc/mime.types가 없어도 woff2가 폰트로 나가는 것 포함)와 middleware(request id 필터·보안 헤더·panic 봉투·access log 상태/flush 유지). 검증:retryAfterSeconds경계 테이블 테스트와 “광고한 시간만큼 기다리면 반드시 통과한다”는 윈도우 전 구간 스윕 테스트를 추가한 뒤gofmt -l,go vet ./...,go test -race(CI와 같은 패키지 목록),go build ./cmd/server,check-env-contract.sh,check-docs.sh통과. 프런트엔드는 변경하지 않아 npm 검사는 생략. 커밋 9d81c75, f7aaeac. - 보류 아이디어:
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 bucket을 공유 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) · 프런트엔드streamAiChat이 스트림 종료 시 잔여 버퍼·TextDecoder를 플러시하지 않고 chunk 경계 CRLF를 놓침(가치 2/위험 2/S) ·brandingURL 가져오기(fetchBrandingSource)의 오류 메시지에 upstream 상태 코드와 err 원문이 새어 나감(가치 2/위험 2/S) ·auth_handlers.go의 남은Retry-After: 60— ee70048 머지 후 정리(가치 1/위험 1/S) · 앱screenshotsURL에 scheme 검증이 없음 — 현재 프런트엔드가 렌더링하지 않아 실제 표면 없음(가치 1/위험 2/S)
dataworks
- 선택: 엔타이틀먼트 만료 임박 경고 추가 + action center 만료 예정 기준(30일 고정)을
expiring_within쿼리 파라미터로 노출 (가치 4 / 위험 1 / 작업량 M) - 결과: 성공
- 요약:
GET /admin/dataworks/action-center는 계약(Contract Scope)에만 30일 고정 예고를 주고 API Entitlement 에는 예고가 전혀 없어, 고객 API 키는 만료일에 그냥 죽고 화면에는 그 뒤에야inactive_access로 나타났다(운영자가 갱신을 준비할 신호가 0). 활성·미만료 엔타이틀먼트가 창 안에 만료되면expiring_access/entitlement_expiring로 보고하도록 하고, 분기 단위 갱신 주기를 위해 창을?expiring_within=13w|45d|72h로 지정할 수 있게 했다(응답에 적용된 창을expiring_within으로 반환). 해석 불가·비양수 값은 기본값으로 조용히 되돌리면 운영자가 물어본 창과 다른 답을 주게 되므로400 invalid_expiring_within으로 거부한다. 내장 admin UI 는 새 액션을 기존 “접근 만료” 탭에 합산·필터하고 두 접근 액션에 만료일을 표시하며, workbench 검토 화면의 summary→action type 매핑이 5개만 있어 나머지 카운터 버튼이 빈 목록을 보여주던 것도 같이 채웠다. 검증: HTTP 회귀 테스트(기본 30일에서 20일 뒤 만료 엔타이틀먼트 1건 감지·200일짜리는 미감지,13w로 60일 계약까지 감지,7d로 미감지)와 잘못된 파라미터 400 테스트,parseExpiryHorizon표 테스트를 추가하고 옛 동작(경고 없음 / 창 하드코딩)으로 되돌려 각각 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과,go run ./cmd/api-surface-auditgap 0, 수정·신규 Go 파일gofmt -l클린, web 에서npm ci && npm run lint && npm test && npm run build전부 통과(vitest 14건).docs/OPERATIONS.md5절과 README 에 문서화. 릴리즈 커밋은 남기지 않았다. -
보류 아이디어: Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /
npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결(이번 회차에도 재현·수동 복원) / Publish Gate 의maskingConfigured가 계약이 하나도 없으면 true 로 폴백(미병합 브랜치 bf27f7d 와 같은 영역이라 병합 후 처리) / 엔타이틀먼트 선택이 계약(Contract Scope) 활성 여부까지 보도록 확장 /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 - 릴리즈: v0.9.45 (2026-09-08, run 2026-09-08-171056-dataworks-improve)
git-ctx
- 선택: Gradle 매니페스트 파서가 통째로 버리던 선언 형태들을 읽도록 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공 (commit 214bf01)
- 요약:
internal/manifest/manifest.go의parseGradle은 열거된 7개 configuration에 인용된group:artifact:version문자열이 붙은 한 가지 형태만 매치해서, 실제 빌드 스크립트의 대부분이 의존성 인벤토리에 도달하지 못했습니다. 직접 확인한 8줄짜리 스크립트에서 1줄만 파싱됐습니다 — 옛 Groovy 빌드가 쓰는 맵 표기(implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.14.1')가 통째로 사라졌고, Android·Kotlin 플러그인이 소스셋·빌드 변형마다 생성하는 configuration(androidTestImplementation,debugImplementation,kapt,ksp,testRuntimeOnly,compileOnlyApi, 레거시testCompile)도 전부 빠졌습니다. 그렇게 빠진 저장소는find-dependency-usage의 답에서 그 라이브러리를 안 쓰는 저장소와 구별되지 않아, log4j 같은 권고가 무사 통보로 읽힙니다. configuration을 변형이 생성되는 방식 그대로 base 이름의 접미사로 매치하게 하고, 좌표를 shorthand 문자열 또는 group/name/version 맵 양쪽에서 읽습니다. 맵 분기는 group과 name을 둘 다 요구해서 후행 클로저의exclude group:…, module:…이 의존성으로 들어오지 않게 했습니다(POM 파서가 exclusion을 거르는 것과 같은 이유).project(':shared')·platform(...)·버전 카탈로그 접근자는 종전대로 좌표를 내지 않아 제외됩니다. 덤으로 보간된 버전($log4jVersion)은 변수 이름을 버전으로 기록해 저장소마다 별도 버전 그룹을 만들고 아무것도 판정하지 못했으므로, 다른 파서가 락파일에 번호를 미룰 때와 같이 빈 버전으로 남깁니다. 검증: 새 테스트 2건(TestGradleReadsEveryDeclarationShape8개 선언 전부의 이름·버전·scope,TestGradleLeavesOutWhatIsNotADeclaredLibrary플러그인·리포지터리·project·platform·카탈로그·exclusion 제외 및 보간 버전) 추가 — 수정 전에는 8건 중 1건만 파싱됨을 직접 확인.gofmt -l,go vet ./...,go build -tags sqlite_fts5 ./...,go test -tags sqlite_fts5 ./...전체 통과(exit 0), manifest·search·indexer는-race도 통과. - 보류 아이디어: (1)
parseTOMLDependencies가[dependencies.serde]·[workspace.dependencies]같은 하위 테이블 섹션을 못 읽어 Cargo 워크스페이스의 버전 핀이 통째로 빠짐 (가치 3 / 위험 2 / M). (2)parseRequirements에!=·===가 없어urllib3!=1.25.0의 버전이"!"로 기록됨 (가치 2 / 위험 1 / S). (3)clampResponse의 truncation notice 예약치 320B보다 실제 공지가 길어 예산을 십수 바이트 초과 (가치 2 / 위험 2 / S). (4) 인덱스 작업의warning이 매니페스트 경고를 앞 3건만 join하고candidates == 0일 때 warning 전체를 덮어씀 (가치 2 / 위험 1 / S). (5)parsePOM이<dependencyManagement>선언까지 실제 의존성으로 집계 (가치 3 / 위험 3 / M).
igame
- (원장 항목에 비밀/내부 정보 의심 문자열이 있어 비공개 기록으로 옮김 — run 2026-09-08-202131-igame-improve)
jupiq
- 선택: internal/secure의 문자열 암복호화·파생키·토큰 생성 테스트 공백 보강 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
EncryptString·DecryptString·Derive·RandomToken·HashToken은 OIDC state 쿠키, 허브 자격증명, JWT 서명키, API 키 해시가 모두 의존하는데 테스트가 라운드트립 하나뿐이었다. 잘못된 base64·표준 패딩·nonce보다 짧은 blob·변조·절단 ciphertext를DecryptString이 모두 거부하고 오류 시 빈 문자열을 돌려주는지,Derive가 label과 키로 분리되며 결정적인지,RandomToken이 요청한 바이트 수를 그대로 디코딩하고 재사용되지 않는지 덮는 테스트 6개를 추가해 커버리지가 39.5%→87.5%로 올랐다. 겸사겸사RandomToken이 크기 0 이하에 빈 문자열을 조용히 돌려주던 계약을 오류로 바꿔(현재 호출자는 모두 상수라 동작 변화 없음) 나중에 크기를 계산해 넘기는 호출자가 빈 state·nonce를 비밀값으로 쓰지 못하게 했다. 1바이트 토큰 유일성 검사는 256개 값에서 16회 추출 시 ~37% 확률로 충돌해 플레이키하므로 12바이트 이상에만 적용했고,-count=20으로 반복 확인했다.go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 59개) 모두 통과했다. -
보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(호출자 없음을 이번에도 재확인) /collectPrometheus가 metric마다featureEnabled로 features 설정을 다시 읽는 중복 조회 제거 /internal/config의Load()테스트 신설(현재ParseEncryptionKey만 덮여 있고 필수 환경변수 누락 집계·비밀번호 최소 길이·오류 우선순위는 미검증) / 로그인 리미터succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 - 릴리즈: v1.4.8 (2026-09-08, run 2026-09-08-202139-jupiq-improve)
jikim
- 선택: OpenBao Transit batch_input/batch_results 지원 추가 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
/v1/transit/{encrypt,decrypt}/{key}가 단건만 처리해baoCLI와 OpenBao SDK가 흔히 쓰는 batch 호출을 사용할 수 없었습니다.batch_input이 있을 때만 동작하는 추가 경로로 구현해 기존 단건 계약은 그대로 두고, 입력 순서를 유지한batch_results·reference반향·항목별error·전부 실패 시에만 400이라는 OpenBao 의미를 맞췄으며, 미구현 파라미터(context,nonce,associated_data,key_version)는 조용히 무시하지 않고 항목 오류로 처리했습니다. batch decrypt는 감사 기록에 실패하면 평문을 전부 withhold하는 기존 보안 불변식을 유지합니다. 검증은 순서·항목 오류·빈 배열·전부 실패·감사 실패·권한 거부를 덮는 단위 테스트 5개(hook 기반, DB 불필요)를 추가하고./scripts/verify.sh전체(Go test·vet·gofmt, React test·lint·build, docs, compose)를 통과시켜 확인했습니다. 커밋29788a6. -
보류 아이디어: 로그인 성공 판정 전에 rate limiter를 succeeded로 초기화하는 순서 정리 (2/1/S) / settings GET이 주입하는 파생 필드가 PUT 왕복 시 workflow 설정에 저장되는 문제 정리 (2/1/S) / 감사 로그 보존(audit_retention_days) 자동 정리 구현 (3/3/M) / requestedOpenBaoVersion이 음수 version 쿼리를 오류 대신 latest로 처리하는 동작 정리 (2/2/S) / 데드 코드(var _ = …) 제거와 관리 화면의 ‘v0.2.0 프리뷰’ 문구 최신화 (1/1/S)
- 릴리즈: v0.2.4 (2026-09-08, run 2026-09-08-202134-jikim-improve)
moyro
- 선택: 리마인더 전달이 중간에 끊기면 알림이 영구히 묶이는 문제 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
reminders워커는 전달 중delivered_at을 -1로 뒤집고 브로드캐스트가 끝난 뒤에야 실제 시각을 찍었는데, 그 사이를 회수하는 장치가 없었다.fire()가 워커 컨텍스트에서 파생되므로 종료 중 tick이면MarkDelivered가 취소로 실패하고,fire()안의 패닉은 recover가 한 단계 위tick()에 있어 배치의 나머지를 같은 방식으로 묶는다. -1로 남은 행은ClaimDue(delivered_at=0만 조회)·ListPending·Delete어디서도 닿지 않아, 사용자는 알림을 받지 못하고 그 사실을 알거나 지울 방법도 없었다. 예약 게시물이 000003에서 이미 해결한 문제인데 reminders 패키지 주석만 “같은 claim 규율”이라고 주장하고 있었다. 마이그레이션 000018로post_reminders에 claimed_at/lease_until/claim_token/attempt_count를 추가하고 이미 -1인 행에는 마이그레이션 시각 기준 회수 리스를 부여했으며,ClaimDue가 만료된 리스를 다시 집고MarkDelivered는 claim token 범위로 좁혀 리스가 끝난 워커가 후임의 행을 덮어쓰지 못하게 했다. 재시도는 3회에서 멈추고 그 뒤에는 종료 상태로 찍는다. 로컬 postgres:16-alpine으로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 회수 조건을 빼면 새 통합 테스트 3건이, 새 컬럼이 없으면 마이그레이션 업그레이드 테스트가 실제로 실패하는 것까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1)
userstatus.Get이 실제 DB 오류까지 삼키고 offline을 반환해 장애가 “전원 오프라인”으로 위장되는 문제 — 같은 패키지GetMany·tos.GetForUser는 이미 pgx.ErrNoRows만 분기하므로 그 형태를 따르면 된다. (2)getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 “설정 없음”으로 위장하는 문제. (3)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 쓰도록 허용. (4) 커스텀 상태가 write-only — 저장되지만 어떤 API로도 다시 읽히지 않음(작업량 L).
moina
- 선택: 미디어 응답을 1시간 캐시 대신 ETag 재검증으로 전환 (가치 3 / 위험 2 / 작업량 S)
- 결과: 성공
- 요약:
getMedia는 요청마다mediaAccessSQL로 차단 관계와 공개 범위를 검사하는데 응답에Cache-Control: private, max-age=3600을 붙여, 게시물을 비공개·팔로워 전용으로 돌리거나 상대를 차단한 뒤에도 이미 이미지를 받아 간 브라우저는 최대 한 시간 동안 캐시에서 그대로 볼 수 있었습니다(권한 검사 결과가 낡은 채 남음).private, no-cache로 바꿔 매 요청 서버 검사를 거치게 하고, 그 대신 저장할 때 계산해 둔 SHA-256을 강한ETag로 내려보내If-None-Match재검증이 본문 없이 304로 끝나게 했습니다(media ID의 본문은 절대 바뀌지 않으므로 강한 ETag가 안전하고, digest가 비었거나 hex가 아닌 예전 행은 ETag 없이 기존 Last-Modified 재검증만 씁니다).api/openapi.yaml의 media 조회 설명에 이 계약을 적었습니다. 검증은 새 단위 테스트 5케이스와 새 integration test 1개(200+ETag → 304 재검증 → 차단 후 같은 재검증이 404, 수정 전 코드에서 실패하는 것 확인) 포함 로컬 PostgreSQL(moina_ci)에MOINA_TEST_POSTGRES_DSN을 걸어go test -race ./...전체 통과,make fmt·make check·go vet·staticcheck 통과(frontend 무변경이라 ESLint·vitest 생략). - 보류 아이디어:
updatePost가 DB 오류를 409not_editable로 보고해 원인을 감춤(가치 2 / 위험 1 / S) ·safeFilename이 확장자와 판정한 MIME의 불일치를 그대로 둬 JPEG이photo.png로 저장·다운로드됨(가치 2 / 위험 2 / S) · HEIC 업로드 415 거절 안내가 형식 목록뿐이라 아이폰 사용자가 원인을 알기 어려움(가치 3 / 위험 2 / M) · Makefiletest가 CI와 달리-race미사용(가치 2 / 위험 1 / S)
kanpic
- 선택: 원격 탓으로 난 외부 호출 실패는 잠깐만 캐시에 담는다 (가치 3 / 위험 2 / 작업량 S)
- 결과: 성공
- 요약:
internal/external의 답 캐시가 성공과 실패를 가리지 않고cache_seconds(기본 5분, 최대 하루) 동안 담아, 배포 중의 500 이나 한 번의 시간 초과가 원격이 다시 멀쩡해진 뒤에도 하루 내내 답인 척 남을 수 있었다 — 허용 호스트와 크기 상한은 이미 캐시 바깥이거나 자리 이름에 들어 있어 관리자가 고치면 곧바로 통하는데, 정작 이쪽에서 고칠 수 없는 저쪽 사정만 가장 오래 남던 셈이다. 원격이 책임지는 실패(오류 상태·닿지 못함·응답을 읽지 못함)를 새remoteFailure로 표시해 최대 30초(failureCacheFor)만 담고, 이 응답의 성질이 정한 거절(크기 상한·리다이렉트·사설망)은 같은 응답이면 언제나 같은 답이라 예전대로 두었다. 아예 담지 않으면 시간 제한을 매 다시 계산마다 처음부터 다시 기다리므로 짧게 담는 쪽을 골랐다. 검증: 새 테스트TestRemoteFailuresAreKeptOnlyBriefly(고치기 전에 실제로 깨지는 것을 확인했다 — 두 갈래를 한 시험에서 갈라 본다),gofmt -l,go vet ./...,go build ./...,go test ./...(전체 통과)와go test -race ./internal/external/,scripts/check-release-docs.sh,scripts/check-commit-identities.sh. 커밋 1개(a981f8e). 서버 안쪽 캐시 수명이라 웹·문서·설정 화면은 손대지 않았고 npm 검사도 돌리지 않았다. -
보류 아이디어:
parseCSV가 쉼표·탭만 보아 세미콜론으로 가른 유럽식 CSV 를 한 열로 읽는다(internal/importexport의detectDelimiter는 이미;를 세므로 그 규칙에 맞출 것) /csvNumber자리(formula.DecimalNumber)가 IMPORTDATA 의"1,200"·"12%"·앞뒤 통화 기호를 글자로 남긴다 /DOLLARDE·DOLLARFR이math.Pow(10, ceil(log10(fraction)))로 자리를 밀어DOLLARDE(1.02,16)이 1.1250000000000002 다 /?의 자리 맞추기 빈칸을 그리지 않아# ??/??의 한 자리 분자가 자릿수를 맞추지 못한다 / IMPORTDATA 의 CSV 를 읽지 못했다는#VALUE!도cache_seconds만큼 담기는데, 이것은 원격이 보낸 본문의 성질이라 지금은 그대로 두었다 — 원격이 파일을 고쳤을 때의 회복 속도를 저울질할 것 - 릴리즈: v0.238.0 (2026-09-08, run 2026-09-08-210919-kanpic-improve)
pii-masker
- 선택:
/v1/history의limit상한 적용 (가치 3 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
handleHistory가?limit=을parsed > 0만 확인하고 그대로ListJobs에 넘겨서,?limit=100000요청 하나로 보존 기간(기본 24시간) 안의 모든 job 레코드를 복제·정렬·JSON 직렬화하게 만들 수 있었습니다(다른 경로에는 이미 동시 실행·본문 크기 상한이 걸려 있어 이곳만 무제한). 새historyLimit헬퍼가 기본 20 / 최대 100으로 클램프하고, 값이 없거나 파싱 불가·0·음수·int 범위 초과이면 기본값으로 돌아가며, UI가 쓰는?limit=8은 그대로 동작합니다.internal/httpapi첫 내부 패키지 단위 테스트(테이블 10케이스)와 job 150개를 디스크에 미리 심어 두고 5가지 쿼리(기본/5/100000/-1/all)의 반환 개수와 최신순 정렬을 확인하는 통합 테스트를 추가했고,gofmt -l(무출력)·go vet ./...·go build ./...·go test -count=1 ./...·go test -race -count=1 ./...전부 통과,-race -count=5로 플래키 여부 확인, 상한 코드를 임시로 제거해 새 테스트 2개가 실제로 실패(historyLimit("100000") = 100000/ 150건 반환)하는 것까지 확인했습니다. README의 엔드포인트 목록에도 기본값·상한을 적었습니다. -
보류 아이디어: 동기 슬롯 대기열의 메모리 상한(대기 중인 요청이 이미 읽은 업로드 바이트를 보유) / 종료 시 실행 중인 비동기 job이
running으로 남고 재기동 시failed처리될 뿐 재개되지 않음 /internal/config의 나머지 순수 함수(normalizeAllowHosts,normalizeEndpointURL,normalizePIILang/Schema,envInt/envNonNegativeInt/envBool) 단위 테스트 //v1/jobs/{id}/result가GET만 라우팅되어HEAD프로브가 405를 받음(ServeContent는 이미 HEAD 처리) / 다운로드Content-Disposition이 한글 파일명을 quoted-string에 원시 UTF-8로 넣어 RFC 6266의filename*=UTF-8''…가 없음 - 릴리즈: v1.0.15 (2026-09-08, run 2026-09-08-215056-pii-masker-improve)
releasedock
- 선택: 로그 다운로드가 어느 실행인지 알 수 없고, 중간에 끊겨도 완결된 것처럼 저장되는 문제 수정 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
downloadSimpleRunLog은original_filename·status를 조회해 놓고 쓰지 않아 브라우저가 저장하는 이름이releasedock-run-<uuid>.log뿐이었고, 실패한 업로드를 따라가며 여러 실행의 로그를 받으면 열어 보기 전에는 구분할 수 없었습니다. 더 나쁜 쪽은 본문이었습니다 — 200 과 헤더를 이미 보낸 뒤rows.Scan이 실패하면 조용히return하고rows.Err()는 아예 확인하지 않아, 연결이 결과셋 중간에 끊긴 경우 실행 중간에서 끝나는 로그가 아무 표시 없이 “전체 로그” 로 저장됐습니다. 배포가 실제로 실행됐는지 감사할 때 읽는 파일이 바로 이 파일입니다. 파일 이름을releasedock-<패키지>-<상태>-<실행 ID>.log로 만들되 허용 문자 밖은 전부 대시로 치환해 헤더 인젝션·경로 구분자가 들어갈 수 없게 하고(filename*=UTF-8''로 한글 패키지명은 원형 그대로 함께 실어 보냄), 렌더링을writeSimpleRunLog(io.Writer, logRowScanner)로 분리해Scan실패와rows.Err()양쪽에서[releasedock] 로그를 끝까지 읽지 못해 이 파일은 잘려 있습니다: <사유>를 마지막 줄에 붙입니다.logRowScanner인터페이스 덕에 DB 없이 잘림 경로를 시험할 수 있어 순수 단위 테스트 7건을simple_log_test.go에 추가했고(정상 렌더링, 행 읽기 실패, 결과셋 조기 종료, 이름 구성, 헤더 이스케이프 방지, 한글 이름 인코딩, 길이 제한), 로컬 도커 PostgreSQL 16 으로TEST_POSTGRES_DSN을 채워 backend/runnergo vet·go test ./...(통합 테스트 포함),npm ci,npm test -- --run(78건),npm run build를 모두 통과했습니다. docs/simple-mode.md 의 다운로드 설명을 갱신했습니다. VERSION 은 릴리즈 세션의 몫이라 건드리지 않았습니다. - 보류 아이디어: CI 와 Makefile 에
go vet(또는 golangci-lint) 단계가 없어 정적 검사가 매 세션 수동입니다 (가치 3 / 위험 1 / S). - 보류 아이디어: 실행 상세 화면에 같은 묶음(batchId)의 다른 실행 목록·링크를 표시하면 어느 패키지가 실패했는지 바로 찾을 수 있습니다 (가치 3 / 위험 1 / M).
- 보류 아이디어:
simpleRunLogger.append가 빈 payload 를 저장하지 않아 스크립트 출력의 빈 줄(문단 구분)이 로그에서 사라집니다 (가치 2 / 위험 1 / S). - 보류 아이디어: 실행 상세의 로그 조회가 2000줄 × 50쪽에서 조용히 멈춰, 그보다 긴 실행은 화면에 일부만 보이고 안내가 없습니다 (가치 2 / 위험 1 / S).
-
보류 아이디어:
web/dist/assets/vendor청크가 617KB 로 커서 폐쇄망 초기 로딩 최적화 여지가 있습니다 (가치 2 / 위험 3 / M). - 릴리즈: v0.5.10 (2026-09-08, run 2026-09-08-215106-releasedock-improve)
ptium
- 선택: 숨긴 시트를 그대로 가져와 설정·코드표가 슬라이드가 되는 문제 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
internal/docs/workbook.go의workbookIndex가xl/workbook.xml의sheet state를 아예 읽지 않아서, 통합 문서가 숨겨 둔 시트(수식이 참조하는 코드표, 매크로가 읽는 설정, 지난 분기 작업본)가 전부 슬라이드가 되고 보고서인 양 출처까지 달렸다 — 스크래치 스텁으로 “분기 실적 다음에 코드표 슬라이드, 경고 없음”을 먼저 재현했다.state를 읽어hidden/veryHidden(대소문자·공백 무시) 시트를 건너뛰고 어떤 시트를 빼놓았는지 경고로 이름을 말하게 했으며, 전부 숨김이면 “읽을 표가 없습니다”(숫자가 보이는 파일을 든 사람을 되돌려 보내는 말) 대신 시트가 모두 숨겨져 있다고 이름과 함께 말한다.sheethidden_test.go(통합 문서 5가지 + 전부 숨김 1가지 +sheetHidden8가지)를 추가하고make test(go test -race, go vet, tsc, vite build) 전부 통과. 커밋 75376f7. 버전·릴리스 노트는 이번 세션 범위가 아니라 손대지 않았다. -
보류 아이디어:
gridOf가 행의r을 보지 않고trimGrid이 빈 행을 지워서 빈 줄이 있는 시트의 출처 행 번호가 어긋나는 문제 (3/3/M) ·allNumeric이 통화 기호·괄호 음수(“₩1,200”, “(1,200)”)를 숫자로 보지 않아 금액 시트가 차트가 못 되는 문제 (3/3/M) · 맥 엑셀의 1904 날짜 체계(workbookPr date1904)를 무시해 날짜가 4년 이르게 읽히는 문제 (2/2/S) ·writeSheet가 첫 행을 무조건 머리글로 삼아 A1 에 제목 한 칸만 있는 시트가 어긋나는 문제 (2/3/M) · 유럽식 Excel 의;구분 CSV 를 한 열로 읽는 문제 (2/3/S) - 릴리즈: v1.69.28 (2026-09-08, run 2026-09-08-215101-ptium-improve)
umm
- 선택: 이름 지은 사람이 남의 내려받기 이름을 정하던 헤더 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약: Markdown 내보내기는 백업이고, 그 파일의 이름은 공간의 이름 — 즉 사람이 쓴 말 — 에서 나오는데
filename="umm-%s.md"한 줄에 그대로 들어갔습니다."는 인용부호를 먼저 닫아 나머지를 헤더 문법으로 만들고(mime.ParseMediaType이invalid media parameter로 거절 — 공유 공간이면 이름을 지은 사람이 남의 백업 파일 이름을 흔듭니다), 한글은 RFC 6266 없이 원시 바이트로 나가 클라이언트마다 다른 인코딩으로 짐작했습니다. 웹 캔버스는anchor.download로 스스로 이름을 붙이므로 이것만이 유일한 이름인 쪽 — 백업 스크립트·curl -OJ같은 API 클라이언트 — 에만 보이던 결함입니다.attachmentDisposition을 새로 두어filename*=UTF-8''로 진짜 이름을 퍼센트 인코딩해 싣고 따옴표 안filename은 ASCII 대체 이름으로 남겼으며(못 쓰는 글자의 연속은 대시 하나로 줄여 말이 있던 자리를 보이게), 지우는 글자는store.safeFilename과 같게(한 화면의 딱지가 디스크에 쓰이는 순간 경로가 되므로) 길이도 같은 120바이트를LimitUTF8Bytes로 글자 경계에서 끊게 했습니다. 검증: 새 시험 3개(단위 2 + 통합 1) 추가 후 옛 헤더 형식으로 되돌리면 통합 시험이the download name is not a readable header: mime: invalid media parameter로 실패함을 확인, 실제 PostgreSQL 17(도커umm-test-pg, DSNpostgres://umm:umm@127.0.0.1:15433/umm)에 대한go test -p 1 ./...전체 통과 ·go vet ./...· gofmt · tsc · oxlint/Prettier · i18n 994키 · vitest 156개 ·scripts/check-version.sh통과. 버전은 올리지 않았습니다(릴리스는 별도 세션). -
보류 아이디어: 내보내기 본문에
##로 시작하는 줄이 있으면 여전히 거기서 잘림 — 이스케이프 없이는 생각 경계와 구별 불가 / Markdown 백업에 그림이 담기지 않는 사실이 릴리스 노트에만 있고 내보내기 메뉴·user-guide·features.md 어디에도 없음 / 401·403이 오프라인 큐 전체를 세우는데 403은 한 변경에 대한 거부일 수 있음 /README.md배지·소개와docs/README.md허브의 버전이 실제와 어긋나고 check-version.sh가 그 자리를 보지 않음 /usableSections가 서로 다른 부에 같은 제목이 오는 제안을 막지 않아 같은 이름의 부가 두 번 열림 - 릴리즈: v0.71.3 (2026-09-08, run 2026-09-08-221101-umm-improve)
weekly
- 선택: 주 격자를 옮긴 주에 MCP 보고서 검색이 그 주의 보고서를 찾지 못하던 버그 수정 (가치 3 / 위험 2 / 작업량 S)
- 결과: 성공
- 요약:
weekly_reports_search의weekStart필터만 아직r.week_start정확일치로 남아 있었습니다(mcp.go:524). 주차 시작 요일을 바꾸면 격자만 옮겨지고 보고서는 그 자리에 남으므로 전환되는 한 주에 대해 이 도구는 빈 목록을 돌려줬고, 같은 도구 묶음의weekly_submission_overview는 v0.287.0 부터 겹침으로 세므로 AI 클라이언트가 한 응답 안에서 “이 주에 1명이 제출했습니다” 를 듣고 그 주의 보고서를 물어 아무것도 받지 못하는 자기모순이 났습니다 — 어느 쪽이 틀렸는지 가릴 단서가 payload 에 없고, 모델이 시도할 만한 복구(없는 보고서 채워 넣기)는weekIsFree가 거절합니다. 필터를weekCoveringDays겹침으로 바꿨고(세는 질의와 읽는 질의가 같은where를 쓰므로total도 목록과 같은 질문에 답합니다), 도구 스키마의weekStart설명과 docs/MCP.md 한 문단에 그 규칙을 적었습니다. 회귀 시험 1개(currentweekgrid_test.go, guards: mcpSearchReports/weekCoveringDays)는 두 도구를 나란히 불러 제출 인원과 검색 결과가 같은 주를 말하는지 보고, 아무도 쓰지 않은 다음 주가 여전히 비어 있는지도 봅니다. 그 과정에서 기존TestMCPSearchReturnsOnlyTheCallersOwnOrganisation이 인자를weekStart가 아니라week로 보내 주차 필터를 한 번도 걸어 보지 않았다는 것이 드러나 이름을 바로잡았습니다. 검증: 수정 전 질의로 되돌려 새 시험이 실패(이 주의 보고 [])하는 것을 확인 → 실제 DB(WEEKLY_TEST_POSTGRES_DSN)로go test ./...전체 통과(141s),go vet,gofmt, guard-check –changed(40개 모두 도달, 새 가드는 mcpSearchReports 76%/weekCoveringDays 100%), openapi·modal-close·version·paging 검사 통과. mutation-check –test 는 6건 중 5건을 새 시험이 잡았고 남은 1건(515행 조직 범위 분기!=→==)은 다른 시험이 잡아 exit 0 입니다. 프런트엔드는 손대지 않아 lint·build·test 는 돌리지 않았고, backup-check 는 로컬에 psql 이 없어 건너뛰었습니다(CI 에서 실행). 커밋은 mutation-check 가 돌지 않는 동안에만 했습니다. - 보류 아이디어: analyticsOrganizations 의 제출 CTE 는 날짜 범위, 기대건은 격자 주라 창 가장자리에서 어긋납니다 (2/2/S) · 상황판 편집이 workItemId 를 담지 않아 업무 링크를 조용히 끊습니다 (2/1/S) · listScheduleTasks 에 상한이 없어 최대 366일 × 부서 전원이 한 응답에 실립니다 (3/2/M) · 상황판 담당자 선택 목록이 report-inclusions 를 재사용해 그 밖의 담당자를 본인으로 보여 줍니다 (2/1/S) · 키워드 분석의 두 창(collectTerms)도
week_start BETWEEN이라 전환 주가 옆 창으로 넘어갑니다 (2/2/S)
← 대시보드 · Atom 피드 · 원본 데이터 runs.jsonl