자율 개선 일일 보고 — 2026-09-07
요약. 2026-09-07에 자율 개선 에이전트가 29개 프로젝트에서 65회차를 돌려 11건을 릴리즈하고 13건은 머지만 했으며 0건은 변경이 없었고 실패는 0건이다. 에이전트 시간 6시간 43분, 추정 비용 $135.58. 릴리즈: kanpic v0.237.0, pii-masker v1.0.14, releasedock v0.5.9, AgentHub v0.235.0, Clustara v0.9.274, Vendra v0.7.45, weekly v0.290.0, dataworks v0.9.43….
- 65회차
- 29프로젝트
- 11배포 준비 완료
- 0릴리즈 진행 중
- 16병합 완료
- 28검토 대기
- 8검증 실패
- 0변경 없음
- 2실행 오류
- $135.58비용
- 6시간 43분에이전트 시간
회차
| 시각 | 프로젝트 | 결과 |
|---|---|---|
| 00:25 | aiportal-py | 검토 대기 CI no-ci, PR open PR #9 |
| 00:36 | appstore | 검증 실패 verify failed: secrets in diff |
| 00:54 | dataworks | 병합 완료 merged PR #8, release blocked (secrets) |
| 01:11 | git-ctx | 검토 대기 guarded files, PR open PR #21 |
| 01:26 | igame | 검증 실패 verify failed: secrets in diff |
| 01:41 | jikim | 검토 대기 guarded files, PR open PR #16 |
| 01:59 | jupiq | 검토 대기 review held, PR open PR #5 |
| 02:17 | kanpic | 배포 준비 완료 merged PR #10, released v0.237.0 |
| 02:36 | moina | 병합 완료 merged PR #9, release blocked (secrets) |
| 02:51 | moyro | 검증 실패 verify failed: 실행한 검증 명령이 없음 — 정책(verify) 또는 자동 감지 필요 |
| 03:13 | muni | 검토 대기 review held, PR open PR #9 |
| 03:27 | pii-masker | 배포 준비 완료 merged PR #11, released v1.0.14 |
| 03:37 | ptium | 검증 실패 verify failed: secrets in diff |
| 03:54 | releasedock | 배포 준비 완료 merged PR #10, released v0.5.9 |
| 04:09 | relio | 검증 실패 verify failed: secrets in diff |
| 04:22 | umm | 검증 실패 verify failed: secrets in diff |
| 04:30 | vibe-coders | 실행 오류 error: fetch |
| 04:54 | visitflow | 병합 완료 merged PR #10, release blocked (secrets) |
| 05:52 | weekly | 병합 완료 merged PR #9, release blocked (secrets) |
| 06:24 | AgentHub | 배포 준비 완료 merged PR #12, released v0.235.0 |
| 07:07 | Clustara | 배포 준비 완료 merged PR #13, released v0.9.274, asset manifest failed |
| 07:15 | Invenqor | 검증 실패 verify failed: 실패한 검증: cargo test --quiet (exit 127) |
| 07:36 | ReSSO | 검토 대기 review held, PR open PR #10 |
| 08:04 | Vendra | 배포 준비 완료 merged PR #111, released v0.7.45 |
| 08:20 | ai-admin | 검토 대기 guarded files, PR open PR #11 |
| 08:45 | aiportal-front-admin | 병합 완료 merged PR #9, release skipped |
| 08:59 | aiportal-front | 병합 완료 merged PR #7, release skipped |
| 09:18 | aiportal-java | 검증 실패 verify failed: 실패한 검증: ./gradlew --quiet test (exit 126) |
| 09:46 | weekly | 배포 준비 완료 release-only, released v0.290.0 |
| 09:49 | moina | 병합 완료 release-only, release skipped |
| 09:51 | visitflow | 병합 완료 release-only, release skipped |
| 10:00 | dataworks | 배포 준비 완료 release-only, released v0.9.43 |
| 10:02 | jupiq | 병합 완료 release-only, release skipped |
| 10:07 | AgentHub | 검토 대기 approved PR #10 |
| 10:08 | AgentHub | 검토 대기 approved PR #11 |
| 10:10 | Quantoss | 병합 완료 merged PR #12 (approved), release skipped |
| 10:20 | AgentHub | 검토 대기 approved PR #10, rebase conflict |
| 10:22 | AgentHub | 검토 대기 approved PR #11, rebase conflict |
| 10:23 | Quantoss | 검토 대기 approved PR #13, rebase conflict |
| 10:25 | Quantoss | 병합 완료 merged PR #15 (approved), release skipped |
| 10:35 | ai-admin | 배포 준비 완료 merged PR #10 (approved), released v1.2.10 |
| 10:37 | aiportal-front | 병합 완료 merged PR #5 (approved), release skipped |
| 10:39 | aiportal-front | 병합 완료 merged PR #6 (approved), release skipped |
| 10:41 | aiportal-py | 병합 완료 merged PR #7 (approved), release skipped |
| 10:43 | aiportal-py | 병합 완료 merged PR #8 (approved), release skipped |
| 10:45 | relio | 검토 대기 approved PR #12 |
| 10:54 | aiportal-py | 병합 완료 merged PR #10, release skipped |
| 10:55 | AgentHub | 검토 대기 approved PR #10, rebase conflict |
| 10:56 | AgentHub | 검토 대기 approved PR #11, rebase conflict |
| 10:57 | Quantoss | 검토 대기 approved PR #13, rebase conflict |
| 10:58 | aiportal-py | 검토 대기 approved PR #9, rebase conflict |
| 10:59 | relio | 검토 대기 approved PR #12 |
| 11:10 | AgentHub | 검토 대기 approved PR #10, rebase conflict |
| 11:11 | AgentHub | 검토 대기 approved PR #11, rebase conflict |
| 11:13 | Quantoss | 검토 대기 approved PR #13, rebase conflict |
| 11:14 | aiportal-py | 검토 대기 approved PR #9, rebase conflict |
| 11:14 | relio | 검토 대기 approved PR #12 |
| 11:35 | appstore | 배포 준비 완료 merged PR #6, released v2.5.2 |
| 11:40 | AgentHub | 검토 대기 approved PR #10, rebase conflict |
| 11:41 | AgentHub | 검토 대기 approved PR #11, rebase conflict |
| 11:43 | Quantoss | 검토 대기 approved PR #13, rebase conflict |
| 11:44 | aiportal-py | 검토 대기 approved PR #9, rebase conflict |
| 11:44 | relio | 검토 대기 approved PR #12 |
| 12:01 | dataworks | 배포 준비 완료 merged PR #9, released v0.9.44 |
| 12:10 | (runner) | 실행 오류 daily cap reached: 회차 64 ≥ 60 |
비용·사용량
| 시각 | 프로젝트 | 단계 | 시간 | 턴 | 비용 | 토큰 입력/출력 | 종료 |
|---|---|---|---|---|---|---|---|
| 00:18 | aiportal-py | 개선 | 8분 | 49 | $2.81 | 2.4M / 35K | success |
| 00:20 | aiportal-py | review | 3분 | 14 | $0.99 | 605K / 11K | success |
| 00:36 | appstore | 개선 | 7분 | 51 | $2.54 | 2.5M / 24K | success |
| 00:46 | dataworks | 개선 | 6분 | 35 | $2.18 | 1.8M / 24K | success |
| 00:49 | dataworks | review | 3분 | 28 | $1.18 | 1.1M / 11K | success |
| 00:54 | dataworks | 릴리즈 | 5분 | 33 | $1.70 | 1.6M / 13K | success |
| 01:10 | git-ctx | 개선 | 10분 | 49 | $2.57 | 2.3M / 27K | success |
| 01:26 | igame | 개선 | 6분 | 58 | $3.36 | 3.7M / 24K | success |
| 01:41 | jikim | 개선 | 12분 | 74 | $5.93 | 6.8M / 43K | success |
| 01:55 | jupiq | 개선 | 5분 | 34 | $1.71 | 1.4M / 19K | success |
| 01:58 | jupiq | review | 4분 | 14 | $0.99 | 540K / 14K | success |
| 02:05 | kanpic | 개선 | 5분 | 30 | $1.67 | 1.3M / 17K | success |
| 02:07 | kanpic | review | 2분 | 12 | $0.74 | 437K / 8K | success |
| 02:15 | kanpic | 릴리즈 | 1분 | 19 | $0.60 | 402K / 5K | success |
| 02:24 | moina | 개선 | 4분 | 31 | $1.55 | 1.4M / 15K | success |
| 02:26 | moina | review | 2분 | 10 | $0.46 | 164K / 7K | success |
| 02:35 | moina | 릴리즈 | 3분 | 24 | $1.31 | 1.0M / 12K | success |
| 02:51 | moyro | 개선 | 12분 | 70 | $3.93 | 4.1M / 34K | success |
| 03:08 | muni | 개선 | 8분 | 86 | $3.93 | 4.7M / 30K | success |
| 03:13 | muni | review | 5분 | 32 | $1.64 | 1.2M / 20K | success |
| 03:23 | pii-masker | 개선 | 3분 | 21 | $1.18 | 840K / 11K | success |
| 03:25 | pii-masker | review | 2분 | 13 | $0.55 | 272K / 7K | success |
| 03:27 | pii-masker | 릴리즈 | 2분 | 19 | $0.61 | 421K / 5K | success |
| 03:37 | ptium | 개선 | 7분 | 43 | $2.90 | 2.8M / 26K | success |
| 03:46 | releasedock | 개선 | 6분 | 39 | $2.12 | 1.8M / 22K | success |
| 03:48 | releasedock | review | 2분 | 15 | $0.71 | 484K / 7K | success |
| 03:52 | releasedock | 릴리즈 | 3분 | 16 | $0.57 | 333K / 5K | success |
| 04:09 | relio | 개선 | 9분 | 47 | $3.40 | 2.9M / 36K | success |
| 04:22 | umm | 개선 | 12분 | 58 | $4.12 | 4.3M / 40K | success |
| 04:47 | visitflow | 개선 | 7분 | 52 | $2.84 | 3.0M / 23K | success |
| 04:51 | visitflow | review | 4분 | 17 | $0.95 | 670K / 10K | success |
| 04:54 | visitflow | 릴리즈 | 3분 | 18 | $0.73 | 494K / 7K | success |
| 05:24 | weekly | 개선 | 25분 | 86 | $6.47 | 8.0M / 47K | success |
| 05:33 | weekly | review | 8분 | 25 | $1.36 | 1.0M / 17K | success |
| 05:52 | weekly | 릴리즈 | 4분 | 38 | $1.91 | 1.8M / 15K | success |
| 06:09 | AgentHub | 개선 | 10분 | 78 | $5.29 | 6.5M / 36K | success |
| 06:13 | AgentHub | review | 4분 | 17 | $1.23 | 787K / 13K | success |
| 06:17 | AgentHub | 릴리즈 | 3분 | 25 | $0.96 | 802K / 6K | success |
| 06:56 | Clustara | 개선 | 28분 | 53 | $4.24 | 4.3M / 40K | success |
| 07:03 | Clustara | review | 6분 | 23 | $1.48 | 1.1M / 16K | success |
| 07:07 | Clustara | 릴리즈 | 4분 | 20 | $0.91 | 600K / 8K | success |
| 07:15 | Invenqor | 개선 | 5분 | 33 | $1.46 | 1.1M / 16K | success |
| 07:30 | ReSSO | 개선 | 10분 | 53 | $3.25 | 3.4M / 26K | success |
| 07:36 | ReSSO | review | 6분 | 30 | $1.53 | 1.3M / 17K | success |
| 07:57 | Vendra | 개선 | 17분 | 83 | $6.05 | 7.1M / 50K | success |
| 08:01 | Vendra | review | 4분 | 32 | $1.63 | 1.5M / 16K | success |
| 08:03 | Vendra | 릴리즈 | 1분 | 12 | $0.45 | 274K / 4K | success |
| 08:19 | ai-admin | 개선 | 9분 | 65 | $4.91 | 5.7M / 35K | success |
| 08:38 | aiportal-front-admin | 개선 | 7분 | 56 | $3.62 | 3.8M / 33K | success |
| 08:44 | aiportal-front-admin | review | 5분 | 16 | $1.09 | 628K / 14K | success |
| 08:45 | aiportal-front-admin | 릴리즈 | 1분 | 12 | $0.46 | 251K / 4K | success |
| 08:55 | aiportal-front | 개선 | 5분 | 32 | $1.89 | 1.5M / 22K | success |
| 08:58 | aiportal-front | review | 2분 | 13 | $0.80 | 366K / 9K | success |
| 08:59 | aiportal-front | 릴리즈 | 1분 | 12 | $0.46 | 242K / 5K | success |
| 09:18 | aiportal-java | 개선 | 8분 | 34 | $1.99 | 1.5M / 22K | success |
| 09:31 | weekly | 릴리즈 | 12분 | 18 | $0.99 | 641K / 10K | success |
| 09:49 | moina | 릴리즈 | 2분 | 16 | $1.02 | 585K / 9K | success |
| 09:51 | visitflow | 릴리즈 | 2분 | 11 | $0.66 | 263K / 10K | success |
| 09:58 | dataworks | 릴리즈 | 6분 | 42 | $2.28 | 2.0M / 20K | success |
| 10:02 | jupiq | 릴리즈 | 1분 | 11 | $0.55 | 274K / 5K | success |
| 10:10 | Quantoss | 릴리즈 | 1분 | 9 | $0.33 | 185K / 3K | success |
| 10:25 | Quantoss | 릴리즈 | 1분 | 9 | $0.33 | 157K / 3K | success |
| 10:28 | ai-admin | 릴리즈 | 2분 | 18 | $0.72 | 460K / 7K | success |
| 10:37 | aiportal-front | 릴리즈 | 1분 | 10 | $0.33 | 179K / 3K | success |
| 10:39 | aiportal-front | 릴리즈 | 1분 | 8 | $0.34 | 153K / 3K | success |
| 10:41 | aiportal-py | 릴리즈 | 1분 | 13 | $0.39 | 211K / 4K | success |
| 10:43 | aiportal-py | 릴리즈 | 1분 | 13 | $0.33 | 177K / 3K | success |
| 10:50 | aiportal-py | 개선 | 4분 | 32 | $1.67 | 1.3M / 20K | success |
| 10:52 | aiportal-py | review | 1분 | 8 | $0.51 | 254K / 6K | success |
| 10:54 | aiportal-py | 릴리즈 | 1분 | 13 | $0.47 | 219K / 5K | success |
| 11:21 | appstore | 개선 | 5분 | 45 | $2.38 | 2.3M / 23K | success |
| 11:23 | appstore | review | 2분 | 10 | $0.56 | 293K / 7K | success |
| 11:29 | appstore | 릴리즈 | 3분 | 30 | $1.12 | 978K / 8K | success |
| 11:51 | dataworks | 개선 | 6분 | 39 | $2.43 | 2.2M / 22K | success |
| 11:55 | dataworks | review | 3분 | 22 | $1.02 | 824K / 9K | success |
| 11:59 | dataworks | 릴리즈 | 3분 | 22 | $1.22 | 964K / 9K | success |
무엇을 왜 바꿨나 (원장 발췌)
aiportal-py
- 선택: 기동 시 정책 토큰 무기한 대기 제거하고 degraded 기동 도입 (감사 A-002) (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
api.py의lifespan이while token_store.policy_token is None에서 timeout 없이 0.5초 폴링을 반복해, 토큰 endpoint 장애·잘못된 credential·빈 URL 이면 startup 이 영원히 끝나지 않았다. FastAPI 는 lifespan 이yield하기 전까지 요청을 받지 않으므로/health조차 호출할 수 없고(docs/OPERATIONS.md 에도 알려진 제약으로 적혀 있었다) 원인 로그도 남지 않았으며, 토큰 갱신 루프는 600초마다 재시도하므로 첫 실패 후 10분간 무의미한 폴링만 돌았다. 총 대기 제한(STARTUP_WAIT_TIMEOUT기본 60초)·exponential backoff(0.5초→5초)·ready/timeout/aborted원인과 경과 시간·확인 횟수를 돌려주는wait_until()을 담은 설정 모듈 비의존util/startup_wait.py를 추가하고, lifespan 이 결과와 무관하게yield해 degraded 상태로 기동하도록 바꿨다(실패 시 DEGRADED 로그, 갱신 task 가 이미 죽었으면abort로 즉시 종료하고 그 예외를 로깅). degraded 상태를 관측할 수 있게/health에checks.m2m_token을 추가했고(HTTP 는 계속 200, bodystatus만unhealthy— 기존 계약 유지), 동의어 API 장애가 갱신 루프 task 를 죽여 토큰이 영영 발급되지 않던 경로를 막으려util/token_store.py의 Kiwi·동의어 초기화를try/except로 감쌌다.sleep/monotonic을 주입할 수 있어 실제로 기다리지 않고 backoff·deadline·abort 를 검증하는 단위 테스트와,api.py·token_store.py는 import 불가라 AST 정적 검사(무기한 폴링 금지·wait_until사용·무조건yield·m2m_token검사·초기화 보호)를tests/unit/test_startup_wait.py에 추가했다. 옛 코드를 되돌려 넣어 정적 검사 5건이 실제로 실패하는지 확인했다.python -m pytest691 passed(기존 654),python -m pyflakes .undefined name 0건. docs 4종(CURRENT_STATE_AUDIT/OPERATIONS/CONFIGURATION/TESTING) 갱신. 커밋90199dc. - 보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
- 보류 아이디어:
util/extract_minor.py의except Exception: return e정리 — 예외 객체가extract_logic→fileobj["text"]→ Milvus 색인까지 흘러가고, 지원하지 않는 확장자는None이 된다."err"로 통일 필요 — 가치 3 / 위험 2 / S - 보류 아이디어: A-102/A-206 후속 — 추천 질문·토큰 캐시를 프로세스 간 공유(외부 cache 또는 sticky routing) — 가치 3 / 위험 3 / M
- 보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용
requests.Session도입 — 가치 3 / 위험 3 / M - 보류 아이디어:
api.py의 미사용 import 정리 후 pyflakes 를 CI 게이트로 승격 — 가치 2 / 위험 1 / S
appstore
- 선택: bootstrap 로그인 username 길이 상한 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
bootstrapLogin이 본문의 username을 길이 검사 없이"bootstrap:"+IP+":"+username형태의 rate limiter key와GetBootstrapCredential의 DB 파라미터로 그대로 썼다. 본문 한도는 2MB이고fixedWindowLimiter는 1024번째 호출에서만, 그것도 이미 끝난 윈도우의 bucket만 지우므로, 비인증 클라이언트가 매번 다른 메가바이트급 username을 보내면 map에 수백 MB가 몇 분간 남았다(분당 IP별 API 한도가 낮을수록 sweep이 늦게 와 더 오래 유지됨). 이제normalizedLoginUsername이 소문자·trim 후 빈 값이거나 120 rune을 넘으면 limiter와 PostgreSQL을 건드리지 않고 기존과 동일한 401 문구로 응답하며, 조회에는 쿼리가 어차피lower()로 비교하던 정규화된 이름을 넘긴다. 로그인 폼의 username에도 같은maxLength={120}을, OpenAPIbootstrapLogin스키마에도 minLength/maxLength를 문서화했다. 검증: 순수 함수 경계 테이블 테스트와 “사용 불가 username은 limiter bucket을 만들지 않는다”는 핸들러 테스트를 추가한 뒤gofmt -l,go vet ./...,go test ./...,go test -race ./internal/httpapi ./internal/auth,go build ./cmd/server,npm run lint,npm test,npm run build, prettier check,check-env-contract.sh·check-docs.sh·check-offline-assets.sh모두 통과. 커밋 ee70048. - 보류 아이디어: MCP
featured_apps가Sort:"updated"를 강제해 featured_rank 편집 순서를 무시(가치 2/위험 1/S) ·httpapi.decodeSetting이 Unmarshal 실패 시 부분 변형된 fallback을 반환(가치 2/위험 1/S) · rate limiter의Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산(가치 2/위험 1/S) ·clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 bucket을 공유(가치 3/위험 3/M) · 앱screenshotsURL에 scheme 검증이 없어cleanStrings길이 검사만 통과하면 저장됨(가치 2/위험 2/S)
dataworks
- 선택: 한 API 키가 같은 상품에 엔타이틀먼트를 여러 개 가질 때 활성 행 대신 최근 행을 평가하던 문제 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
dw_api_entitlements는id만 PRIMARY KEY 라 admin POST 마다 새 행이 생겨 한 API 키가 같은 상품에 여러 엔타이틀먼트를 가질 수 있는데(만료된 체험 옆에 발급한 갱신, 감사용으로 남긴revoked행),store.FindAPIEntitlement는ORDER BY updated_at DESC LIMIT 1로 마지막에 쓰인 행 하나만 읽었다. 그래서 활성 계약이 revoked/만료 행보다 먼저 쓰였다면 유효한 권한이 있는데도POST /v1/data-products/{key}/query가 403inactive_entitlement로 막혔고, 활성 계약이 둘이면 어느 쪽allowed_fields·rate_limit·과금으로 처리될지 임의였다(신규 두 행은updated_at이 같은 값으로 저장될 수 있어 순서가 비결정적). 활성·미만료 행을 우선하고 그중 가장 최근 것을 고르도록 바꿨으며, 활성 행이 없으면 종전처럼 최근 행을 돌려줘 게이트가missing_entitlement가 아닌inactive_entitlement로 응답하게 유지했다. 활성 판정은store.EntitlementActive(파싱 불가 만료일은 만료로 취급) 하나로 모으고 런타임 게이트entitlementActive가 이를 위임하게 해 선택 규칙과 게이트 규칙이 어긋나지 않도록 했다. 검증: 스토어 선택 테스트(활성 우선, 활성 부재 시 최근 행 반환)·EntitlementActive표 테스트·HTTP 회귀 테스트(만료 행이 나중에 쓰여도 200 이고 활성 계약 키로 처리)를 추가하고 활성 우선 로직을 꺼서 두 회귀 테스트가 실제로 실패하는 것을 확인,go build ./...·go vet ./...(0건)·go test ./...전체 통과, 수정·신규 파일gofmt -l클린,go run ./cmd/api-surface-auditgap 0.docs/OPERATIONS.md5절에 선택 규칙을 문서화했다. web 변경이 없어 웹 체크는 미실행이고, 직전 회차(v0.9.43)가 아직 main 에 병합되지 않아 버전 충돌을 피하려 이번 회차는 릴리즈 커밋 없이 수정 커밋만 남겼다. -
보류 아이디어: Playwright e2e 를 서비스 컨테이너 기반 CI 잡으로 편입 /
npm run build가 추적 파일web/dist/.gitkeep을 삭제하는 문제를 vite 설정으로 해결 / 계약·엔타이틀먼트 만료 임박 기준(30일 고정)을 쿼리 파라미터로 노출 /contractResponseFields가allowed_fields와 다른 대소문자로 요청한 필드를 요청 표기 그대로 응답(계약 표기로 정규화 필요) /internal/dataworks도메인 함수(EvaluatePublishGateV2·EvaluateRetirementCandidate) 단위 테스트 보강 - 릴리즈: v0.9.43 (2026-09-07, run 2026-09-07-095229-dataworks-release)
git-ctx
- 선택: 의존성 인벤토리가 조용히 버리던 네 곳(매니페스트·락파일 크기 초과, 락파일 4000개 절단, ref당 매니페스트 60개 상한)을 색인 경고로 보고 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공 (commit fbcff94, 릴리즈 fecee25)
- 요약:
manifest.Parse/ParseLock은MaxManifestBytes(1MB)·MaxLockBytes(8MB)를 넘긴 파일에 아무 표시 없이nil을 돌려주고,MaxLockPackages(4000)를 넘긴 락파일은 앞쪽 4000개만 남기며,indexer의 2차 패스는 크기 초과 파일을 조용히 건너뛰고 ref당 60개 상한에서는break로 남은 것을 세지 않았습니다. 없어지는 것은 해석된 버전 — 권고를 즉답할 수 있는 유일한 근거이고, 인벤토리에 없는 저장소는find-dependency-usage의 답에서 그 라이브러리를 안 쓰는 저장소와 똑같이 보입니다(go.sum은 모듈 경로 순 정렬이라 4000 절단은 알파벳 뒤쪽을 통째로 지웁니다). 한계는 그대로 두고ParseNoted/ParseLockNoted가 읽지 못한 사실을 note로 돌려주게 했고,appendPackages가 그것을 이미 있던manifestWarnings경로로 올립니다.break는 남은 매니페스트를 세는continue로 바꿔 “60개를 다 읽었다”와 “60개를 읽고 40개를 버렸다”를 구분합니다.Parse·ParseLock의 서명과 한계값은 그대로입니다. 검증: 새 테스트 5건(TestOversizedManifestSaysWhatItDropped,TestOversizedLockSaysWhatItDropped,TestTruncatedLockSaysHowManyItDropped,TestAFileReadWholeCarriesNoNote,TestAppendPackagesReportsWhatItCouldNotRead) 추가 후gofmt -l,go vet ./...,go build -tags sqlite_fts5 ./...,go test -tags sqlite_fts5 ./...전체 통과, manifest·indexer·search는-race도 통과. -
보류 아이디어: (1)
parseRequirements에!=·===가 없어urllib3!=1.25.0의 버전이"!"로 기록되고foo===1.0은 빈 버전이 됨 (가치 2 / 위험 1 / S). (2)clampResponse의 truncation notice 예약치 320B보다 실제 공지가 길어 “예산에 맞춰 잘랐다”면서 예산을 십수 바이트 초과 (가치 2 / 위험 2 / S). (3)parsePOM이<dependencyManagement>선언까지 실제 의존성으로 집계 — BOM만 정의한 부모 POM이 허위 양성 (가치 3 / 위험 3 / M). (4)mcp.filterLibraries/cacheKey가 호출자 슬라이스를 제자리 변경 (가치 2 / 위험 1 / S). (5) 인덱스 작업의warning이 매니페스트 경고를 앞 3건만 join하고,candidates == 0일 때는 warning 전체를 덮어써 매니페스트 경고를 잃음 (가치 2 / 위험 1 / S). - 릴리즈: v0.77.8 (2026-09-07, run 2026-09-07-010047-git-ctx-improve)
igame
- 선택: 진행 중인 세션을 일일 플레이 제한에 반영 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
duration_ms는 세션이 끝날 때만 기록되므로playAllowed(internal/api/catalog.go)의sum(duration_ms)가 아직 열려 있는 세션을 0분으로 셌다. 제한은 세션을 시작할 때 확인하는데 그 순간 열려 있는 세션이 바로 합계에서 빠진 것이라, 종료된 세션만 제한 아래면 이미 몇 시간을 플레이했든 다시 시작이 허용돼 매일 한 세션만큼 한도를 넘겼다(제한 60분에 55분씩 플레이하면 110분까지 통과). 열린 세션은 시작 이후 경과 시간을 더하도록 고쳤다 — 세션을 finish 할 때와 새 세션이 이전 세션을 abandoned 로 닫을 때 이미 쓰는 것과 같은 계산이다. 판정 주체가 SQL이라 실제로 돌려서 검증했다:IGAME_TEST_DSN이 있을 때만 도는 PostgreSQL 테스트(internal/api/catalog_pg_test.go)가 마이그레이션을 버릴 DB에 적용하고 진행 중 세션·서비스 하루 시작 이전 세션·duration 없이 닫힌 세션 세 경우를 확인하며,make test-db DSN=...타깃과 README 안내를 추가했다. 검증:gofmt -l,go vet ./cmd/... ./internal/... ./migrations/...,go build ./...,go test·go test -race전체 통과, dockerpostgres:17-alpine컨테이너로 새 테스트 3개 통과 및 옛 쿼리로 되돌리면 실패함을 확인. 웹 테스트/린트는 이 워크트리에web/node_modules가 없어(오프라인) 실행하지 못했고, 변경은 Go 1개·테스트 1개·Makefile·README 뿐이라 프런트엔드에 닿지 않는다. - 보류 아이디어: (1)
cmd/igame커버리지 13.7% — 기동/종료 경로 테스트 보강. (2)listUsers만 검색어를 TrimSpace 하지 않아 다른 목록과 동작이 다른 점 정리. (3)Migrate의 체크섬 불일치·트랜잭션 경계 통합 테스트 — 이제IGAME_TEST_DSN하네스가 생겨 실제로 쓸 수 있다. (4)<input type="time">은 24:00을 입력할 수 없어 “종일 허용” 창을 UI에서 만들 수 없는 점 정리. (5)issueSession의 쿠키 MaxAge가s.Now()가 아니라 실제 시계(time.Until)로 계산돼 주입된 시계와 어긋나는 점 정리.
jikim
- 선택: 신뢰 Reverse Proxy 설정 기반 X-Forwarded-For 처리로 감사 로그·로그인 rate limit IP 고정 문제 해결 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
remoteIP가RemoteAddr만 사용해 운영 권장 구성인 TLS Reverse Proxy 뒤에서는 감사 로그의 IP가 항상 Proxy 주소로 기록되고 로그인 실패 제한 키의 IP 성분도 모든 클라이언트에서 동일해졌습니다.X-Forwarded-For를 무조건 신뢰하면 누구나 IP를 위조할 수 있어 환경변수 대신security설정에trusted_proxies(CIDR·IP 목록)를 추가하고, 접속 주소가 등록 대역일 때만 전달 체인을 오른쪽에서 왼쪽으로 훑어 신뢰 대역 밖 첫 주소를 클라이언트로 판정하도록clientIP를 새로 만들었습니다(목록이 비면 기존 동작 유지, 30초 TTL 캐시). 저장소의 “환경변수 네 개” 제품 계약을 깨지 않으려고 설정 값으로 두었고 관리 화면 보안 탭에 입력 필드와 안내를 추가했습니다. 검증은clientIP판별 10개 케이스·rate key 분리·trusted_proxies검증/파싱 단위 테스트를 추가하고./scripts/verify.sh전체(Go test·vet·gofmt, React test·lint·build, docs, compose)를 통과시켜 확인했습니다. 커밋aa0eccf, 릴리스 커밋a20a367(v0.2.4). - 보류 아이디어: Transit batch_input/batch_results 지원으로 OpenBao 호환 범위 확대 (3/3/M) / 로그인 성공 판정 전에 rate limiter를 succeeded로 초기화하는 순서 정리 (2/1/S) / settings GET이 주입하는 파생 필드가 PUT 왕복 시 workflow 설정에 저장되는 문제 정리 (2/1/S) / 감사 로그 보존(audit_retention_days) 자동 정리 구현 (3/3/M) / requestIsHTTPS가 신뢰 Proxy 여부와 무관하게 X-Forwarded-Proto를 신뢰하는 부분을 trusted_proxies와 일관되게 정리 (2/3/S)
jupiq
- 선택: 허브 수집 goroutine 동시 실행 상한과 종료 대기 도입 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
collectHubs가 수집 대상 허브 수만큼 goroutine을 제한 없이 띄우고 아무도 기다리지 않아, 허브가 많으면 30초 주기마다 그만큼의 아웃바운드 HTTP·DB 연결이 동시에 열리고 종료 시에는main의defer database.Close()가 진행 중인 상태 쓰기 아래에서 풀을 닫아 버렸다.updateHubHealth가context.WithoutCancel로 쓰기 컨텍스트를 분리해 둔 의도가 무산되던 지점이다.goHub(용량 8 세마포어, 취소된 컨텍스트면 대기 중인 프로브를 버림)와waitForHubs(10초 상한 대기)를 추가하고Run이 반환할 때만 대기하도록 해 kubernetes·prometheus 수집이 주기마다 지연되지 않게 했으며,main은 HTTP 종료 후 collector 종료를 기다린 뒤 DB를 닫는다. 동시 실행 상한·종료 대기·취소 시 대기열 폐기를 덮는 테스트 3개를 추가해 collector 커버리지가 13.9%→18.7%로 올랐고, 세마포어를 제거한 변형에서 테스트가 실제로 실패하는지 확인했다.go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 58개) 모두 통과했다. - 보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(boundedTimeRange로 대체됨) /internal/secure의EncryptString·DecryptString·Derive·RandomToken테스트 공백 보강(현재 39.5%) /collectPrometheus가 metric마다 features 설정을 다시 읽는 중복 조회 제거 / 로그인 리미터succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 /collectHubs의due가 프로브 성공 여부와 무관하게 시각을 선기록해 실패한 허브가 전체 간격만큼 재시도되지 않는 문제 검토
kanpic
- 선택: 외부 호출 캐시가 그때의 크기 상한과 시간 제한을 함께 기억한다 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
internal/external의 답 캐시는 자리 이름을 함수·주소로만 지어, 옛external.max_kb에서 받은 “응답이 허용된 크기를 넘습니다” 를 관리자가 상한을 올린 뒤에도cache_seconds(기본 5분, 최대 하루) 동안 그대로 내주었다 — 관리자에게는 설정이 먹지 않은 것으로 보인다. 허용 호스트 목록은 이미 캐시 바깥에 있어 고치면 곧바로 통하는데(TestPolicyIsNotCachedInEitherDirection), 크기 상한과 시간 제한도 부르는 방법을 정하는 정책이라는 점에서 다르지 않으므로 새cacheKey가MaxBytes·Timeout을 자리 이름에 함께 넣게 했다(엔진이 답을 찾는formula.ExternalKey는 그대로 두었다). 검증: 새 테스트 2개(TestRaisingTheSizeCeilingIsBelievedAtOnce— 고치기 전에 실제로 깨지는 것을 확인했다,TestResolveIsSafeToShare— 여덟 갈래가 같은 주소들을 한꺼번에 부르는 길을-race로 못 박음),gofmt -l,go vet,go build ./...,go test ./...(전체 통과)와go test -race ./internal/external/,scripts/check-release-docs.sh,scripts/check-commit-identities.sh. 커밋 1개(7cdb124), 릴리즈 노트 v0.237.0 과 README VERSION 갱신. 웹·가이드 문서는 external 설정을 다루지 않아 손대지 않았고 npm 검사도 돌리지 않았다. -
보류 아이디어: 원격 500·타임아웃 같은 실패도 캐시에 담겨 원격이 고쳐져도 cache_seconds 동안 그대로다(정책과 달리 실패는 짧게 담거나 담지 않을지 검토) /
parseCSV가 쉼표·탭만 보아 세미콜론으로 가른 유럽식 CSV 를 한 열로 읽는다 /csvNumber자리(formula.DecimalNumber)가 IMPORTDATA 의"1,200"·"12%"·앞뒤 통화 기호를 글자로 남긴다 /DOLLARDE·DOLLARFR이math.Pow(10, ceil(log10(fraction)))로 자리를 밀어DOLLARDE(1.02,16)이 1.1250000000000002 다 /?의 자리 맞추기 빈칸을 그리지 않아# ??/??의 한 자리 분자가 자릿수를 맞추지 못한다 - 릴리즈: v0.237.0 (2026-09-07, run 2026-09-07-020048-kanpic-improve)
moina
- 선택: 미디어 응답
Content-Disposition에 RFC 6266filename*추가 (가치 3 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
getMedia가 저장된 파일 이름을fmt.Sprintf("inline; filename=%q", …)로 header에 그대로 넣어,사진.png같은 한글 이름이 charset 표시 없는 raw UTF-8 바이트로 나갔고 브라우저는 이를 대개 Latin-1로 읽어 “이미지 저장” 이름이 깨졌습니다(header 문법을 깨뜨릴 수 있는"도 조용히 지워 이름이 달라졌습니다). RFC 6266대로 ASCII fallback을filename에, 원래 이름을 RFC 8187 percent-encoding으로filename*=UTF-8''에 함께 내려보내는contentDisposition헬퍼를 추가하고(ASCII 이름은 기존과 완전히 동일한 header 유지),api/openapi.yaml의 media 조회 설명에 이 계약을 적었습니다. 검증은 새 테스트 6케이스(생성 값 비교 +mime.ParseMediaType으로 원래 이름 복원 확인) 포함go test -race ./...전체 통과,make fmt·make check·go vet·staticcheck 통과(DB를 건드리지 않는 순수 로직이라 integration test는 skip, frontend 무변경이라 ESLint·vitest 생략). - 보류 아이디어:
updatePost가 DB 오류를 409not_editable로 보고해 원인을 감춤(가치 2 / 위험 1 / S) ·safeFilename이 확장자와 판정한 MIME의 불일치를 그대로 둬 JPEG이photo.png로 저장·다운로드됨(가치 2 / 위험 2 / S) · Makefiletest가 CI와 달리-race미사용(가치 2 / 위험 1 / S) · 일간 DigestdigestDue가 DST 전환일에 존재하지 않는 지역 시각을time.Date정규화에 맡겨 발송 시각이 한 시간 밀림(가치 2 / 위험 3 / M)
moyro
- 선택: 클라이언트가 preferences 테이블에 무한정 적재할 수 있던 경로 차단 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약:
preferences테이블에는 어떤 상한도 없었다 —value는 TEXT이고, 행은 (user_id, category, name) 키에 소유자 본인이나 사용자 삭제 캐스케이드 외에는 아무도 지우지 않는다. 그런데 두 쓰기 핸들러(PUT /users/{id}/preferences,POST .../preferences/delete)는MaxBytesReader없이 요청 본문을 곧바로 무제한[]Preference로 디코드했고, 서비스는 각 행의 user_id가 행위자와 같은지만 확인했다. 따라서 인증된 계정이면 누구나 임의 크기 배열을 서버 메모리에 버퍼링시킨 뒤 영구 저장할 수 있었고, 그 전량이 이후 매 로그인마다ListAll로 되읽히고 WS 브로드캐스트로 자기 소켓에 다시 실려 나갔다. Upsert/Delete가 공유하는Validate하나에 배치 크기·category/name/value 길이(바이트가 아니라 룬 기준이라 한국어 값이 4배 빨리 거절되지 않는다)·PostgreSQL이 TEXT에 저장 자체를 못 해 드라이버 500으로 새어나가던 NUL 바이트 거절을 넣었다. 상한은 Mattermost 자신의 32/32/2000보다 느슨하게 잡았는데, moyro는 채널을 36자 uuid로 식별하므로 32룬 name 상한이면favorite_channel이 통째로 거절되기 때문이다. Upsert는 기존 트랜잭션 안에서 계정의 행 수를 세어 사용자당 상한을 넘기는 배치를 통째로 롤백한다(한 행 넘긴 상태로 눌러앉지 않도록). 핸들러는 본문에MaxBytesReader를 씌우고, 뭉뚱그려 400으로 내보내던 두 실패 모드를 분리해 거절된 페이로드만 400, 실제 저장 실패는 500이 되게 했다. 테스트가 하나도 없던 패키지에 검증 표 테스트 2건과 PostgreSQL 통합 테스트 3건(상한·롤백·정상 경로)을 추가하고 CI PostgreSQL 잡 대상에./internal/preferences를 넣었다. 로컬 postgres:16-alpine 컨테이너로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 검증 코드를 잠시 무력화해 통합 테스트 2건이 실제로 실패하는 것까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1) 커스텀 상태가 write-only — 재확인 결과
userstatus.GetCustomStatus는 여전히 호출자가 없고 moyro 웹앱은 커스텀 상태를 읽지도 쓰지도 않아 이득이 공식 클라이언트 한정이라 가치를 3으로 내림. (2)userstatus.Get이 실제 DB 오류까지 삼키고 offline을 반환해 장애가 “전원 오프라인”으로 위장되는 문제. (3)getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 “설정 없음”으로 위장하는 문제 — 이번 회차의 400/500 분리와 같은 부류. (4)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 쓰도록 허용. (5) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합.
muni
- 선택: 그림이 그려진 모양 그대로 편집기와 HTML·PDF 까지 가게 하기 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 지난 회차에 한글 두 형식이 그린 높이를 읽게 되었지만 그 높이는 문 앞에서 계속 사라지고 있었습니다 —
.docx리더는<wp:extent>의cx만 읽었고(라이터는 예전부터 둘을 짝으로 읽습니다), PDF 리더도 상자의 너비만 담았으며, HTML 가져오기는height를 무시했고, 무엇보다 화면과 인쇄의 스타일시트가 쪽보다 넓은 그림을 줄이려고 모든 그림에height:auto를 주는 탓에 눌러 둔 그림이 다시 바이트의 모양으로 펴지고 있었습니다. 그래서 크기를 지닌 그림은aspect-ratio로 그리도록 서버의 HTML 과 편집기의SizedImage를 함께 고쳤습니다(비율은 줄어드는 동안 살아남고 고정 높이는 그러지 못합니다). 그림 메뉴의 % 단추도 높이를 같은 비율로 옮깁니다. 검증은 새 테스트 여덟(고치기 전에 실패하는 것을 확인한.docx왕복, 렌더·가져오기 넷, 프런트 넷)과gofmt -l·go vet ./...·go test ./...·npm test(258)·npm run build·npm run lint전체 통과,scripts/check-webui-placeholder.sh로 빌드 산출물이 커밋에 섞이지 않았음을 확인. 커밋 7c5e172. - 보류 아이디어: CI 에 go vet 과 gofmt -l 추가 (2/1/S) — 일곱 회차째 손으로 돌렸음 / .hwpx 인용문·코드블록 왕복 (3/2/M) — 읽기가 되살릴 단서가 없어 스타일을 심어야 함 / .hwpx 머리글 칸의 기본 음영 (2/3/S) — .docx 는 F3F4FA 를 까는데 .hwpx 는 굵게뿐 / .hwp 표 캡션의 위치(위/아래) 읽기 (2/2/S) — 지금은 언제나 표 뒤에 붙임 / 그림의 그린 크기를 markdown·.hwpx 왕복에서도 지키기 (2/2/S) — markdown 은 크기를 담을 자리가 없어 잃는 것이 정상이지만, 붙임말로 남길 수 있는지 볼 만함
pii-masker
- 선택: job 결과 다운로드를 디스크 스트리밍으로 전환 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
handleGetJobResult가os.ReadFile로 마스킹 결과 파일(최대 50MB)을 통째로 메모리에 올린 뒤 응답해서, 동기 마스킹(PII_MASKER_MAX_CONCURRENT_SYNC)·비동기 job(PII_MASKER_MAX_CONCURRENT_JOBS)에 걸어 둔 메모리 상한과 달리 다운로드 경로에는 아무 제한이 없어 동시 다운로드 수만큼 메모리가 선형으로 늘었습니다.os.Open+file.Stat()+http.ServeContent로 바꿔 파일을 버퍼링 없이 흘려보내고, 덤으로Accept-Ranges/Range(재개 가능한 다운로드)와 조건부 요청을 지원하며, 보존 기간 스위퍼나 외부 요인으로 결과 파일이 사라진 경우500 result_read_failed대신404 job_result_not_found를 반환하도록 고쳤습니다(errors.Is(err, fs.ErrNotExist)).Content-Type은 메타데이터 값이 있을 때만 설정해 비어 있으면ServeContent의 확장자 추론에 맡깁니다. 검증은 통합 테스트 2개(완료된 PDF job에 대해Accept-Ranges: bytes확인 +Range: bytes=8-23→ 206·Content-Range·본문이 전체 응답의 해당 슬라이스와 일치, 결과 파일을 디스크에서 지운 뒤 404 +job_result_not_found)를 추가했고,gofmt -l(무출력)·go vet ./...·go build ./...·go test -count=1 ./...·go test -race -count=1 ./...전부 통과,-race -count=5로 플래키 여부 확인, 수정 전os.ReadFile구현으로 되돌려 새 테스트 2개가 실제로 실패(각각Accept-Ranges빈 값 / 500result_read_failed)하는 것까지 확인했습니다. -
보류 아이디어:
/v1/history의limit상한 없음(?limit=100000한 번으로 전체 job 직렬화, 기본 20/최대 100 클램프 필요) / 동기 슬롯 대기열의 메모리 상한 — 대기 중인 요청이 이미 읽은 업로드 바이트를 들고 있어 대기열 메모리는 여전히 무제한 /internal/config의 나머지 순수 함수(normalizeAllowHosts,normalizeEndpointURL,normalizePIILang/Schema,envInt/envNonNegativeInt/envBool) 단위 테스트 / 종료 시 실행 중인 비동기 job이running으로 남고 재기동 시failed로 표시될 뿐 재개되지 않음(종료 전 완료 대기 또는queued되돌림) //v1/jobs/{id}/result가GET만 라우팅되어HEAD다운로드 프로브가 405를 받음(ServeContent는 이미 HEAD를 처리 가능) - 릴리즈: v1.0.14 (2026-09-07, run 2026-09-07-032049-pii-masker-improve)
ptium
- 선택: 이어지는 슬라이드가 모두 첫 아홉 줄을 출처로 적는 문제 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
internal/docs/tables.go의writeSheet가 이어지는 장마다rangeOf(sheet, columns, len(piece)+1)을 불러서, 슬라이드 한 장에 안 들어가는 시트가 나뉠 때 모든 장이실적!A1:B9라고 똑같이 말했다(스크래치 테스트로 24줄 시트 → 세 장이 전부 A1:B9 재현). 출처를 보고 시트를 연 사람은 그 숫자가 없는 여덟 줄로 가고, 틀렸다는 표시도 없다 — 출처가 있는 이유가 그것 하나인데. 각 조각이 시트에서 시작하는 행(머리글이 1행이므로 본문은 2행부터)을sheetPiece로 함께 들고 다니게 하고rangeOf를 줄 수 대신 첫 행·끝 행을 받게 바꿔, A1:B9 → A10:B17 → A18:B25 로 겹치지 않고 이어지게 했다. 한 장짜리 시트와 워드 표 경로는 그대로다.sheetranges_test.go(시트 6가지 + 범위 안 항목 확인 1가지 +rangeOf7가지)를 추가하고make test(go test -race, go vet, tsc, vite build) 전부 통과. VERSION 1.69.28 스탬프(openapi·kubernetes·offline-deployment)와 릴리스 노트 작성, 커밋 ae451c5. - 보류 아이디어:
gridOf가 행의r을 보지 않고trimGrid이 빈 행을 지워서, 시트 중간에 빈 줄이 있으면 출처 행 번호가 실제 시트와 어긋나는 문제 (가치 3 / 위험 3 / M)- 숨긴 시트(
sheet state="hidden")를 그대로 가져와 설정·코드표 같은 시트가 슬라이드가 되는 문제 (가치 3 / 위험 1 / S) allNumeric이 통화 기호·괄호 음수(“₩1,200”, “(1,200)”)를 숫자로 보지 않는 문제 —deck/compile.go의parseBareNumber도 같이 손봐야 함 (가치 3 / 위험 3 / M)- 맥 엑셀의 1904 날짜 체계(
workbookPr date1904)를 무시해 날짜가 4년 이르게 읽히는 문제 (가치 2 / 위험 2 / S)
- 릴리즈: v1.69.28 (태그·푸시는 외부 스크립트)
releasedock
- 선택: 업로드 도중 종료된 프로세스가 대상 디렉터리에 남긴 임시 업로드 파일 정리 (가치 3 / 위험 2 / 작업량 S)
- 결과: 성공
- 요약: 0.5.5 에서 도입한 스테이징(
<패키지>.partial-<토큰>으로 먼저 쓰고 실행 기록이 대상을 확보한 뒤 확정)은createSimpleRun이 반환하는 모든 경로에서discard로 정리되지만, 프로세스가 업로드 도중 죽는 경우만은 예외라 임시 파일이 그대로 남습니다. 그 파일은 어떤 실행 기록도 가리키지 않고 화면에도 보이지 않으면서 패키지 크기(기본 상한 10 GiB)만큼 디스크를 차지하고, 아무도 지우지 않아 재시작마다 쌓입니다 — 이미 같은 이유(자식 프로세스가 함께 사라짐)로RecoverSimpleRuns가PENDING/RUNNING행을 마감하고 있는데 파일 쪽만 비어 있었습니다. 부팅 시simple_targets.upload_dir을 훑어 스테이징 이름 규칙에 정확히 맞는 일반 파일만 지우는RemoveStagedSimpleUploads를 추가하고 main.go 에서RecoverSimpleRuns바로 뒤에 호출했습니다. 이름 판정은isStagedUploadName으로 분리해 마커 뒤가RandomToken(16)의 base64url 22자와 정확히 일치할 때만 참이 되게 했고(운영자가 같은 디렉터리에 둔 파일·디렉터리·심볼릭 링크는 제외), 하드코딩한 접미사 두 곳을 상수로 묶어 스테이징 쪽과 어긋날 수 없게 했습니다. 파일시스템 단위 테스트 3건을simple_artifact_test.go에 추가했고(실제stageSimpleArtifact가 만든 이름이 인식되는지, 비슷한 이름들이 거부되는지, 스윕이 패키지·디렉터리·없는 경로를 건드리지 않는지), 로컬 도커 PostgreSQL 16 으로TEST_POSTGRES_DSN을 채워 backend/runnergo vet·go test ./...(통합 테스트 포함),npm ci,npm test -- --run(78건),npm run build를 모두 통과했습니다. docs/simple-mode.md 동시성 절에 한 줄 추가하고 VERSION 을 0.5.9 로 올렸습니다(저장소 관례). 주의: 아직 병합되지 않은 브랜치auto/2026-09-06-1820도 0.5.9 를 사용하므로 VERSION 충돌이 나며 둘 중 하나만 그 번호로 릴리즈해야 합니다. - 보류 아이디어: CI 와 Makefile 에
go vet(또는 golangci-lint) 단계가 없어 정적 검사가 매 세션 수동입니다 (가치 3 / 위험 1 / S). - 보류 아이디어:
simpleRunLogger.append가 빈 payload 를 저장하지 않아 스크립트 출력의 빈 줄(문단 구분)이 로그에서 사라집니다 (가치 2 / 위험 1 / S). - 보류 아이디어:
downloadSimpleRunLog이 조회한original_filename·status를 쓰지 않아 다운로드 파일명이 run id 뿐입니다 (가치 2 / 위험 1 / S). - 보류 아이디어: 실행 상세 화면에 같은 묶음의 다른 실행 목록·링크를 표시하면 어느 패키지가 실패했는지 바로 찾을 수 있습니다 (가치 3 / 위험 1 / M).
-
보류 아이디어:
web/dist/assets/vendor청크가 617KB 로 커서 폐쇄망 초기 로딩 최적화 여지가 있습니다 (가치 2 / 위험 3 / M). - 릴리즈: v0.5.9 (2026-09-07, run 2026-09-07-034048-releasedock-improve)
relio
- 선택: 로그인 결과가 무엇이든 같은 비밀번호 검증 비용을 치르게 하기 (가치 5 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
auth.Login의 조건이err != nil || !active || !VerifyPassword(hash, password)였는데 Go 의||는 단락 평가라, 존재하지 않는 사용자명(그리고 존재하지만 비활성인 계정)은 argon2id 에 아예 도달하지 못하고 수십 나노초 만에 거절됐습니다. 실제 계정은 m=64MiB,t=3 이 요구하는 ~100ms 를 쓰므로, 요청 두 번의 시간만 재면 비밀번호를 한 번도 맞히지 않고 어떤 사용자명이 실재하는지·그중 무엇이 비활성인지 가려낼 수 있었습니다 — 로그인 리미터는 주소당 시도 횟수를 셀 뿐 두 응답의 시간 차이를 없애지 못합니다. 이제VerifyLoginPassword가 계정을 쓸 수 없을 때 프로세스당 한 번 만드는 decoy 해시(지금HashPassword가 쓰는 파라미터 그대로)로 파생을 수행하고, 파생이 끝난 뒤에야 계정 판정을 적용합니다. “쓸 수 없는 계정”에 파싱되지 않는 저장 해시도 포함시키려니VerifyPassword가 퇴화한 인코딩에 정직해져야 했습니다 — 키 길이가 0이면 argon2 가 빈 키를 파생하고ConstantTimeCompare가 그것을 모든 비밀번호와 같다고 보고하며, 실제로는 거기 닿기 전에 blake2b 에서 패닉합니다.parseArgon2id가 8바이트 미만 salt 와 16바이트 미만 키를 거절하고, 이 코드가 써온 모든 해시는 16·32 바이트입니다. 검증은 새 테스트 4개(다섯 가지 로그인 결과가 모두 5ms 이상 걸리는지, 일곱 가지 계정 상태의 수락·거절, decoy 가 실제 해시와 같은 파라미터·모양이고 한 번만 만들어지며 어떤 추측도 통과시키지 않는지, 퇴화한 네 가지 인코딩이 임의 비밀번호를 받아들이지 않는지)를 옛 구현으로 되돌리면 실제로 실패하는지 확인 — 단락 평가를 되살리면 존재하지 않는 사용자명이 55ns 에 답해 실패하고, 길이 하한을 빼면 빈 키 인코딩이 blake2b nil 역참조로 패닉 — 한 뒤go test -race ./...,go vet ./...,gofmt -l, web typecheck·build,check-env-contract.sh,check-static-assets.sh전체 통과. 커밋 253c768. -
보류 아이디어: 로컬 로그인이 꺼져 있을 때
/auth/login이 사용자명 존재 여부에 따라 403 과 401 을 갈라 돌려주는 두 번째 열거 통로 (가치 3 / 위험 2 / S) ·system.timezone설정을 읽는 Go 코드가 한 줄도 없어 관리자 화면의 선택이 순전히 장식이고 계약·견적 번호의 날짜까지 UTC 로 찍힘 (가치 4 / 위험 3 / M) · 네 intelligence 필터의Cursor필드는 어떤 질의도 쓰지 않고 어떤 핸들러도 채우지 않는 죽은 코드 — 상위 200건 뒤를 볼 방법이 없음 (가치 3 / 위험 1 / M) ·internal/audit는 테스트가 하나도 없고nullableJSON이 marshal 오류를 버려 감사 데이터를 조용히 NULL 로 만듦 (가치 3 / 위험 1 / M) · CI 에gofmt -l검사 추가 — 지금은 포맷 위반이 CI 를 통과함 (가치 2 / 위험 1 / S) - 릴리즈: v1.11.19 (2026-09-08, run 2026-09-08-094239-relio-release)
umm
- 선택: 자기 글씨 때문에 조각난 생각 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: Markdown 내보내기는 백업이고 v0.65.0부터 umm이 그 파일을 자기 파일로 읽는데, 절을 나누는 규칙이 일반 문서용 그대로였습니다 — 제목마다, 가로선마다 자름. 내보내기 안에서 그 표시는 대개 누군가 붙여 넣은 본문의 일부(
### 배경, 문단 사이---)라, 생각 하나가 여러 조각으로 쪼개지고 메타데이터 목록을 가진 것은 마지막 조각뿐이었습니다. 나머지는 id·좌표·색·갈래 없이 돌아왔고, 연결은 id로 적혀 있으므로 그 조각에 그어져 있던 선은 다시 그을 대상을 잃었습니다. 배너를 본 지점부터는 내보내기가 실제로 쓰는 두 표시(##, 배너를 이고 오는#)에서만 자르게 했고(위에 이미 타이핑한 글은 원래대로 읽으므로 “생각 적고 파일 붙이기” 순서와 파일 두 개 잇기는 그대로), 같은 성질의 두 번째 결함 — 메타데이터를 절 전체에서 찾아 첫 일치가 이기는 바람에 본문의- id: \ours-2024-11`이 그 생각의 id가 되던 것 — 을 절 꼬리에서만 읽도록 고쳤습니다. 내보내기 형식은 그대로라 이미 받아 둔 백업 파일에도 적용됩니다. 검증: 새 시험 4개 추가 후 절 나누기를 되돌리면 2개, 메타데이터 자리를 되돌리면 1개가 실패함을 확인, 실제 PostgreSQL 17(도커umm-test-pg)에 대한go test -p 1 ./…전체 통과 ·go vet ./…· gofmt · tsc · oxlint/Prettier · i18n 994키 · vitest 160개 ·scripts/check-version.sh통과. 곁들여 루트node_modules/가 .gitignore에 없어npm –prefix web test`가 남기는 vite 캐시가 커밋 후보로 올라오던 것도 고쳤습니다. v0.71.5로 릴리스 커밋(147bcf8, 다른 브랜치가 v0.71.3·v0.71.4를 이미 썼으므로 번호를 비켜 씀). - 보류 아이디어: 내보내기 본문에
##로 시작하는 줄이 있으면 여전히 거기서 잘림 — 남은 한 자리는 이스케이프 없이는 생각 경계와 구별 불가 / 내보내기가Content-Disposition파일 이름에 공간 이름을 그대로 넣어"가 헤더를 깨고 한글이 RFC 6266 없이 나감 / Markdown 백업에 그림이 담기지 않는 사실이 릴리스 노트에만 있고 내보내기 메뉴·user-guide·features.md 어디에도 없음 / 401·403이 오프라인 큐 전체를 세우는데 403은 한 변경에 대한 거부일 수 있음 /README.md배지·소개와docs/README.md허브의 버전이 실제와 어긋남
visitflow
- 선택: CSV 내보내기가 행 상한에 걸려 잘렸을 때 사용자에게 알리기 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약: 감사 로그 CSV(10,000행)와 방문 이력 CSV(50,000행)는 상한에 걸리면 조용히 잘려 나갔고, 이 파일들은 화면을 떠나 한참 뒤에 특정 기간의 근거 자료로 읽히므로 잘려 사라진 행이 애초에 없던 행과 구분되지 않았다. 두 쿼리가 상한보다 한 행 더 요청해 잘림 여부를 판정하고, 잘린 경우 파일 마지막에
#로 시작하며 한도를 명시하는 안내 행을 덧붙이도록exports.go에writeExportTruncationNotice와auditExportRowLimit/visitExportRowLimit상수를 두었다. 다운로드가 평범한 링크라 운영자가 응답 헤더를 볼 일이 없어 안내를 헤더가 아닌 파일 안에 넣었고, 감사 기록에는truncated와 실제 적용된limit을 남겼으며 방문 이력 내보내기에도 감사 로그와 같은limit파라미터(상한 50,000으로 클램프)를 추가했다. 검증은go vet ./..., docker postgres:16-alpine을 띄운VISITFLOW_TEST_DSN전체 테스트 통과,npm ci && npm run build이며, 두 내보내기를?limit=1로 호출해 헤더·행 1개·안내 행 3줄이 나오고 상한을 걸지 않은 완전한 파일에는 안내가 없는지 보는 통합 테스트 1개와 단위 테스트 2개를 추가한 뒤 안내 행 작성을 임시로 제거해 실제로 실패하는 것까지 확인했다. API_AND_MCP·ADMIN_GUIDE 문서에 각각 한두 문장을 덧붙였다. - 보류 아이디어:
bestAcceptLanguage의q=0(수용 불가)·q>1·잘못된q값 처리 정정 / CSV·XLSX 가져오기 파서(visitorInputsFromRows) 엣지케이스 단위 테스트 보강 / 설정 내보내기 JSON을 되돌려 넣는 가져오기 경로와 스키마 검증 / MCPget_lobby_status의 사업장 범위 — 로비 화면 쿼리와 동일하게role='lobby'일 때만site_scope를 적용하고dept_manager는 애초에 호출 권한이 없어 불일치 아님(rejected) / 방문 이력 CSV의 50,000행 경계에서 한 방문의 동행자가 잘려 인원이 부분만 나오는 문제(안내 행은 붙지만 마지막 방문이 불완전할 수 있음)
weekly
- 선택: 주 격자를 옮겨도 참여 분석이 이미 낸 보고서를 세도록 고침 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
analyticsParticipation의 두 반쪽이 모두week_start정확일치였습니다. 제출률 추이는 보고서를 자기week_start로 묶고 화면은 그 행을 격자에서 찾으므로, 주차 시작 요일을 바꾼 뒤에는 그 전에 쓰인 어떤 보고서도 어느 행에도 실리지 않습니다 — 지금까지 고쳐 온 일곱 자리와 달리 전환 주 하나가 아니라 창에 담긴 12주 전부가, 매주 보고한 팀에게 제출률 0% 로 보입니다. 미제출자 명단은 같은 질문을 뒤집은 것(NOT EXISTS ... week_start=week.day)이라 같은 답을 냈습니다: 전원이, 자기가 보고한 모든 주에 대해 밀린 사람으로 이름이 오르고 그 명단은 독촉의 근거가 됩니다. 전환 주는weekIsFree가 두 번째 보고서를 막으니 다시 써서 지울 수도 없는 미제출이었습니다. 추세를generate_series격자에서 만들고 각 행이 그 7일을 덮는 보고서를 세도록weekCoveringDaysOf(placeholder 대신 날짜 식을 받는weekCoveringDays의 형제)로 바꾸되, 옮긴 격자의 한 주가 옛 격자의 두 주와 겹치므로 사람마다DISTINCT ON으로 한 건만 골라 제출률이 100% 를 넘지 않게 했습니다.weekIsOwed의NOT EXISTS도 같은 겹침으로 바꿔 미제출 판정이weekIsFree의 거절과 같은 말을 합니다. 정시·지각은 보고서 자신의 주차 마감으로 재는 지금이 옳다고 정하고(작성자가 실제로 지켜야 했던 마감) 관리자 안내서에 그렇게 적었습니다. 검증: 수정 전 코드에서 새 시험이 세 주의 제출 건수·미제출자 명단·미제출자 수 다섯 곳에서 실패하는 것을 확인 → 실제 DB(WEEKLY_TEST_POSTGRES_DSN)로go test ./...전체 통과(138s),go vet,gofmt, guard-check –changed(28개 도달, 새 가드는 analyticsParticipation 71%/weekCoveringDaysOf 100%), version·openapi·modal-close 검사 통과, frontend lint·build·test(121개) 통과. mutation-check –changed 는 MUTATION_RESULT. backup-check 는 로컬에 psql 이 없어 건너뛰었습니다(CI 에서 실행). 커밋은 mutation-check 가 돌지 않는 동안에만 했습니다. v0.290.0 으로 냈습니다(아홉 곳 버전, 릴리즈 본문, 로드맵 기록, 관리자 안내서 문단과 세 안내서 생성물). -
보류 아이디어: analyticsOrganizations 의 제출 CTE 는 날짜 범위, 기대건은 격자 주라 창 가장자리에서 어긋납니다 (2/2/S) · 키워드 분석의 두 창(collectTerms)도
week_start BETWEEN이라 전환 주가 옆 창으로 넘어갑니다 (2/2/S) ·summarizeWorkItem의 AgeWeeks·SilentWeeks 가 7일 등간격을 전제합니다 (2/2/S) · MCPweekly_reports_search의 weekStart 필터가 정확일치라 같은 응답 안의weekly_submission_overview와 다른 답을 합니다 (2/2/S) · 겹침 규칙에서는 옛 격자의 보고서 한 건이 새 격자의 두 주를 면제하므로 ‘가장 많이 겹치는 한 주’ 규칙과 견줄 자리가 남습니다 (2/2/M) - 릴리즈: v0.290.0 (2026-09-07, run 2026-09-07-091911-weekly-release)
AgentHub
- 선택: 정책 규칙의 사용자·역할 조건이 모델 호출 경계에서 절대 적용되지 않던 문제 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공 — 커밋 d1b479a (auto/2026-09-07-0600)
- 요약: policy 패키지 주석이 첫 문장으로 드는 예시가 “계약직은 쓰기 도구를 못 쓰고, 누구도 주민등록번호를 모델로 보낼 수 없다”인데, 뒷 문장을 사람 단위로 좁혀 쓴 규칙(역할·계정)은 판정되는 단 한 곳 — 모델로, 그리고 런타임 자체 엔진으로 텍스트가 나가는 내용 검사기 — 에서 한 번도 매치되지 않았습니다. 그 경계는 동작·에이전트·데이터 등급만 채우고 사람을 비운 채 정책에 물었고, 비어 있는 선택자는 아무것도 매치하지 않으므로 규칙은 그냥 탈락했습니다. 대신 그 등급의 전역 조치가 결정했으므로,
계약직 → 차단을 써 두고 등급은기록만으로 둔 배포는 주민번호를 그대로 보내면서 findings만 남겼습니다. 다른 판정 지점은 전부 누가 하는지 알고 있었습니다: 워커는 task.create 전에 작업 소유자를, 런타임 게이트는 runtime.start 전에 에이전트 소유자를 읽고, Pod 게이트웨이에 컴파일되는 규칙은 소유자별로 컴파일됩니다. 콘솔도 알고 있었습니다 — 시뮬레이터는 사용자를 입력받으므로, 실제로는 허용되고 있던 바로 그 요청에 계속차단이라고 답했고 이 불일치는 어느 화면에도 나타나지 않았습니다. workflow.Step에 OwnerID를 실어(실행 플레인은 Step을 만드는 모든 지점에서 소유자를 알고 있습니다) 검사기가 스캐너가 무언가를 찾았을 때에만 역할·계정을 조회하도록 했습니다. 조회 실패는 런타임·작업 게이트와 같은 답 — 기록하고 비운 채 진행 — 으로 처리해 쿼리 하나 때문에 플랫폼 전체가 멈추지 않게 했습니다. 검증:TestARuleAboutAPersonDecidesAtTheModelBoundary가 경계와 시뮬레이터가 같은 요청에 같은 답을 내는지 확인하고(수정 전 형태인 빈 actor는 allow가 되는 것을TestAnUnreadableOwnerIsNotADecision이 그대로 문서화), 누락이 재발하면 클러스터가 아니라 여기서 실패하도록TestEveryStepSaysWhoItIsFor가 internal/execution·internal/api의 모든 workflow.Step 리터럴을 훑습니다(실제로 한 곳을 되돌려 실패하는 것 확인). go vet ./…, go test -race ./cmd/… ./internal/…, web npm ci+lint+build,release-catalog-images.sh check-versions·validate모두 통과. 변경한 패키지는 런타임 base 이미지 소스가 아니라 BASE_VERSION 상향은 불필요합니다. -
보류 아이디어: 데이터 등급(dataClasses) 조건이 붙은 tool.call 규칙이 컴파일 단계에서 절대 매치되지 않아 Pod에 전달되지 않음 (3/3/M) / 정책 시뮬레이터가 ‘이 규칙이 Pod에서 어떻게 컴파일되는가’를 보여주지 않아 순서 실수를 저장 전에 볼 수 없음 (3/1/M) / plan·judge 단계의 Step에 AgentID가 없어 에이전트 조건 규칙이 플래너·판정자의 모델 호출에는 적용되지 않음 (3/2/S) / scrubDecision이 record.Agent 등 남은 자유 텍스트를 검사하지 않아 에이전트 이름으로는 무엇이든 나갈 수 있음 (2/2/S) / korean.EndsInConsonant가 괄호·따옴표로 끝나는 값에서 조사를 잘못 고름 (2/2/S)
- 릴리즈: v0.235.0 (2026-09-07, run 2026-09-07-060049-AgentHub-improve)
- 릴리즈: v0.236.0 (2026-09-08, run 2026-09-08-091949-AgentHub-release)
Clustara
- 선택: 보안 스캔 결과 ingest 정규화(
analyzer.NormalizeVulnerabilityScan·NormalizeKubeBench)의 오분류 5종 (가치 4 / 위험 2 / 작업량 M) - 결과: 성공
- 요약: 취약점·CIS 스캔 import 는 CI 나 Trivy Operator 가 만든 JSON 을 그대로 받아 Admission 게이트·컴플라이언스 화면·DW export 가 쓰는 원장으로 바꾸는 경로인데, 다섯 지점이 틀려 있었다 — ①
detectScanner가 형식을 못 맞히면"unknown"을 돌려주고 그 값이switch의default로 떨어져 Trivy 파서가 실행되는데 스캔 행에는scanner="unknown"이 저장됐다(호출자가"snyk"처럼 지원 안 하는 이름을 적어도 동일). 스캔 목록·감사 로그가 실행된 적 없는 파서를 말했고, 더 중요하게는 읽히지 않은 아티팩트와 정말 깨끗한 이미지가 둘 다 findings 0건이라 게이트가 구분할 수 없었다 — 이제 실행된 리더로 라벨링하고summary.scanner_detected·requested_scanner·parse_notice로 구분한다. ②NormalizeSeverity매핑표에 Grype 의Negligible과 RPM 권고의Important·Moderate가 없어 셋 다Unknown(랭크 0)으로 접혔다 — 벤더가 High 로 매긴ImportantCVE 가 Admission 승인 임계(SeverityRank >= 3)에 걸리지 않고 통과했다. 게이트 임계값은 저장된 계약이므로 등급 번호는 바꾸지 않고Negligible을 Low 와 같은 랭크로 뒀다. ③ severity 카운트 맵 두 곳이 손으로 적은 키 목록 +m[sev].(int)+1이라 정규화가 표에 없는 등급을 하나라도 내보내면 nil 타입 단언 패닉(요약 API 500)이었다 — 새analyzer.SeverityLevels()에서 키를 만들게 했다. ④ kube-bench 는Controls[{id,text}] > tests[{section}] > results[]로 중첩되는데 수집기가 상속값을 먼저 채택해 가장 바깥 노드의 자유 문구가 모든 control 의 Section 에 눌러앉아1.1을 잃었다. ⑤scored는 JSON bool 인데 문자열로 읽어(strV(true)="") 언제나 scored 였고,BenchmarkVersion은 핸들러가 읽고 있는데 정규화기가 설정한 적이 없어 cis-1.7 과 cis-1.23 결과를 구분할 수 없었다. 검증: 신규 테스트 8개(analyzer 7 + proxy 1)를 고치기 전 코드에 되돌려 붙여 다섯 결함을 각각 지목하며 실패함을 확인했고(인식된 형식의 라벨·section 없는 평평한 문서의 기존 동작을 지키는 오탐 회귀 포함)go build ./...·go vet ./...·go test ./...전부 통과. 저장소 관례대로 AppVersion·changelog·docs 버전 마커를 v0.9.274 로 올렸다(release gate 테스트가 강제). -
보류 아이디어: ①
.github에 CI 워크플로 없음 — build/vet/test 게이트 추가 (가치 3 / 위험 1 / S) ② PSS Restricted 검사에 seccompProfile 항목이 없어RuntimeDefault/Localhost미설정 Pod 가 Restricted 로 남음 (가치 3 / 위험 2 / S) ③capacity.podRequestGPU는 nvidia 만,node_monitoring.podGPURequests는 amd·intel 도 세어 화면마다 GPU 요청량이 다름 (가치 2 / 위험 1 / S) ④kube.splitCommandLine이 빈 인자(sh -c "")를 버려 argv 가 밀림 (가치 2 / 위험 1 / S) ⑤podControllerOwned가 라벨 휴리스틱만 봐서ownerReferences로만 소유된 Pod 를 standalone 으로 판정 (가치 2 / 위험 1 / S) - 릴리즈: v0.9.274 (2026-09-07, run 2026-09-07-063049-Clustara-improve)
Invenqor
- 선택: 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)
ReSSO
- 선택: 자격증명을 대조하지 못한 것을 거절한 것으로 답하던 Client 인증 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공 (커밋 acac754)
- 요약: Token·Introspection·Revocation 셋은
authenticateOIDCClient하나를 공유하는데, 그것이 “조회를 완료하지 못한 것”과 “Secret이 맞지 않은 것”을 똑같이 401invalid_client로 답했다.invalid_client는 호출자의 자격증명에 관한 단언이라 그 답을 받은 RP에게는 재시도할 것이 없다 — 서버가 방금 그 Secret을 받지 않겠다고 말했기 때문이다. 그래서clients테이블 장애가 이 셋을 저하시킨 게 아니라 Realm의 모든 연동에 “너희 자격증명이 거절됐다”고 한꺼번에 통지했다. 로그인·userinfo·introspection에 이미 그은 그 구분이 여기에는 없었다. 장애가 끝난 뒤까지 남는 것은 그 다음 줄이다: 실패 버킷 둘(Client별·출발지 주소별)은 Secret 대입을 막으려고 있는 건데, 자격증명을 검사조차 못 한 요청이 그 둘에 실패로 기록됐다. 장애 중 스무 번이면 Client 버킷이 차고, 그때부터는 맞는 Secret에도 5분 창이 끝날 때까지 429가 나간다 — 저장소는 이미 복구된 뒤에도. 드러나는 것도 없었다: 여기서 401은 오래된 Secret이 하루 종일 만드는 평범한 답이라 요청 카운터와 접근 로그에는 한산한 시간대로 보였고, store 에러는 그 자리에서 버려졌다. 이제store.ErrNotFound만 옛 답을 유지한다(없는 Client·틀린 Secret·Secret을 보낸 Public Client·비활성 Client는 모두 호출자에 관한 사실이므로 그대로). 나머지는 500server_error+ 어느 조회가 멈췄는지 로그이고, 어느 버킷에도 실패를 적지 않는다. 그 결과resso_client_auth_failures_total은 실제로 대조해서 거절한 것만 세게 되어 이름이 늘 주장하던 뜻이 됐다. 검증: 새 연동 테스트가 두 조회를 차례로 없애며(테이블clientsRENAME →ClientByIdentifier실패, 컬럼secret_hashRENAME →VerifyClientSecret실패) 엔드포인트 셋을 모두 확인하고, 이어서 버킷 하나를 가득 채울 만큼(21회) 장애 중 요청을 보낸 뒤 맞는 Secret으로 토큰을 요청한다. 수정 전 코드에서 일곱 건 모두 실제로 실패함을 확인했다(401 invalid_client 여섯, 21번째 요청 429).make test전체 통과(exit 0) —go test -race ./...11개 패키지 ok, FAIL 0, 연동 테스트 SKIP 0건,go vet,golangci-lint(0 issues),govulncheck(0),npm run lint,npm run test(22파일/104테스트, 디스크 파일 수와 일치),npm run build. 빌드가 만든webui/dist/index.html변경은 되돌렸다.docs/operations.md의resso_client_auth_failures_total항목과docs/compatibility.md의 Client 인증 행에 401/429/500 구분을 적었다. - 보류 아이디어:
discovery·jwks가realmFromPath실패를 404realm_not_found로 답해 RP가 “issuer가 없다”로 읽고 캐시한다(가치 3/위험 1/S) /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) /oidcLogout이 hint의sub를 쿠키 세션과 대조하지 않는다(가치 2/위험 2/M)
Vendra
- 선택: 잘못 쓴 승인 규칙을 쓴 자리에서 고치고 끌 수 있게 하고, 그 수정을 생성이 통과한 것과 같은 검사에 묶기 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공 (commit d1337ad)
- 요약: 보류 아이디어 「워크플로 정의는 만든 뒤 화면에서 고칠 수 없음」을 집었는데, PATCH에 호출자를 붙이자 그 엔드포인트가 한 번도 검사한 적 없는 것이 드러났다. workflow_definitions.steps를 쓰는 문은 둘인데 하나만 지키고 있었다 — createWorkflow는 빈 목록과 이름 없는 단계를 거절하고 순번을 찍는데, updateWorkflow는 들어온 것을 그대로 저장했다(
[]도, 애초에 목록이 아닌 값도). 그 대가는 쓰는 자리에서 나지 않는다. 그 규칙에 걸리는 다음 제출에서 난다: 객체는 결재중으로 옮겨지고 결재는 설 단계 없이 열리므로, listApprovals는 current_step이 목록 끝을 넘은 인스턴스를 모든 승인함에서 빼고 workflowAction은 409 invalid_workflow로 답한다. 다시 상신할 수도 없다 — 두 번째 제출을 막는 것이 바로 그 열린 인스턴스다. 애플리케이션의 어떤 화면도 그 객체를 다시 움직일 수 없다. 그리고 규칙 자체가 write-once였다: 최소 금액에 0을 하나 덜 친 규칙은 고칠 수도 끌 수도 없이 활성인 채로 계속 라우팅했고, 답은 옆에 규칙을 하나 더 만드는 것뿐이라 matchingWorkflow가 둘 중 먼저 닿는 쪽을 집었다. validWorkflowSteps를 두 문 앞에 놓고, Workflow 패널이 이름·활성·최소 금액·Risk 조건·단계를 수정하게 했다 — PATCH는 conditions 전체를 갈아치우므로 폼이 보여주지 않는 조건(securityLevel 등)은 그대로 실어 보낸다. 업무 유형은 규칙이 등록된 이름이라 고정이고, 잘못 쓴 유형은 여기서 끄고 다시 쓴다. 검증: docker postgres:16-alpine으로 CI와 동일한 세 DSN을 새 DB에 걸고go test ./internal/... ./cmd/...전체 통과, gofmt·go vet 무결, 웹 테스트(12파일 51개)·tsc·eslint·빌드 통과. 가드는 넷 — 패키지를 파싱해 steps 컬럼을 쓰는 문을 전부 찾아 검사 호출을 요구하는 TestEveryStatementThatWritesApprovalStepsChecksThem, 거절이 상자를 지목하는지 읽는 TestAnApprovalRuleWithNoStepsIsRefused, 거절되는 수정→고쳐진 규칙이 승인함에 닿는 것→끈 규칙이 다음 제출을 통과시키는 것까지 API로 걷는 TestAnApprovalRuleCanBeCorrectedWhereItWasWritten, 그리고 수정 모달을 실제로 렌더링하는 admin-workflow.test.tsx 4개. updateWorkflow의 가드를 되돌리면 앞의 둘이 실패하는 것까지 확인했다. -
보류 아이디어: 업무 객체 status는 여전히 임의 문자열 — 유형별 어휘가 어디에도 정의돼 있지 않아 오타 하나가 집계에서 조용히 빠짐 (3/3/M) / 통합 테스트가 하나의 DB를 공유해 순서에 따라 산발적으로 깨짐 — 같은 실행에서 TestLoginLocksAccountAfterRepeatedFailures(가장 최근 세션을 ORDER BY created_at으로 고름)와 TestFormDraftsAreBoundedPerUser(가장 최근 초안이 축출됨)가 각각 한 번씩 실패했고 단독 실행은 통과 (3/1/M) / 전화번호·웹사이트는 길이만 볼 뿐 형식 검사가 없음 (3/1/M) / 업무 객체의 data jsonb 블롭에 어떤 검증도 없음 (3/2/M) / CI의 go job이 ./cmd/…를 빼놓아 Makefile·README와 불일치 (2/1/S)
- 릴리즈: v0.7.45 (2026-09-07, run 2026-09-07-074049-Vendra-improve)
ai-admin
- 선택: 중간에 끊긴 AI 채팅 응답을 완결된 답변처럼 끝내던 문제 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
chatCompletions는 공급자 응답의 200과 header를 먼저 내보낸 뒤 본문을 중계하는데, 연결이 답변 도중 끊기거나(upstream_read_failed) 공급자 제한 시간을 넘겨도(upstream_timeout) 중계 loop만 빠져나와 응답을 정상적으로 끝냈다. 스트리밍에서는 잘린 답변이 모델이 스스로 멈춘 완전한 답변처럼 보이고, 비스트리밍에서는 잘린 JSON이 200과 함께 전달되어 감사 이벤트에만 실패가 남았다. 중계 loop를relayChatBody로 분리해 전달 완료 여부와 중단 사유를 반환하게 하고, 끝까지 전달하지 못하면 감사에failure+reason을 남긴 뒤panic(http.ErrAbortHandler)로 응답을 중단하도록 했다(v1.2.5 감사 CSV 내보내기와 같은 방식이며recoverer·accessLog가 이미 이 신호를 처리한다). 프런트엔드도 함께 고쳐streamJson이 스트림 read 실패를 조용히 종료하지 않고 “AI 응답이 완료되기 전에 연결이 끊어졌습니다.”로 실패시키며, 플레이그라운드는 이미 받은 부분 답변을 지우지 않고 오류와 함께 남긴다. 다섯 가지 중계 결과(정상·upstream 읽기 실패·제한 시간 초과·요청 취소·클라이언트 쓰기 실패)를 단위 테스트로, 답변 도중 끊는 합성 공급자를 향한 요청이 완전한 응답으로 전달되지 않고 감사에upstream_read_failed로 남는지를 PostgreSQL 통합 테스트로 검증했다(수정 전 코드에서는 잘린 본문이 오류 없이 그대로 수신되어 실패). 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(63개)·npm run build도 모두 통과했다. 저장소 관례에 따라 VERSION을 1.2.13으로 올리고(1.2.10~1.2.12는 아직 병합되지 않은 형제 branch가 사용) CHANGELOG·README·docs·web 버전 메타데이터를 맞췄으며docs/api.md에 중단 계약을 명시했다(직전 릴리즈 커밋들과 동일하게internal/ui/dist는 재빌드하지 않음). -
보류 아이디어: DB 오류를 업무 규칙 충돌로 잘못 보고하는 handler들(
rotateKey의 409key_not_active,decideApproval의 409approval_not_pending,updateUser의 404user_not_found·400roles_invalid)을 로그인 분류(v1.2.10)와 같은 방식으로 구분 (가치 3 / 위험 2 / M) · 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) ·decideApproval이approval_action.comment에는 trim한 값을,approval_request.decision_comment에는 원문을 저장해 같은 결정의 두 기록이 달라짐 (가치 2 / 위험 1 / S) - 릴리즈: v1.2.10 (2026-09-07, run 2026-09-07-102543-ai-admin-approve)
- 릴리즈: v1.2.11 (2026-09-08, run 2026-09-08-150055-ai-admin-improve)
aiportal-front-admin
- 선택: 딥링크 query 변경에 화면이 반응하지 않는 문제 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 사용자·부서 화면이 거는
#/content?tab=access&userId=…와#/catalog?tab=shared&userId=…링크는 경로가 같고 query만 바뀌므로 vue-router가 화면을 다시 mount하지 않는데, 두 화면 모두queryValue()를 setup/onMounted에서만 읽어 이전 사용자의 조건과 결과가 그대로 남았다(사이드바로 같은 화면을 다시 눌러 query를 비워도 마찬가지).shared/routeQuery.ts에useRouteQuerySync()를 두어 진입과 query 변경이 같은 적용 함수를 부르게 하고, 화면이 쓰지 않는 query가 바뀌거나 다른 경로로 떠나는 중(route는 컴포넌트보다 먼저 바뀐다)이면 조회하지 않도록 진입 경로를 기억해 걸렀다. 세 세션째 “컴포넌트 테스트 환경 선행 필요”로 미뤄지던 항목이라jsdom·@vue/test-utils를 devDependency로 넣고 파일 단위// @vitest-environment jsdom으로 전역 설정 변경 없이 두 화면의 딥링크 재진입 테스트를 붙였다. 검증은npm run verify(typecheck + vitest 83개 통과, 기존 70개 + 신규 13개)와npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 query 감시를 꺼서 신규 컴포넌트 테스트 5개가 실제로 실패하는지 직접 확인했다. 커밋 c99bca9. - 보류 아이디어:
monitoringParser.toPod이 kubectl 스타일restarts("3 (5d ago)")에서 NaN을 표에 노출 (2/1/S) · AdvancedPolicyView가snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · PaginationBar가 범위 밖 페이지에서 “31–25 / 25”를 표시하고 이전 버튼이move()가드에 막힘 — 이제 컴포넌트 테스트로 검증 가능 (2/2/S) · CatalogView·DirectoryView가 두 탭이 공유하는loading·error를 써서 한쪽 탭의 오류가 다른 탭에 남음 (2/2/S) · 루트 앱의crypto-jslocal tarball 의존성 제거로 clean install 복구 (4/4/M)
aiportal-front
- 선택: 로그아웃 후 사용자 정보 잔존 및 탭 간 동기화 누락 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
src/storage/userStorage.js에서 6건을 고쳤다 — (1)readUser()가userRef.value를 반환하는데clearUser()는 저장소만 비우고userRef를 그대로 둬서 로그아웃(HeaderSetting/redirectToLogin/인터셉터 401) 이후에도 이전 사용자의 이름·이메일·is_admin이 계속 반환되던 문제, (2) 파일 상단 인라인if (!window._userStorageSyncInitialized)블록이 플래그를 먼저 세워서 아래setupUserStorageSync()가 항상 조기 반환 → 다른 탭 변경 시 현재 탭의 sessionStorage 가 전혀 갱신되지 않던 문제(다른 탭에서 로그아웃해도 이 탭을 새로고침하면readFromStorage가 sessionStorage 를 우선 읽어 로그아웃된 사용자가 되살아남), 리스너를 하나로 통합, (3) storage 이벤트의JSON.parse(e.newValue)미보호로 깨진 값에서 리스너가 예외를 던지던 문제, (4) 사용자 변경 통지가writeUser에서만 발생해clearUser·다른 탭 변경이 구독자(Sidemenu)에게 전달되지 않던 문제와 구독자 하나의 예외가 나머지 통지를 끊던 문제, (5)writeUser저장 형태를 순수 함수normalizeUserRecord로 분리, (6)Sidemenu.vue가addUserChangeListener의 해제 함수를 버려 마운트마다 리스너가 누적되던 누수(+ 로그아웃 통지에 반응해 앱 목록을 재조회하지 않도록readUser()가드). 검증은npm test(총 151건 통과, 신규 21건 — 수정 전 코드에서 11건 실패함을 확인)와npm run build:dev(빌드 성공)로 수행했다. - 보류 아이디어:
mitt이eventBus.js에서 import 되는데 package.json 직접 의존성에 없어 전이 의존성에 기대는 문제 (가치 3 / 위험 1 / S) ·globalLoading이 참조 카운트 없이 boolean 이라 병렬 요청 중 하나만 끝나도 스피너가 사라지는 문제 (가치 3 / 위험 3 / S) ·authStorage.readAuth가 localStorage 폴백 시 sessionStorage 를 복구하지 않고clearAuth도 통지가 없어 userStorage 와 동작이 어긋나는 문제 (가치 3 / 위험 2 / S) ·parseUtcToKstDate가 로컬 타임존이 KST 일 때 +9h 를 이중 적용하는 문제 정리 (가치 3 / 위험 4 / S) · 빌드 산출물 단일 청크 6.2MB 문제(manualChunks 코드 스플리팅) 개선 (가치 3 / 위험 3 / M)
aiportal-java
- 선택: DRM 복호화 실패가 성공으로 위장되어 암호문이 그대로 처리되는 문제 수정 (가치 4 / 위험 3 / 작업량 M)
- 결과: 성공
- 요약:
DrmUtil.decrypt는CreateDecryptFileDAC가 실패 코드를 돌려주면log.error만 남기고 암호화된 원본 파일encFile을 그대로 반환했습니다(52-58행,throw new Exception()이 주석 처리된 채). 호출부ToolsServiceImpl.decFileDrm은resultFile == null || !resultFile.exists()만 확인하는데encFile은 당연히 존재하므로 실패가 전혀 보이지 않았고, OCR 파이프라인이 암호문을 업스트림 파싱 API 로 보낸 뒤completeOcrAttempt로 작업을 “완료” 로 기록했습니다 — 사용자는 원인을 알 수 없는 깨진 추출 결과를 받습니다. 실패 시 코드와 파일명을 담은IOException을 던지고 복호화되지 않은 채 남는 복사본을 지우도록 고쳤습니다. 호출부에는 이미 실패 경로(parse()의filePaths.add(null)→status=400,FILE_PREPARE_FAILED)가 준비되어 있어 그대로 흘러갑니다.AthenaServiceImpl2005행은decFileDrm호출만try밖에 있어 파일 하나가 실패하면 요청 전체가 실패하므로, 같은 메소드 1931행과 동일하게try안으로 옮겨 해당 파일만 건너뛰게 했습니다. 함께,generatePath(62-79행)가File.createTempFile로 만든 유니크 이름을 버리고 원본 파일명을 그대로 쓰던 문제도 고쳤습니다 — 동명 파일이 동시에 처리되면decrypt의REPLACE_EXISTING복사가 서로 덮어썼습니다. 다운로드(preview→downFile) 시 쓰이는 원본 파일명은 유지해야 하므로 파일명을 바꾸는 대신 요청마다 유니크한 하위 디렉토리를 만들어 그 안에 두도록 했고, 쓰이지 않던ext지역 변수를 제거했습니다. 네이티브 DRM 라이브러리(SCSL.SLDsFile) 호출을 패키지 전용createDecryptFile(File, File)로 분리해 테스트에서 대체할 수 있게 한 뒤DrmUtilTest(6: 성공 코드 0 / 비암호화 파일 코드 -36 / 실패 시 예외(암호문 반환 회귀 가드) / 실패 시 암호문 복사본 삭제 / 동명 파일 격리 / 루트 디렉토리 자동 생성)를 추가했습니다. 검증은sh gradlew check build실행으로 했고 25개 테스트 클래스 119개 테스트 전부 통과했습니다. 커밋 12cfb27. (참고: 이 세션 환경에도 JDK 가 없고 JRE 만 있어 Temurin 21 을$HOME/jdks에 내려받아 빌드했습니다. 저장소에는 아무 변경도 하지 않았습니다. 또한 이 워크트리는 3ffa5ca 기준이라 직전 네 세션의 커밋이 아직 병합되지 않았습니다.) - 보류 아이디어:
AppController·ToolsController·WidgetController등이@RequestParam user_id를 그대로 신뢰 — 인증 사용자와 대조하지 않아 타인 데이터 접근 가능(IDOR).ToolsController.preview는@PathVariable id만 받고 매퍼에도 user_id 필터가 없어 id 증가만으로 남의 OCR 원본 파일을 받을 수 있다 (가치 5 / 위험 4 / 작업량 L)BoardServiceImpl.get()/LibraryServiceImpl.get()이 조회수 증가를 겸해 수정(PUT 당 +2)·삭제·등록 경로에서도view_cnt가 오른다 (가치 3 / 위험 2 / 작업량 S)SystemPolicyUtil.saveExtension/saveNote의!isEmpty()가드로 빈 배열 저장이 조용히 무시되는데 API 는 success 를 반환 —deleteExtension의 마지막 항목 삭제도 반영되지 않는다. 별개로 쓰이지 않는getFileLimit()이 읽는 키(fileLimit)와saveFileLimit()이 쓰는 키(simpleBotFileLimit)가 서로 다르다 (가치 3 / 위험 2 / 작업량 S)- 인증 필터의 요청 헤더 전량 INFO 로깅 축소 — 요청마다 모든 헤더를 남겨 운영 로그에서 실제 오류를 가림 (가치 3 / 위험 2 / 작업량 S)
- 남은 외부 응답 무검증 접근에
JsonNodeUtil확대 적용 —getAthenaCallApi반환값의(JsonNode)캐스팅,PythonApiCallUtil/RerankModelCallUtil응답 파싱 등. 유틸을 만든 커밋 602dfc3 이 main 에 병합된 뒤 진행할 것 (가치 3 / 위험 2 / 작업량 M)
← 대시보드 · Atom 피드 · 원본 데이터 runs.jsonl