Releases: hkjang/invenqor
Release list
Invenqor v0.2.43
Invenqor Server·Agent v0.2.43 릴리즈 노트
릴리즈 일자: 2026-10-05
호환 Agent: v0.2.43 (Linux·Windows)
이번 릴리즈는 결함 하나를 고칩니다. 자리는 자산 관계 API 두 곳
(POST /api/v1/assets/{assetId}/relations,
DELETE /api/v1/assets/{assetId}/relations/{relationId})이고, 문제는
openapi.yaml 이 format: uuid 로 선언한 모양이 아닌 id 를 받았을 때
PostgreSQL 과 SQLite fallback 이 서로 다른 답을 했다는 점입니다. 공개 계약은
POST 의 실패를 400 으로, DELETE 의 실패를 404 로 약속하는데, 운영 기본값인
PostgreSQL 에서는 계약에 없는 500 이 되거나 거절해야 할 요청으로 관계를 실제로
만들고 끊었습니다. v0.2.40 이 split 에서, v0.2.41 이 merge 에서, v0.2.42 가
{assetId} 단건 다섯 곳에서 닫은 것과 같은 결함이 관계 쓰기 두 곳에 남아
있었습니다. 이제 세 개의 id — 경로의 assetId, 본문의 target_asset_id,
경로의 relationId — 를 쿼리에 닿기 전에 정규형 UUID 로만 받습니다. 콘솔과
Agent 는 손대지 않았습니다.
1. 같은 요청에 방언마다 다른 답을 했습니다
createAssetRelation 은 chi.URLParam(request, "assetID") 와 본문의
target_asset_id 를 빈 값인지만 보고 그대로 INSERT INTO asset_relations 로
넘겼고, deleteAssetRelation 은 chi.URLParam(request, "relationID") 를 검사
없이 UPDATE asset_relations ... WHERE id=$2 로 넘겼습니다. 이 두 핸들러가 닿는
asset_relations.id · source_asset_id · target_asset_id 는 PostgreSQL 에서
모두 UUID 열이고 SQLite fallback 에서는 TEXT 라, 계약이 선언하지 않은 철자가
한쪽에서만 오류가 되거나 조용히 정규화되었습니다. 증상이 세 갈래였고 셋 다
나빴습니다.
DELETE는 UUID 가 아닌 글자와urn:uuid:표기에 500 을 답했습니다.
UUID 열 입력 변환이 SQLSTATE 22P02 로 거절해UPDATE자체가 실패했고,
s.internalError를 거쳐 이 경로가 선언하지 않은 500INTERNAL_ERROR가
되었습니다. 같은 입력에 SQLite fallback 은 TEXT 비교로 0행을 만나 404
RELATION_NOT_FOUND를 답했습니다.- 중괄호·하이픈 없는 32자 표기로
DELETE가 관계를 실제로 끊었습니다.
이쪽이 가장 나빴습니다. 두 표기 모두openapi.yaml이 선언하지 않은 모양인데
PostgreSQL 이 조용히 정규화해 실제 행을 찾아냈습니다 — 즉 404 여야 할
요청이valid_to를 써서 관계를 끝내고relation.delete감사 로그를 남겼고,
같은 요청에 SQLite fallback 은 404 였습니다.POST쪽도 같은 방향으로
어긋났습니다. 같은 표기로 보낸assetId와target_asset_id를 PostgreSQL 이
정규화해 외래 키를 만족시켜 400 이어야 할 요청으로 수동 관계를 201 로
만들었고, SQLite fallback 은 TEXT 외래 키가 맞는 자산을 못 찾아 409
RELATION_CONFLICT로 끝냈습니다. - 대문자 36자 UUID 는 방향이 반대였습니다. 대문자는 계약이 선언한 정규형인데
PostgreSQL 은 접어서 자산과 관계를 찾아 주고 SQLite 의 TEXT 비교는 접지 않아,
POST는 409 로,DELETE는 404 로 끝났습니다 — 계약대로 보낸 호출자가
fallback 에서만 실패했습니다.
감사 로그도 함께 어긋났습니다. createAssetRelation 은 호출자가 보낸 철자를 그대로
audit_logs.after_json 에 적었으므로, PostgreSQL 이 정규화해 저장한
asset_relations 행과 그 행을 설명하는 감사 기록이 다른 글자로 같은 자산을
가리켰습니다.
콘솔 경로와 API key 로 들어오는 /api/v1/external/... 경로가 같은 핸들러를
공유하므로, 두 입구 모두 같은 증상을 보였습니다.
2. 쿼리 전에 정규형 UUID 로만 받습니다
v0.2.40 이 splitAsset 을 위해 들여오고 v0.2.41 이 mergeAssets 에,
v0.2.42 가 assetIDParam 으로 단건 다섯 곳에 적용한 canonicalUUID 를 이 두
핸들러에도 씁니다.
POST는 세 id 를 모두 쓰기 전에 봅니다. 경로의assetId는 v0.2.42 의
assetIDParam으로, 본문의target_asset_id는canonicalUUID로 36자
하이픈 표기만 받습니다. 둘 중 하나가 아니면INSERT가 실행되기 전에
400INVALID_RELATION으로 돌아갑니다.DELETE는relationId를 봅니다. 정규형이 아니면UPDATE에 닿지 않고
404RELATION_NOT_FOUND로 돌아갑니다.- 응답 코드 집합을 넓히지 않았습니다.
POST는 이미 선언된 400 으로,
DELETE는 이미 선언된 404 로 거절합니다. 두 방언이 이제 같은 코드를 답하며,
그 코드는 모두openapi.yaml이 이미 적어 둔 것입니다. - 정규화한 값만 저장합니다.
INSERT와 감사 로그가 모두 소문자 36자 표기를
쓰므로, 대문자로 보낸 요청도 행과audit_logs.after_json이 같은 글자를
가리킵니다.relation.delete의resource_id도 정규형입니다. - 409 의 뜻이 좁아졌습니다. 이전에는 PostgreSQL 에서 잘못된 모양의 id 도
409RELATION_CONFLICT로 보일 수 있었습니다. 이제 409 는 없는 자산과
중복 관계라는 본래의 뜻만 남고, 모양 문제는 400 으로 갈라집니다. - 콘솔과 외부 API 가 함께 고쳐졌습니다. 두 경로가 같은 핸들러를 공유하므로
프로덕션 파일 한 개(server/internal/httpapi/assets.go)의 변경으로 두 입구가
같이 닫혔습니다. 라우팅은 손대지 않았습니다.
검증
- 회귀 테스트 4건을 더했습니다. 대역 없이 실제 라우터와 실제
Runtime을
지나 콘솔·외부 두 입구로 같은 요청을 보내고, 응답 코드만 보지 않고
asset_relations행 수와source_asset_id·target_asset_id·valid_to
행 상태,audit_logs행 수를 직접 세어 거절된 요청이 아무 것도 쓰지
않았음을 확인합니다. 같은DELETE를 두 번 보내 두 번째가valid_to를
덮어쓰지 않는 것과, 409 를 남겨 두어야 하는 세 경우(없는 source, 없는 target,
중복)가 여전히 409 인 것도 함께 봅니다. - 두 저장 엔진 모두에서 통과합니다.
go test ./...(SQLite fallback)와
scripts/test-postgres.sh(postgres:17-alpine) 전 패키지가 통과합니다. 기본
go test는 SQLite fallback 이라 이 결함을 보지 못합니다 — 방언 차이가 그대로
드러나는 자리라scripts/test-postgres.sh없이는 재현이 반쪽입니다. - Server.
go vet·gofmt -l빈 출력 ·go build가 통과합니다. 콘솔
정적 파일(server/internal/webui/dist)은 바뀌지 않았습니다. - OpenAPI.
@redocly/cli lint openapi.yaml이 통과합니다. - Agent. Rust 코드는 이번 릴리즈에서 바뀌지 않았습니다. 릴리즈 커밋에서
cargo fmt --all -- --check·cargo clippy --all-targets -D warnings·
cargo test --all-targets가 통과합니다.
호환성
- 데이터베이스 마이그레이션이 없습니다. 열을 더하거나 바꾸지 않고, 쿼리에
닿기 전에 id 의 모양을 한 번 더 볼 뿐입니다. - OpenAPI 문서가 바뀌지 않았습니다. 이미 적혀 있던
format: uuid와 두
경로의 응답 코드 집합을 구현이 뒤늦게 지킨 것이라, 계약 쪽은 손대지
않았습니다. - 정규형 UUID 를 쓰던 호출자에게는 아무 변화가 없습니다. 201·200·404·409
응답의 모양도, 권한과 CSRF 검증도 이전과 같습니다. 달라지는 것은 계약이
선언하지 않은 모양으로 id 를 보내던 요청뿐입니다 —DELETE에서 500 을 받던
호출자는 이제 404 를 받고, 중괄호나 하이픈 없는 표기로 관계가 만들어지거나
끊어지던 호출자는 이제 400 또는 404 를 받습니다. - 대문자 id 를 쓰던 호출자는 이제 두 방언에서 같게 동작합니다. 대문자는
계약이 선언한 정규형이라 계속 받아들이며, 소문자 정규형으로 접은 뒤 쓰므로
SQLite fallback 에서도 관계를 만들고 끊습니다. - 감사 로그의 글자가 달라질 수 있습니다. 대문자나 비정규형으로 보낸 요청의
audit_logs.after_json과resource_id가 이제 소문자 36자 표기로 적힙니다.
이미 쌓인 기록은 고치지 않습니다 — 과거 감사 기록을 글자 그대로 맞추어 찾는
외부 도구가 있다면 정규형으로 한 번 더 찾아야 합니다. - 관계 읽기와 자동 분류는 그대로입니다.
GET .../relations는 v0.2.42 가
정한 대로 동작하고,classify가 만드는 자동 관계와ingest·
softwarecatalog의 관계 정리는 호출자 입력을 받지 않아 이번 변경에 닿지
않습니다. merge·split·단건 다섯 곳의 계약은 그대로입니다. 그 일곱 곳은
v0.2.40 · v0.2.41 · v0.2.42 가 정한 대로 답합니다. 이번 변경은 관계 쓰기 두
곳에만 닿습니다.- 알려진 제한. 트랜잭션 안에서 오류를 보고하면 SQLite fallback 이
교착하는 문제는 그대로 남아 있습니다. 다른 핸들러의internalError는
v0.2.42 와 같습니다. 운영 기본값인 PostgreSQL 에서는 500 이 되며, fallback 을
쓰는 평가·개발 환경에만 해당합니다. - 콘솔이 바뀌지 않았습니다. 임베디드 정적 파일은 v0.2.42 와 같습니다.
- Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜은 이전과
같고 버전 문자열만 올라갑니다. 기존 Agent 는 서두르지 않아도 Server 와의
호환에 영향이 없습니다.
Invenqor v0.2.42
Invenqor Server·Agent v0.2.42 릴리즈 노트
릴리즈 일자: 2026-10-04
호환 Agent: v0.2.42 (Linux·Windows)
이번 릴리즈는 결함 하나를 고칩니다. 자리는 자산 단건 API 다섯 곳
(GET·PATCH·DELETE /api/v1/assets/{assetId},
POST /api/v1/assets/{assetId}/restore, GET .../history, GET .../relations)
이고, 문제는 openapi.yaml 이 format: uuid 로 선언한 모양이 아닌 id 를
받았을 때 PostgreSQL 과 SQLite fallback 이 서로 다른 답을 했다는 점입니다.
공개 계약은 앞의 네 곳의 실패를 404 하나로, history·relations 는 200
하나로만 약속하는데, 운영 기본값인 PostgreSQL 에서는 계약에 없는 500 이 되거나
거절해야 할 요청으로 자산을 실제로 고치고 삭제했습니다. v0.2.40 이 split
에서, v0.2.41 이 merge 에서 닫은 것과 같은 결함이 {assetId} 경로 파라미터를
쓰는 다섯 핸들러에 남아 있었습니다. 이제 경로 파라미터를 쿼리에 닿기 전에
정규형 UUID 로만 받습니다. 콘솔과 Agent 는 손대지 않았습니다.
1. 같은 요청에 방언마다 다른 답을 했습니다
getAsset · updateAsset · setAssetDeleted(삭제·복원) · assetHistory ·
assetRelations 는 chi.URLParam(request, "assetID") 가 돌려준 글자를 검사
없이 WHERE id = $1 로 넘겼습니다. 이 다섯이 닿는 assets.id ·
asset_changes.asset_id · asset_relations.source_asset_id ·
target_asset_id 는 PostgreSQL 에서 모두 UUID 열이고 SQLite fallback 에서는
TEXT 라, 계약이 선언하지 않은 철자가 한쪽에서만 오류가 되거나 조용히
정규화되었습니다. 증상이 세 갈래였고 셋 다 나빴습니다.
urn:uuid:표기와 UUID 가 아닌 글자는 PostgreSQL 에서 500 이었습니다.
UUID 열 입력 변환이 SQLSTATE 22P02 로 거절해 조회 자체가 실패했고,
internalError를 거쳐 계약에 없는 500INTERNAL_ERROR가 되었습니다.
같은 입력에 SQLite fallback 은 TEXT 비교로 0행을 만나 404 를,history·
relations는 빈 목록 200 을 답했습니다.- 중괄호·하이픈 없는 32자 표기는 PostgreSQL 에서 자산을 실제로 고쳤습니다.
이쪽이 가장 나빴습니다. 두 표기 모두openapi.yaml이 선언하지 않은 모양인데
PostgreSQL 이 조용히 정규화해 실제 행을 찾아냈습니다 — 즉 404 여야 할
요청으로PATCH가 자산의 이름을 바꾸고DELETE가 삭제 표시를 남겼습니다.
고치기 전postgres:17-alpine에서 직접 재현했고, 그 경로로asset_changes
6행이 쓰였습니다. 같은 요청에 SQLite fallback 은 모두 404 였습니다. - 대문자 36자 UUID 는 방향이 반대였습니다. 대문자는 계약이 선언한 정규형인데
PostgreSQL 은 접어서 자산을 찾아 주고 SQLite 의 TEXT 비교는 접지 않아 404 를
답했습니다 — 계약대로 보낸 호출자가 fallback 에서만 자산을 못 찾았습니다.
콘솔 경로와 API key 로 들어오는 /api/v1/external/... 경로가 같은 핸들러를
공유하므로, 두 입구 모두 같은 증상을 보였습니다.
2. 쿼리 전에 정규형 UUID 로만 받습니다
v0.2.40 이 splitAsset 을 위해 들여오고 v0.2.41 이 mergeAssets 에 적용한
canonicalUUID 를 이 다섯 핸들러에도 씁니다.
assetIDParam헬퍼를 두었습니다.chi.URLParam(request, "assetID")를
canonicalUUID에 통과시켜 36자 하이픈 표기만 받고, 아니면 거절을 알립니다.
다섯 핸들러가 첫 줄에서 이것을 부르므로 어떤 쿼리도 실행되기 전에
판정이 끝납니다.- 응답 코드 집합을 넓히지 않았습니다. 404 를 선언한
GET·PATCH·DELETE·restore는 404ASSET_NOT_FOUND로, 200 만 선언한
history·relations는 빈 목록 200 으로 즉시 돌아갑니다. 두 방언이 이제
같은 코드를 답하며, 그 코드는 모두openapi.yaml이 이미 적어 둔 것입니다. - 트랜잭션 밖에서 판정합니다. 다섯 모두
BeginTx전에 거절하므로 거절된
요청은assets·asset_changes·audit_logs어느 것도 건드리지 않고,
트랜잭션 안에서 오류를 보고하면 SQLite fallback 이 교착하는 구역으로도
들어가지 않습니다. - 콘솔과 외부 API 가 함께 고쳐졌습니다. 두 경로가 같은 핸들러를 공유하므로
프로덕션 파일 한 개(server/internal/httpapi/assets.go)의 변경으로 두 입구가
같이 닫혔습니다. 라우팅은 손대지 않았습니다.
검증
- 고치기 전에 실패를 먼저 확인했습니다.
scripts/test-postgres.sh로
PostgreSQL 에서urn:uuid:가 500INTERNAL_ERROR가 되는 것과, 중괄호·하이픈
없는 32자 표기로PATCH가 이름을 바꾸고DELETE가 삭제 표시를 남기는 것을
실제로 재현한 뒤 고쳤습니다. 기본go test는 SQLite fallback 이라 이 결함을
보지 못합니다 — 방언 차이가 그대로 드러나는 자리라scripts/test-postgres.sh
없이는 재현이 반쪽입니다. - 회귀 테스트 3건을 더했습니다. 대역 없이 실제 라우터와 실제
Runtime을
지나 다섯 경로를 부르고, 응답 코드만 보지 않고assets.status·
assets.deleted_at·assets.name행 상태와asset_changes행 수를 직접
세어 거절된 요청이 같은 자산에 아무 것도 하지 않았음을 확인합니다. - 두 저장 엔진 모두에서 통과합니다.
go test ./...(SQLite fallback)와
scripts/test-postgres.sh(postgres:17-alpine) 전 패키지가 통과합니다. - Server.
go vet·gofmt -l빈 출력 ·go build가 통과합니다. 콘솔
정적 파일(server/internal/webui/dist)은 바뀌지 않았습니다. - Agent. Rust 코드는 이번 릴리즈에서 바뀌지 않았습니다. 릴리즈 커밋에서
cargo fmt --all -- --check·cargo clippy --all-targets -D warnings·
cargo test --all-targets가 통과합니다.
호환성
- 데이터베이스 마이그레이션이 없습니다. 열을 더하거나 바꾸지 않고, 쿼리에
닿기 전에 경로 파라미터의 모양을 한 번 더 볼 뿐입니다. - OpenAPI 문서가 바뀌지 않았습니다. 이미 적혀 있던
format: uuid와 각
경로의 응답 코드 집합을 구현이 뒤늦게 지킨 것이라, 계약 쪽은 손대지
않았습니다. - 정규형 UUID 를 쓰던 호출자에게는 아무 변화가 없습니다. 200·404 응답의
모양도, 권한과 CSRF 검증도 이전과 같습니다. 달라지는 것은 계약이 선언하지
않은 모양으로 id 를 보내던 요청뿐입니다 — PostgreSQL 에서 500 을 받던
호출자는 이제 404(또는 빈 목록 200)를 받고, 중괄호나 하이픈 없는 표기로
자산이 고쳐지거나 삭제되던 호출자는 이제 404 를 받습니다. - 대문자 id 를 쓰던 호출자는 이제 두 방언에서 같게 동작합니다. 대문자는
계약이 선언한 정규형이라 계속 받아들이며, 소문자 정규형으로 접은 뒤 조회하므로
SQLite fallback 에서도 자산을 찾습니다. merge·split의 계약은 그대로입니다. 그 두 곳은 v0.2.40·v0.2.41 이
정한 대로 비정규형에 400 을 답합니다. 이번 변경은{assetId}경로 파라미터를
쓰는 다섯 핸들러에만 닿습니다.- 알려진 제한. 트랜잭션 안에서 오류를 보고하면 SQLite fallback 이
교착하는 문제는 그대로 남아 있습니다. 이번 수정은 트랜잭션을 열기 전에
거절하므로 이 다섯 경로의 입력 검증이 그 길로 들어가지 않게 되었을 뿐이고,
다른 핸들러의internalError는 v0.2.41 과 같습니다. 운영 기본값인
PostgreSQL 에서는 500 이 되며, fallback 을 쓰는 평가·개발 환경에만
해당합니다. - 콘솔이 바뀌지 않았습니다. 임베디드 정적 파일은 v0.2.41 과 같습니다.
- Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜은 이전과
같고 버전 문자열만 올라갑니다. 기존 Agent 는 서두르지 않아도 Server 와의
호환에 영향이 없습니다.
Invenqor v0.2.41
Invenqor Server·Agent v0.2.41 릴리즈 노트
릴리즈 일자: 2026-10-03
호환 Agent: v0.2.41 (Linux·Windows)
이번 릴리즈는 결함 하나를 고칩니다. 자리는 자산 병합 API
POST /api/v1/assets/merge 이고, 문제는 openapi.yaml 이 format: uuid 로
선언한 모양이 아닌 id 를 받았을 때 PostgreSQL 과 SQLite fallback 이 서로 다른
답을 했다는 점입니다. 공개 계약은 이 엔드포인트의 오류를 400 하나로만
약속하는데, 운영 기본값인 PostgreSQL 에서는 500 이 되거나 거절해야 할 요청을
200 으로 수행했습니다. v0.2.40 이 split 에서 닫은 것과 같은 결함이 merge
에 남아 있었습니다. 이제 primary_id 와 secondary_ids 의 모든 원소를
트랜잭션을 열기 전에 정규형 UUID 로만 받고, 아니면 아무 것도 쓰지 않은 채
400 으로 거절합니다. 콘솔과 Agent 는 손대지 않았습니다.
1. merge 가 같은 요청에 방언마다 다른 코드를 답했습니다
mergeAssets 는 primary_id 와 secondary_ids 를 uuid.Parse 모양 검사
(missingAssetID)만 거쳐 보낸 글자 그대로 SQL 로 넘겼습니다. assets.id 와
asset_sources.asset_id 는 PostgreSQL 에서 UUID 열이고 SQLite fallback 에서
TEXT 라, uuid.Parse 가 받아 주는 비정규형 철자는 한쪽에서만 조회 자체가
오류가 되거나 조용히 정규화되었습니다. 증상이 세 갈래였고 셋 다 나빴습니다.
urn:uuid:표기는 PostgreSQL 에서 500 이었습니다. UUID 열 입력 변환이
SQLSTATE 22P02 로 거절해 조회 자체가 실패했고,internalError를 거쳐
계약에 없는 500INTERNAL_ERROR가 되었습니다. 같은 입력에 SQLite
fallback 은 TEXT 비교로 0행을 만나 400 을 답했습니다.- 중괄호·하이픈 없는 32자 표기는 PostgreSQL 에서 병합을 수행했습니다.
이쪽이 더 나빴습니다. 두 표기 모두openapi.yaml이 선언하지 않은 모양인데
uuid.Parse는 통과시키고, PostgreSQL 은 조용히 정규화해 실제 행을
찾아냈습니다 — 즉 거절되어야 할 요청이 200 으로 성공하며asset_sources
를 옮기고assets.status='merged'와asset_changes·audit_logs를
남겼습니다. 같은 요청에 SQLite fallback 은 모두 400 이었습니다. - 대문자 UUID 는 병합 결과를 MCP 가 찾지 못하게 만들었습니다. 대문자는
계약이 선언한 정규형이고 PostgreSQL 은 접어서 같은 행을 찾지만, SQLite 의
TEXT 비교는 접지 않습니다. 더구나 보낸 대문자가 그대로
asset_changes.after_json과audit_logs에 들어가, 문자 일치로 묻는 MCP
merged_into조회(mcp.go:747·752, 두 방언 모두)가 병합된 자산을
못 찾았습니다 — v0.2.38 이 세운 안내가 이 경로에서만 조용히 깨져
있었습니다.
이제 canonicalUUID 가 36자 하이픈 표기만 받습니다.
- 모양 검사를 트랜잭션 전에 합니다.
decodeJSON직후·BeginTx전에
primary_id와secondary_ids각 원소를 확인하고, 아니면 400
INVALID_MERGE로 끝냅니다. 트랜잭션을 열기 전이므로assets·
asset_sources·asset_changes·audit_logs어느 것도 건드리지
않습니다. - 정규형만 아래로 흐릅니다. 존재 확인 쿼리, 자기 병합 스킵,
asset_sources이동,asset_changes의secondary_idsmetadata, 감사
기록이 모두 지역 변수primaryID·secondaryIDs만 읽습니다. 호출자가
보낸 철자가 저장소에 남는 자리는 더 이상 없으므로, 대문자로 병합해도 MCP
merged_into가 primary 를 가리킵니다. missingAssetID의 모양 검사를 지웠습니다. 호출자가 하나뿐이고 그
호출자가 호출 전에 정규화를 끝내므로 더는 닿지 않는 코드였습니다. 그 전제를
주석으로 적어 두었습니다.- 폴백의 동작은 바뀌지 않습니다. 오류 코드는 SQLite fallback 이 이미
답하던 400INVALID_MERGE를 그대로 두었으므로, 달라지는 것은 PostgreSQL
쪽의 500 과 잘못된 200 뿐입니다.
검증
- 고치기 전에 실패를 먼저 확인했습니다.
scripts/test-postgres.sh로
PostgreSQL 에서urn:uuid:가 500INTERNAL_ERROR로, 중괄호·하이픈 없는
32자 표기가 200 으로 병합을 수행하는 것, 대문자 철자가 PostgreSQL 에서만
병합되는 것을 실제로 재현한 뒤 고쳤습니다. SQLite fallback 에서는 같은
테스트 일부가 고치기 전에도 통과합니다 — 방언 차이가 그대로 드러나는
자리라,scripts/test-postgres.sh없이는 이 결함의 재현이 반쪽입니다. - 회귀 테스트 4건을 더했습니다. 대역 없이 실제 라우터로
POST /api/v1/assets/merge를 부릅니다 — 비정규형primary_id, 비정규형
secondary_ids(urn:uuid:· 중괄호 · 하이픈 없음 각각), 대문자 id 의
정규형 접힘, 그리고 대문자로 병합한 뒤 MCPasset_get이merged_into로
primary 를 가리키는 것. 응답 코드만 보지 않고assets.status·
asset_sources.asset_id·asset_changes.after_json·audit_logs행
수를 직접 셉니다. 거절 경로는 아무 것도 기록되지 않은 것까지
확인합니다. v0.2.39 가 추가한 존재 확인 테스트 3건은 그대로 통과합니다. - 두 저장 엔진 모두에서 통과합니다.
go test ./...(SQLite fallback)와
scripts/test-postgres.sh(postgres:17-alpine) 전 패키지가 통과합니다. - Server.
go vet·gofmt -l빈 출력 ·go build가 통과합니다. 콘솔
정적 파일(server/internal/webui/dist)은 바뀌지 않았습니다. - Agent. Rust 코드는 이번 릴리즈에서 바뀌지 않았습니다. 릴리즈 커밋에서
cargo fmt --all -- --check·cargo clippy --all-targets -D warnings·
cargo test --all-targets가 통과합니다.
호환성
- 데이터베이스 마이그레이션이 없습니다. 열을 더하거나 바꾸지 않고, 쿼리에
닿기 전에 입력의 모양을 한 번 더 볼 뿐입니다. - OpenAPI 문서가 바뀌지 않았습니다. 이미 적혀 있던
format: uuid와
"400": Primary or secondary assets missing을 구현이 뒤늦게 지킨 것이라,
계약 쪽은 손대지 않았습니다. 응답 코드 집합도 그대로입니다. - 정상 병합의 응답은 그대로입니다. 200 과 본문의 모양도,
assets.merge
권한과 CSRF 검증도 이전과 같습니다. 달라지는 것은 계약이 선언하지 않은
모양으로 id 를 보내던 요청뿐입니다 — PostgreSQL 에서 500 을 받던 호출자는
이제 400 을 받고, 중괄호나 하이픈 없는 표기로 병합이 되던 호출자는 이제
400 을 받습니다. 정규형 36자 UUID 를 보내던 호출자에게는 아무 변화가
없습니다. - 대문자 id 로 병합하던 호출자는 이제 두 방언에서 같게 동작합니다. 대문자는
계약이 선언한 정규형이라 계속 받아들이지만, 저장되는 글자가 소문자 정규형으로
바뀝니다. 이전에 PostgreSQL 에서 대문자로 병합해 둔 자산의
asset_changes.after_json과audit_logs에는 대문자가 남아 있고, 그
과거 행을 고치지는 않습니다 — MCPmerged_into안내는assets테이블을
보므로 과거 병합에도 영향이 없습니다. - 알려진 제한. 트랜잭션 안에서 오류를 보고하면 SQLite fallback 이
교착하는 문제는 그대로 남아 있습니다. 이번 수정은 트랜잭션을 열기 전에
거절하므로 merge 의 입력 검증 경로가 그 길로 들어가지 않게 되었을 뿐이고,
mergeAssets의 트랜잭션 안쪽과 ingest 등 다른 핸들러의internalError는
v0.2.40 과 같습니다. 운영 기본값인 PostgreSQL 에서는 500 이 되며, fallback 을
쓰는 평가·개발 환경에만 해당합니다. - 콘솔이 바뀌지 않았습니다. 임베디드 정적 파일은 v0.2.40 과 같습니다.
- Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜은 이전과
같고 버전 문자열만 올라갑니다. 기존 Agent 는 서두르지 않아도 Server 와의
호환에 영향이 없습니다.
Invenqor v0.2.40
Invenqor Server·Agent v0.2.40 릴리즈 노트
릴리즈 일자: 2026-10-01
호환 Agent: v0.2.40 (Linux·Windows)
이번 릴리즈는 결함 하나를 고칩니다. 자리는 자산 분리 API
POST /api/v1/assets/{assetId}/split 이고, 문제는 openapi.yaml 이
format: uuid 로 선언한 모양이 아닌 id 를 받았을 때 PostgreSQL 과 SQLite
fallback 이 서로 다른 답을 했다는 점입니다. 공개 계약은 이 엔드포인트의 오류를
400 하나로만 약속하는데, 운영 기본값인 PostgreSQL 에서는 500 이 되거나 거절해야
할 요청을 201 로 수행했습니다. 이제 경로 파라미터와 source_ids 의 모든 원소를
트랜잭션을 열기 전에 정규형 UUID 로만 받고, 아니면 아무 것도 쓰지 않은 채 400 으로
거절합니다. 콘솔과 Agent 는 손대지 않았습니다.
1. split 이 같은 요청에 방언마다 다른 코드를 답했습니다
splitAsset 은 {assetId} 경로 파라미터와 source_ids 를 검사 없이
UPDATE asset_sources SET asset_id=$1 WHERE id=$2 AND asset_id=$3 에 그대로
넣었습니다. asset_sources.id 와 asset_sources.asset_id 는 PostgreSQL 에서
UUID 열이고 SQLite fallback 에서 TEXT 라, 모양이 다른 id 는 한쪽에서만 조회
자체가 오류가 되거나 조용히 정규화되었습니다. 증상이 두 갈래였고 둘 다
나빴습니다.
- UUID 가 아닌 id 는 PostgreSQL 에서 500 이었습니다.
not-a-uuid같은 입력은
UUID 열 입력 변환이 SQLSTATE 22P02 로 거절해 쿼리 자체가 실패했고,
internalError를 거쳐 계약에 없는 500INTERNAL_ERROR가 되었습니다.
같은 입력에 SQLite fallback 은 TEXT 비교로 0행을 만나 400 을 답했습니다. uuid.Parse가 받아 주는 비정규 표기는 PostgreSQL 에서 분리를
수행했습니다. 이쪽이 더 나빴습니다.urn:uuid:접두, 중괄호 포장,
하이픈 없는 32자 표기는 모두openapi.yaml이 선언하지 않은 모양인데
uuid.Parse는 세 가지를 모두 통과시킵니다. PostgreSQL 은urn:표기만
22P02 로 거절하고 중괄호와 하이픈 없는 표기는 조용히 정규화해 실제 행을
찾아냈습니다 — 즉 거절되어야 할 요청이 201 로 성공하며asset_sources를
옮기고 새 자산과asset_changes·audit_logs를 남겼습니다. 같은 요청에
SQLite fallback 은 세 가지 모두 400 이었습니다.- 대문자 UUID 는 기록되는 글자가 방언마다 달랐습니다. 대문자는 계약이
선언한 정규형이고 PostgreSQL 은 접어서 같은 행을 찾지만, SQLite 의 TEXT
비교는 접지 않습니다. 보낸 글자가 그대로asset_changes.after_json과
감사 로그에 들어가 같은 분리가 저장소에 따라 다르게 적혔습니다.
이제 canonicalUUID 가 36자 하이픈 표기만 받습니다.
- 모양 검사를 트랜잭션 전에 합니다.
decodeJSON직후·BeginTx전에
{assetId}와source_ids각 원소를 확인하고, 아니면 각각 400
INVALID_SPLIT·INVALID_SOURCE로 끝냅니다. 트랜잭션을 열기 전이므로
assets·asset_sources·asset_changes·audit_logs어느 것도
건드리지 않습니다. uuid.Parse단독으로는 모양 검사가 아닙니다. 그 godoc 이 직접 그렇게
적어 둔 대로입니다.canonicalUUID는 파싱에 더해 길이 36자를 함께 보아
urn:uuid:· 중괄호 · 하이픈 없는 표기를 모두 거절합니다.- SQL 에는 정규형만 들어갑니다. 호출자가 보낸 글자가 아니라 파싱 결과를
소문자 정규형으로 되돌려 쓰므로, 대문자로 보낸 id 도 두 방언에서 같게
동작하고asset_changes.after_json에 정규형으로 적힙니다. - 폴백의 동작은 바뀌지 않습니다. 오류 코드는 SQLite fallback 이 이미
답하던 400INVALID_SPLIT·INVALID_SOURCE를 그대로 두었으므로, 달라지는
것은 PostgreSQL 쪽의 500 과 잘못된 201 뿐입니다.
검증
- 이 엔드포인트는 테스트가 하나도 없었습니다. 대역 없이 실제 라우터로
POST /api/v1/assets/{assetId}/split를 부르는 회귀 테스트 7건을
추가했습니다 — UUID 가 아닌assetId, UUID 가 아닌source_ids, 네 가지
비정규 표기(urn:uuid:· 중괄호 · 하이픈 없음 · UUID 아님)를assetId와
source_ids양쪽에 각각 적용, 대문자source_ids의 정규형 접힘, 정상 분리
1건(새 자산 · source 이동 ·asset_changes·audit_logs확인), 남의 자산
source 거절(기존 동작 보존). 거절 경로는 source 가 원래 자산에 그대로
남아 있는 것과 아무 것도 기록되지 않은 것까지 확인합니다. - 고치기 전에 실패를 먼저 확인했습니다. PostgreSQL 에서 UUID 가 아닌 두
입력이 500INTERNAL_ERROR로, 중괄호·하이픈 없는 표기가 201 로 분리를
수행하는 것을 확인한 뒤 고쳤습니다. SQLite fallback 에서는 같은 테스트 일부가
고치기 전에도 통과했습니다 — 방언 차이가 그대로 드러나는 자리라,
scripts/test-postgres.sh없이는 이 결함의 재현이 반쪽입니다. - 두 저장 엔진 모두에서 통과합니다.
go test ./...(SQLite fallback)와
scripts/test-postgres.sh(postgres:17-alpine) 전 패키지가 통과합니다. - Server.
go vet·gofmt -l빈 출력 ·go build가 통과합니다. 콘솔
정적 파일(server/internal/webui/dist)은 바뀌지 않았습니다. - Agent. Rust 코드는 이번 릴리즈에서 바뀌지 않았습니다. 릴리즈 커밋에서
cargo fmt --all -- --check·cargo clippy --all-targets -D warnings·
cargo test --all-targets가 통과합니다.
호환성
- 데이터베이스 마이그레이션이 없습니다. 열을 더하거나 바꾸지 않고, 쿼리에
닿기 전에 입력의 모양을 한 번 더 볼 뿐입니다. - OpenAPI 문서가 바뀌지 않았습니다. 이미 적혀 있던
format: uuid와
"400": Invalid split or source does not belong to the original asset을
구현이 뒤늦게 지킨 것이라, 계약 쪽은 손대지 않았습니다. - 정상 분리의 응답은 그대로입니다. 201 과
asset_id의 모양도,
assets.merge권한과 CSRF 검증도 이전과 같습니다. 달라지는 것은 계약이
선언하지 않은 모양으로 id 를 보내던 요청뿐입니다 — PostgreSQL 에서 500 을
받던 호출자는 이제 400 을 받고, 중괄호나 하이픈 없는 표기로 분리가 되던
호출자는 이제 400 을 받습니다. 정규형 36자 UUID 를 보내던 호출자에게는
아무 변화가 없습니다. - 알려진 제한. 트랜잭션 안에서 오류를 보고하면 SQLite fallback 이
교착하는 문제는 그대로 남아 있습니다. 이번 수정은 트랜잭션을 열기 전에
거절하므로 split 의 입력 검증 경로가 그 길로 들어가지 않게 되었을 뿐이고,
splitAsset의 트랜잭션 안쪽과 ingest 등 다른 핸들러의internalError는
v0.2.39 와 같습니다. 운영 기본값인 PostgreSQL 에서는 500 이 되며, fallback 을
쓰는 평가·개발 환경에만 해당합니다. - 콘솔이 바뀌지 않았습니다. 임베디드 정적 파일은 v0.2.39 와 같습니다.
- Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜은 이전과
같고 버전 문자열만 올라갑니다. 기존 Agent 는 서두르지 않아도 Server 와의
호환에 영향이 없습니다.
Invenqor v0.2.39
Invenqor Server·Agent v0.2.39 릴리즈 노트
릴리즈 일자: 2026-09-29
호환 Agent: v0.2.39 (Linux·Windows)
이번 릴리즈는 결함 하나를 고칩니다. 자리는 자산 병합 API
POST /api/v1/assets/merge 이고, 문제는 존재하지 않는 자산 UUID 를 받아도
그대로 병합을 진행했다는 점입니다. openapi.yaml 은 이 경우를 400 으로
공개 계약에 적어 두었지만 구현에는 존재 확인이 아예 없었습니다. 이제 트랜잭션을
연 직후 primary 와 모든 secondary 가 실제 자산인지 확인하고, 하나라도 없으면
아무 것도 쓰지 않고 400 INVALID_MERGE 로 거절합니다. 콘솔과 Agent 는
손대지 않았습니다.
1. 병합이 없는 자산을 받아도 성공했다고 답했습니다
mergeAssets 는 요청의 primary_id · secondary_ids 가 자산인지 한 번도
묻지 않고 곧바로 asset_sources 와 assets 를 갱신했습니다. 그래서 오타가 난
UUID, 이미 하드 삭제된 자산의 UUID, 다른 환경에서 복사해 온 UUID 가 들어오면
어느 쪽이냐에 따라 증상이 달랐고, 셋 다 나빴습니다.
- 없는 secondary 는 200 이었습니다. 갱신할 행이 없으니
UPDATE는 0행을
건드리고 끝났는데, 그 뒤로asset_changes(change_type='merged')와
audit_logs(action='asset.merge',result='success')에는 병합했다는
기록이 남았습니다. 감사 로그를 읽는 사람에게는 일어나지 않은 병합이
일어난 것으로 보였고, 호출자는 성공 응답을 받았습니다. - 없는 primary 는 PostgreSQL 에서 500 이었습니다.
asset_changes.asset_id
의 외래 키가 트랜잭션 도중에 거절했습니다 — 데이터가 깨지지는 않았지만,
계약이 약속한 400 대신 서버 오류였습니다. - SQLite fallback 에서는 응답이 아예 돌아오지 않았습니다. 위 500 을
보고하는internalError가 진단 레코드를 쓰려고 새 커넥션을 요구하는데,
열려 있는 트랜잭션이 fallback 의 유일한 커넥션(SetMaxOpenConns(1))을 쥐고
있어 서로를 기다렸습니다. 요청은 타임아웃까지 매달렸습니다.
이제 트랜잭션을 연 직후 양쪽을 모두 확인합니다.
- 하나라도 없으면 아무 것도 쓰지 않습니다. primary 와 모든 secondary 를
단건SELECT id FROM assets WHERE id=$1로 확인하고, 없는 것이 하나라도
있으면asset_sources·assets·asset_changes·audit_logs어느 것도
건드리지 않은 채 400INVALID_MERGE로 끝납니다. 거짓 감사 기록이 남지
않습니다. - 이미 병합된 자산을 다시 secondary 로 주는 동작은 그대로입니다. 확인은
행이 있는지만 묻고deleted_at이나status를 보지 않으므로, 체인 병합과
재병합은 이전과 똑같이 동작합니다. - UUID 가 아닌 id 도 같은 400 입니다.
assets.id는 PostgreSQL 에서 UUID
열, SQLite fallback 에서 TEXT 라 형식이 틀린 id 는 한쪽에서만 조회 자체가
오류가 됩니다. 조회 전에 모양을 먼저 보아 두 방언이 같은 입력에 같은 코드를
답합니다. - 오류 보고가 교착하지 않습니다. 확인 질의가 실패하면
internalError를
부르기 전에 트랜잭션을 먼저 되돌려 커넥션을 놓아 줍니다 — 위의 SQLite
무한 대기와 같은 길로 들어가지 않기 위해서입니다.
검증
- 실제 병합 경로로 테스트했습니다. 대역 없이 REST
POST /api/v1/assets/merge를 불러 없는 primary(400 · 자산 상태 불변),
없는 secondary 가 섞인 요청(400 · 정상 secondary 도 병합되지 않음 ·
asset_changes와audit_logs에 기록 없음), UUID 가 아닌 id(400) 를
확인합니다. 수정 전에는 두 번째가 200 으로, 첫 번째가 SQLite 에서 응답 없이
실패했습니다. - 두 저장 엔진 모두에서 통과합니다.
go test ./...(SQLite fallback)와
scripts/test-postgres.sh(postgres:17-alpine) 전 패키지가 통과합니다. - Server.
go vet·gofmt -l빈 출력 ·go build가 통과합니다. 콘솔
정적 파일(server/internal/webui/dist)은 바뀌지 않았습니다. - Agent. Rust 코드는 이번 릴리즈에서 바뀌지 않았습니다. 릴리즈 커밋에서
cargo fmt --all -- --check·cargo clippy --all-targets -D warnings·
cargo test --all-targets가 통과합니다.
호환성
- 데이터베이스 마이그레이션이 없습니다. 열을 더하거나 바꾸지 않고, 쓰기
전에 읽기 한 번을 더 할 뿐입니다. - OpenAPI 문서가 바뀌지 않았습니다. 이미 적혀 있던
"400": Primary or secondary assets missing을 구현이 뒤늦게 지킨 것이라, 계약 쪽은 손대지
않았습니다. - 정상 병합의 응답은 그대로입니다. 200 의 모양도,
assets.merge권한과
CSRF 검증도 이전과 같습니다. 달라지는 것은 이전에 200 이었던 잘못된
요청뿐이므로, 없는 UUID 를 보내던 호출자가 있었다면 이제 400 을 받습니다 —
그 200 은 아무 것도 병합하지 않은 200 이었습니다. - 알려진 제한. 트랜잭션 안에서 오류를 보고하면 SQLite fallback 이
교착하는 문제는 병합 경로에서만 막았습니다. 같은 모양이
POST /api/v1/assets/{assetId}/split과 ingest 등 트랜잭션 안에서
internalError를 부르는 다른 핸들러에 남아 있습니다. 운영 기본값인
PostgreSQL 에서는 500 이 되며, fallback 을 쓰는 평가·개발 환경에만
해당합니다. - 콘솔이 바뀌지 않았습니다. 임베디드 정적 파일은 v0.2.38 과 같습니다.
- Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜은 이전과
같고 버전 문자열만 올라갑니다. 기존 Agent 는 서두르지 않아도 Server 와의
호환에 영향이 없습니다.
Invenqor v0.2.38
Invenqor Server·Agent v0.2.38 릴리즈 노트
릴리즈 일자: 2026-09-20
호환 Agent: v0.2.38 (Linux·Windows)
이번 릴리즈는 결함 하나를 고칩니다. 자리는 MCP 도구 asset_get 이고, 문제는
콘솔에서 다른 자산으로 병합된 자산의 UUID 로 물으면 존재한 적 없는 UUID 와
글자까지 같은 asset not found 만 돌려주었다는 점입니다. 이제 병합된 자산은
merged_into 로 어느 자산(primary)으로 합쳐졌는지와 언제 합쳐졌는지를 답하므로,
모델은 그 UUID 로 다시 불러 지금의 자산에 닿을 수 있습니다. 콘솔과 Agent 는
손대지 않았습니다.
1. MCP asset_get 이 병합된 자산을 없는 자산과 같게 답했습니다
콘솔에서 자산 A 를 B 로 병합하면 A 는 status='merged' 가 되고 deleted_at 이
찍힙니다. REST GET /api/v1/assets/{id} 와 콘솔 상세 화면은 그 자산을 여전히
"병합됨" 으로 보여 주지만, MCP asset_get 은 deleted_at IS NULL 로만 걸러
asset not found 라고 답했습니다. 이전 대화, asset_search 결과, 관계 edge
에 남아 있던 A 의 UUID 로 다시 묻는 모델은 자산이 사라졌다고 결론냈고, B 로
가는 길은 어디에도 없었습니다.
이제 live 조회가 비면 그 UUID 가 병합된 자산인지 한 번 더 봅니다.
- 병합된 secondary 는 primary 를 안내합니다.
asset키 없이
{ "merged_into": "<primary UUID>", "merged_at": "<RFC 3339>", "message": "..." }
를 돌려줍니다.merged_into로asset_get을 다시 부르면 상세가 옵니다.
primary 는 예전 그대로asset으로 답합니다. - 한 단계만 안내합니다. A→B→C 로 두 번 병합되었으면 A 는 B 를 받고, B 를 다시
물으면 C 를 받습니다. 체인을 따라가지 않으므로 순환이 생길 수 없습니다. - 존재하지 않는 UUID 는 예전 그대로
asset not found입니다. 병합 표시가
없는 삭제 자산도 같습니다. - 조회 실패는 숨기지 않습니다. primary 는
asset_changes의merged변경
행(after_json.secondary_ids)에서 찾는데, 이 열은 PostgreSQL 에서 JSONB,
SQLite fallback 에서 TEXT 라 엔진마다 다른 JSON 연산(@>·json_each)으로
묻습니다. 그 문장이 실패하면 오류를 그대로 돌려주고asset not found로 접지
않습니다 — SQLite 테스트만 통과한 채 운영에서 조용히 틀리는 길을 막기
위해서입니다.
도구 설명(Description)에 병합 자산의 응답 모양을 적어 MCP 클라이언트가 도구
목록에서 바로 읽을 수 있게 했고, API·MCP 가이드의 도구 표 asset_get 행도 같은
내용으로 고쳤습니다.
검증
- 실제 병합 경로로 테스트했습니다. 대역 없이 REST
POST /api/v1/assets/merge
로 병합한 뒤asset_get을 불러 secondary(merged_into==primary ·
merged_atRFC 3339 ·asset없음), primary(그대로asset), 무작위 UUID
(asset not found), 두 단계 체인(한 단계만)을 확인합니다. 수정 전에는 두 테스트
모두asset not found로 실패했습니다. - 두 저장 엔진 모두에서 통과합니다.
go test ./...(SQLite fallback)와
scripts/test-postgres.sh(postgres:17-alpine) 전 패키지가 통과합니다. PostgreSQL
분기를 일부러 뒤집어 그쪽에서 빨강이 되는 것까지 확인해, 그 분기가 실제로
실행된다는 것을 보았습니다. - Server.
go vet·gofmt -l빈 출력 ·go build가 통과합니다. 콘솔
정적 파일(server/internal/webui/dist)은 바뀌지 않았습니다. - Agent. Rust 코드는 이번 릴리즈에서 바뀌지 않았습니다. 릴리즈 커밋에서
cargo fmt --all -- --check·cargo clippy --all-targets -D warnings·
cargo test --all-targets가 통과합니다.
호환성
- 데이터베이스 마이그레이션이 없습니다.
assets에 열을 더하지 않고 이미
기록되는asset_changes를 읽습니다. - REST API 응답 형식 변경이 없습니다. 바뀐 것은 MCP
asset_get의 병합 자산
응답뿐이고, 그 경우는 이전에 오류였으므로 성공 응답의 모양은 그대로입니다.
scope 는 그대로assets.read입니다. - 알려진 제한.
secondary_ids는 병합 요청이 보낸 글자 그대로 저장되므로,
콘솔이 아닌 호출자가 대문자 UUID 로 병합한 옛 자산은 조회가 찾지 못해 예전처럼
asset not found로 남습니다. 콘솔은 소문자를 보냅니다. - 콘솔이 바뀌지 않았습니다. 임베디드 정적 파일은 v0.2.37 과 같습니다.
- Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜은 이전과 같고
버전 문자열만 올라갑니다. 기존 Agent 는 서두르지 않아도 Server 와의 호환에
영향이 없습니다.
Invenqor v0.2.37
Invenqor Server·Agent v0.2.37 릴리즈 노트
릴리즈 일자: 2026-09-18
호환 Agent: v0.2.37 (Linux·Windows)
이번 릴리즈는 결함 하나를 고칩니다. 자리는 관리 콘솔의 Query DSL 화면이고,
v0.2.31 이 "알려진 제한"으로 남겨 둔 것의 뒷일입니다. v0.2.31 은
POST /api/v1/query/execute 에 offset 을 붙여 REST·API key 호출자가 한도 너머의
결과를 끝까지 순회할 수 있게 했지만, 콘솔은 여전히 첫 페이지만 보여 주고 조건을
좁히라고만 안내했습니다. 이제 콘솔에서도 같은 질의를 이어서 넘겨 볼 수 있습니다.
Server 는 손대지 않았습니다.
1. 콘솔의 Query DSL 화면이 첫 페이지 너머로 갈 수 없었습니다
Query DSL 화면은 실행 응답에서 items 와 truncated 만 읽었습니다. 조건에 맞는
자산이 limit(기본 100, 최대 500) 보다 많으면 제목이 "결과 100건 이상" 이 되고
"조건을 좁히거나 limit 을 올려 다시 실행하십시오" 라는 안내가 붙을 뿐, 나머지
자산으로 가는 길이 화면에는 없었습니다. 1,200대가 조용해진 인벤토리에서
last_seen_at < "now - 720h" 를 실행하면 화면은 100대를 보여 주었고, 나머지
1,100대는 콘솔로는 닿을 수 없었습니다. Server 는 v0.2.31 부터 total ·
offset · has_more · next_offset 을 이미 돌려주고 있었으므로, 응답에 있는
것을 화면이 쓰지 않은 것입니다.
이제 결과 패널이 Server 가 말한 그대로를 그립니다.
- 제목은 맞은 수입니다.
결과 N건의 N 은 이번 페이지의 행 수가 아니라 조건에
맞는 자산 전체 수(total)입니다. 오른쪽에는1–100 표시 · limit 100처럼 지금
표에 있는 범위와 한 번에 읽는 최대 건수가 붙습니다. - 이전·다음으로 이어서 읽습니다. 한도 너머에 행이 있으면 자산 목록과 같은
이전 · 다음 버튼이 표 아래에 나타나고, 같은 표현식을 Server 가 되돌려
준offset에서 이어서 실행합니다. 다음 페이지는offset + 표시 건수
(=next_offset)이고, 요청값이 아니라 Server 가 실제로 읽은 위치를 씁니다. - 질의 실행은 언제나 첫 페이지부터입니다. 표현식이나
limit을 바꾸고
질의 실행 을 누르면offset0 에서 다시 시작합니다. - 끝을 지난 페이지와 0건을 구분합니다. 두 번 누르는 사이에 자산이 지워지거나
병합되어 페이지가 비면 "이 페이지에는 자산이 없습니다" 로 안내하고 이전 으로
돌아갈 수 있습니다. 조건에 맞는 자산이 처음부터 없으면 예전처럼 "조건에 맞는
자산이 없습니다" 입니다. - 거부된 질의는 건수를 보이지 않습니다. 구문 오류나 권한 거부로 실행이
실패하면 제목은 "결과" 로만 남고 이전 결과의 수가 남아 있지 않습니다.
사용자 가이드 12.5 와 Server 설치 가이드 19.4 의 "이 화면에는 다음 페이지가
없으므로" 라는 안내를 고쳤고, query-result.png 를 새 화면으로 다시 찍었습니다.
검증
- 렌더 단위 테스트. 결과 패널을
QueryResultPanel로 떼어 브라우저 없이
Server 응답만으로 그려지는 글자와 버튼을 고정했습니다 — 첫 실행 전, 한 페이지에
다 들어가는 결과, 한도 너머 1,200건의 첫 페이지, 마지막 페이지의 범위와 버튼
상태, 끝을 지난 빈 페이지와 0건의 구분, 거부된 실행의 6개입니다. 콘솔의 vitest
는 150개 통과합니다. - 실제 Server 에서 걸어 보았습니다. SQLite fallback 의 Server 에 자산 7개를
만들고 재빌드한 임베디드 콘솔을 headless Chrome 으로 열어limit3 으로
1→2→3 페이지, 이전, 재실행, 1 페이지 삭제 뒤 끝을 지난 페이지, 0건, 거부까지
화면 글자와 버튼의 disabled 상태를 확인했습니다. - Server·콘솔.
npm run build의 산출물이server/internal/webui/dist와
같고(재빌드해 diff 없음),tsc -b·go test ./...·go vet·gofmt·
go build가 통과합니다. - Agent. Rust 코드는 이번 릴리즈에서 바뀌지 않았습니다. 릴리즈 커밋에서
cargo fmt --all -- --check·cargo clippy --all-targets -D warnings·
cargo test --all-targets가 통과합니다.
호환성
- 데이터베이스 마이그레이션이 없습니다.
- API 응답 형식 변경이 없습니다.
POST /api/v1/query/execute의 입력과 응답은
v0.2.31 그대로이고, 콘솔이 이미 있던offset·total·has_more를 쓰기
시작했을 뿐입니다. REST 의 다른 엔드포인트, MCP 도구, scope 는 바뀌지 않았습니다. - Server 코드가 바뀌지 않았습니다. 바뀐 것은 Server 에 내장된 콘솔 정적
파일뿐이므로, 새 이미지로 바꾸면 화면이 곧바로 새 동작을 합니다. - Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜은 이전과 같고
버전 문자열만 올라갑니다. 기존 Agent 는 서두르지 않아도 Server 와의 호환에
영향이 없습니다.
Invenqor v0.2.36
Invenqor Server·Agent v0.2.36 릴리즈 노트
릴리즈 일자: 2026-09-17
호환 Agent: v0.2.36 (Linux·Windows)
이번 릴리즈는 제품 동작을 바꾸지 않습니다. 바뀐 것은 Agent 가 HTTPS 를 말할 때
쓰는 TLS 라이브러리의 버전 하나입니다. Agent 는 Server 로 나가는 모든 전송에
rustls 를 쓰는데, 지금까지 고정되어 있던 rustls 0.23.42 에 2026-09-14 공개된
권고 RUSTSEC-2026-0285 가 닿습니다. 권고가 닫히는 가장 낮은 버전인 0.23.45
로 올렸고, 같은 감사에서 yanked 로 경고된 crate 하나도 함께 바꿨습니다.
Cargo.toml 은 그대로이고 lockfile 만 바뀝니다.
1. Agent 의 TLS 라이브러리에 권고가 닿아 있었습니다
cargo audit 가 reqwest 아래의 rustls 0.23.42 에서 RUSTSEC-2026-0285 를
찾았습니다 — TLS 1.3 핸드셰이크 메시지가 암호화 단계의 경계를 넘어 받아들여지는
문제로, 심각도는 5.3(medium)입니다. 권고는 트리를 건드린 어떤 변경과도 무관하게
새로 공개된 것이었고, 그 사실은 무관한 PR 의 dependency-audit 작업이 같은
이유로 두 번 실패하면서 드러났습니다. 이 저장소는 그 실패를 그대로 두지 않고
고치거나, --ignore 와 사유를 남기거나 둘 중 하나를 하기로 정해 두었고,
이번에는 고쳤습니다.
| crate | 전 | 후 | 이유 |
|---|---|---|---|
rustls |
0.23.42 | 0.23.45 | RUSTSEC-2026-0285 를 닫는 가장 낮은 버전 |
rustls-webpki |
0.103.13 | 0.103.15 | rustls 0.23.45 가 함께 끌어옴 |
chacha20 |
0.10.1 | 0.10.2 | 0.10.1 이 yanked 되어 감사에 경고로 남음 |
세 crate 모두 crates.io 가 기록한 rust_version 이 1.85 이하이므로,
rust-toolchain.toml 의 1.85.0 은 그대로이고 오래된 배포판을 위한 빌드
하한도 움직이지 않습니다. cargo audit 는 이제 취약점 0, 경고 0 을 보고합니다.
검증
- 의존성 감사.
cargo audit(0.22.1) 취약점 0·경고 0. - Agent.
cargo fmt --all -- --check,cargo clippy --all-targets -D warnings,
cargo test --all-targets105개 통과. 호스트와x86_64-unknown-linux-musl
(rust:1.85-alpine) 에서--locked --release정적 빌드가 성공하고--version
이 실행됩니다. - Server·콘솔. Go 와 콘솔 코드는 이번 릴리즈에서 바뀌지 않았습니다. 릴리즈
커밋에서go test ./...가 SQLite fallback 과 실제 PostgreSQL
(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과하고,go vet·gofmt·
go build와 콘솔의 vitest 가 통과합니다.
호환성
- 데이터베이스 마이그레이션이 없습니다.
- API 응답 형식 변경이 없습니다. REST API, Query DSL, MCP 도구는 이전과
같습니다. - Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜, mTLS·사설 CA
처리는 이전과 같고, TLS 라이브러리의 패치 버전만 올라갑니다. 기존 Agent 는
자동 업데이트 또는 패키지 재설치로 v0.2.36 으로 바꿀 수 있으며, 서두르지 않아도
Server 와의 호환에는 영향이 없습니다. - 소스에서 빌드하는 경우
cargo build --locked가 새 lockfile 을 그대로 씁니다.
Rust 1.85.0 이면 충분합니다.
Invenqor v0.2.35
Invenqor Server·Agent v0.2.35 릴리즈 노트
릴리즈 일자: 2026-09-14
호환 Agent: v0.2.35 (Linux·Windows)
이번 릴리즈는 기능 하나를 더하고 배포 묶음의 이름을 정리합니다. 기능의 자리는
메일 알림이고, 문제는 계정이 잠기거나, 관리자가 계정을 만들어 주거나, Agent
배포가 멈췄을 때 그것을 기다리는 사람에게 알려 주는 길이 없었다는 점입니다.
이제 설정 → 메일 알림에서 사내 SMTP 릴레이를 적고 켜면, 그 네 가지 일이 생길
때마다 기다리는 사람에게만 메일이 갑니다. 발송은 배경에서 이루어져 요청을 붙들지
않고, 시도마다 기록이 남으며, 기본값은 꺼짐입니다. 폐쇄망 배포 묶음은 파일과
이미지 이름이 릴리즈 태그와 같은 규칙(invenqor-v0.2.35.tar.gz ·
invenqor:v0.2.35)을 따르고, 체크섬을 확인하고 적재하는 스크립트가 함께 나갑니다.
1. 계정과 배포를 기다리는 사람이 화면을 새로고침해야 했습니다
로그인 실패가 5회 쌓여 계정이 잠기면 계정 주인은 그 사실을 다음 로그인 시도에서야
알았고, 관리자가 잠금을 풀어 주어도 마찬가지였습니다. 관리자가 로컬 계정을 만들어
주면 새 사용자는 누군가 따로 알려 주기를 기다렸고, 운영자가 Agent 배포를 0% 로
멈추면 다른 super_admin 은 화면을 열어 보기 전에는 몰랐습니다.
설정 → 메일 알림(#/settings/mail)이 이것을 맡습니다.
- 설정은 공용 DB 의
settings표에 사내 표준과 같은mail.*키로 저장되어 모든
Pod 에 즉시 적용되고 재기동이 필요 없습니다. 릴레이 주소·포트(기본25)·보안
방식(auto·none·starttls·tls)·보내는 사람·콘솔 주소·제한 시간을
적습니다. 인증은 선택 사항입니다 — 사용자 이름이 비어 있으면 시도하지 않고,
있으면 릴레이가 알리는 PLAIN·LOGIN·CRAM-MD5 중 하나를 씁니다. - 이벤트는 넷입니다. 계정 잠김(계정 주인과 모든
super_admin), 잠금 해제(계정
주인), 계정 생성(새 사용자, 비밀번호는 담지 않음), Agent 배포 중단(멈춘 운영자를
뺀super_admin). 각각 따로 끌 수 있습니다. 자기가 한 일은 자기에게 보내지
않고, 한 작업에서 한 사람에게는 한 통만 갑니다. 받을 주소는 사용자 관리의 메일
주소를 그대로 쓰며, 주소가 없거나 비활성인 계정은 조용히 건너뜁니다. - 켜려면 릴레이 주소와 보내는 주소가 있어야 저장되고, 잘못된 값은
400으로
거절됩니다. 꺼져 있으면 아무것도 보내지 않고 아무 행도 만들지 않습니다.
2. 릴레이가 죽어 있어도 요청은 즉시 끝납니다
메일은 요청 처리와 분리된 배경에서 보냅니다. 릴레이가 느리거나 포트가 닫혀
있어도 잠금 해제나 계정 생성 요청은 평소처럼 200 으로 즉시 끝나고, 실패는 그
요청의 실패가 아니라 발송 기록의 실패로 남습니다. 한 번 실패하면 2초 뒤 한 번
더 시도하고, 그래도 안 되면 failed 로 기록합니다.
발송 기록은 시도마다 남습니다 — 언제, 어떤 이벤트로, 누구에게, 제목이
무엇이었고, sent 인지 failed 인지와 그 이유. 실패만 남기면 "안 왔다"는 문의에
답할 수 없어 성공도 함께 적습니다. 본문은 담지 않고, 기록은 90일 뒤 지워집니다.
릴레이 설정은 한 번에 맞는 일이 드물어 같은 화면에 시험 발송을 두었습니다.
받는 주소를 비우면 내 계정의 주소로 저장된 설정 그대로 한 통을 보내고, 릴레이가
받으면 200, 거절하거나 응답이 없으면 502 와 함께 어느 SMTP 단계에서 무엇
때문에 막혔는지(SMTP connect … connection refused, RCPT TO failed: 550 …,
the relay does not offer AUTH …)를 그 자리에서 보여 줍니다. 저장하지 않은 변경은
시험에 반영되지 않으므로 단추가 잠깁니다.
3. 비밀번호는 봉인되어 저장되고 API 가 돌려주지 않습니다
mail.password 는 Keycloak client secret 과 같은 마스터 키로 봉인되어 settings
표에 secret=TRUE 로 저장됩니다. GET /api/v1/admin/settings/mail 과 화면에는
password_configured 로 "설정됨"만 보이고, 바꿀 때만 새 값을 보냅니다(빈 문자열은
지우기, 필드를 빼면 유지). 범용 설정 API(PATCH /api/v1/admin/settings)는 이 키를
거절하므로 평문 행이 생길 길이 없고, Server 로그와 감사 기록에도 남지 않습니다.
변경은 감사 기록에 settings.mail.update(비밀번호를 뺀 값)와 settings.mail.test
로 남습니다.
새 경로는 openapi.yaml 에 반영되었습니다 — GET·PATCH /api/v1/admin/settings/mail,
POST /api/v1/admin/settings/mail/test, GET /api/v1/admin/mail/deliveries. 관리자
가이드 21장이 설정 항목, 이벤트, 시험 발송과 발송 기록, API 를 설명합니다.
4. 폐쇄망 묶음의 이름이 릴리즈 태그와 같은 규칙을 따릅니다
릴리즈 태그는 v0.2.34 인데 Server image 묶음은 invenqor-0.2.34.tar.gz, 이미지는
invenqor-server:0.2.34 였습니다. 태그에서 내려받을 주소를 유추하면 어긋났고, 다른
저장소들은 모두 <이름>-v<버전>.tar.gz 를 씁니다. 이번 릴리즈부터 다음과 같습니다.
| 전 | 후 |
|---|---|
invenqor-0.2.34.tar.gz |
invenqor-v0.2.35.tar.gz |
invenqor-server:0.2.34 |
invenqor:v0.2.35 |
컨테이너 안 경로(/var/lib/invenqor-server), 바이너리 이름, GHCR 경로
(ghcr.io/hkjang/invenqor-server)는 그대로입니다. 이미지에는 버전·커밋·빌드 시각이
ldflags 와 OCI 라벨로 새겨져, 떠 있는 컨테이너가 더는 Commit unknown 을
알리지 않습니다.
묶음을 받는 쪽을 위해 네 파일이 함께 나갑니다 — 체크섬을 확인한 뒤 docker load
하는 load-invenqor-0.2.35.sh 와 .ps1, 버전이 박힌 compose.invenqor-0.2.35.yaml,
그리고 비어 있으면 기동이 멈추는 POSTGRES_PASSWORD 를 어떻게 만드는지 보여 주는
invenqor-0.2.35.env.example.
검증
- Server. 설정 저장·조회·비밀번호 봉인과
password_configured, 빈 문자열로
지우기, 잘못된 값의400, 꺼져 있을 때 무발송, 켠 뒤 계정 생성·5회 실패 잠김·
해제 각각이 기다리는 사람에게만 가는 것, 이벤트 스위치 끄기, 닫힌 포트의
릴레이로도 해제 요청이200으로 즉시 끝나고failed가 2회 시도로 기록되는
것, 시험 발송의502와 자기 주소 기본값을internal/mail과internal/httpapi
의 테스트로 고정했습니다. 실제 사내 릴레이는 없어 테스트용 SMTP 서버로 도착을
확인했습니다. - 콘솔. 메일 알림 화면의 렌더링과 설정 하위 화면 이동을 vitest 로 고정했습니다.
- 저장 모드 양쪽에서.
go test ./...가 SQLite fallback 과 실제
PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과하고,
go vet·gofmt·go build가 통과합니다. 콘솔의 vitest 144개가 통과하고,
OpenAPI 경로 대조 테스트가 통과하며, 체크인된server/internal/webui/dist는
React 빌드 결과와 같습니다. - 새 화면의 캡처는 가이드에 싣지 않았습니다. 캡처 스크립트·PNG·가이드 세 곳을
대조하는 테스트가 있어 다음 릴리즈의 과제로 남깁니다.
호환성
- 데이터베이스 마이그레이션 009 가
mail_deliveries표를 만듭니다. 기동 시
자동으로 적용되며, 멀티 Pod 에서는 전과 같이 PostgreSQL advisory lock 아래에서
한 Pod 만 적용하므로 기동 순서를 맞출 필요가 없습니다. - 기본값은 꺼짐이고, 꺼져 있으면 동작 변경이 없습니다. 계정 잠김·해제·생성과
Agent 배포 중단의 응답과 감사 기록은 이전과 같습니다. 설정 화면에 메일 알림
하위 화면 하나가 더해질 뿐입니다. - 폐쇄망 묶음의 파일·이미지 이름이 바뀌었습니다. 내려받기 절차나
docker load
뒤의 이미지 참조를 자동화해 둔 곳은invenqor-v<버전>.tar.gz와
invenqor:v<버전>으로 고쳐야 합니다.compose.offline.yaml과 설치 가이드는
새 이름으로 갱신되었습니다. 이미 만들어진 볼륨과 상태 디렉터리는 그대로 씁니다. - 기존 REST API, Query DSL, MCP 도구는 이번 릴리즈에서 바뀌지 않았습니다. 새 관리
경로 네 개가 더해질 뿐입니다. - Agent(Rust) 동작은 이번 릴리즈에서 바뀌지 않았습니다.
Invenqor v0.2.34
Invenqor Server·Agent v0.2.34 릴리즈 노트
릴리즈 일자: 2026-09-13
호환 Agent: v0.2.34 (Linux·Windows)
이번 릴리즈는 기능 하나를 더합니다. 자리는 Keycloak 로그인이고, 문제는
Keycloak 에 이미 로그인한 사용자도 콘솔을 열 때마다 로그인 화면을 보고
Keycloak으로 계속 을 눌러야 했다는 점입니다. 이제 설정 → Keycloak 의
Keycloak 세션이 있으면 자동 로그인(auto_login)을 켜면, 콘솔은 로그인 화면을
그리기 전에 OIDC prompt=none 으로 한 번 조용히 시도하고, 세션이 있으면 사용자를
열었던 자리로 바로 들여보냅니다. 세션이 없으면 로그인 화면이 한 번 보이고 다시
시도하지 않습니다. 기본값은 꺼짐입니다.
1. Keycloak 에 로그인한 사용자도 매번 로그인 화면을 거쳤습니다
콘솔은 /api/v1/auth/me 가 세션 없음을 답하면 곧바로 로그인 화면을 그렸습니다.
Keycloak 쪽에 살아 있는 세션이 있어도 그것을 물어볼 길이 없었으므로, 사내 포털에서
링크를 타고 들어온 사용자는 화면 하나를 더 거치고 버튼 하나를 더 눌러야 했고,
북마크한 깊은 주소는 로그인 뒤 / 로 돌아갔습니다.
이제 auto_login 이 켜져 있으면 다음 순서로 동작합니다.
- 콘솔은
/api/v1/auth/me와/api/v1/auth/methods의 답을 둘 다 기다립니다.
세션이 없고keycloak_auto_login이 참이면 로그인 화면을 그리지 않고
/api/v1/auth/keycloak/start?prompt=none&return_to=<현재 주소>로 최상위
이동합니다. 숨은 iframe 이 아니므로 서드파티 쿠키 차단이나 Keycloak 의 프레임
정책과 무관합니다. - Keycloak 은
prompt=none에 화면을 그리지 않습니다. 세션이 있으면 인가 코드가
바로 돌아와 평소 로그인 흐름을 타고, 사용자는return_to로 돌아갑니다.
return_to는/로 시작하고//로 시작하지 않는 같은 오리진 경로만 받으며
그 밖의 값은/가 됩니다. - 세션이 없으면
error=login_required로 돌아옵니다. 이것은 실패가 아니라 "로그인
안 되어 있음" 이라는 평범한 대답이므로 Server 는KEYCLOAK_PROVIDER_REJECTED로
기록하지 않고/?sso=none으로 보내 로그인 화면을 보여 줍니다.
로그인 화면은 두 답이 모두 오기 전에는 그려지지 않고, Keycloak 으로 떠나는 동안에도
그려지지 않습니다. 화면이 잠깐 보였다가 사라지는 깜빡임이 이 기능이 없애려는
바로 그것이기 때문입니다.
2. 거절 뒤에 다시 시도하면 무한 루프이므로 세 겹으로 막습니다
prompt=none 의 어려움은 시도가 아니라 거절에 반응하지 않는 것입니다. 거절
뒤에 한 번 더 시도하면 브라우저는 Keycloak 과 콘솔 사이를 끝없이 오갑니다.
| 장치 | 동작 |
|---|---|
| 한 탭 세션에 한 번 | 시도 여부를 sessionStorage 에 남깁니다. 거절 뒤 새로고침해도 다시 시도하지 않고, 새 탭은 다시 시도합니다. |
| 로그아웃 뒤 억제 | 콘솔에서 로그아웃하면 억제 표시를 남깁니다. 로그아웃 직후 조용히 다시 로그인되면 로그아웃이 고장 난 것처럼 보이기 때문입니다. 다시 세션이 생기면 지워집니다. |
| 주소의 표시 | 거절은 /?sso=none 으로 돌아옵니다. 브라우저 저장소가 지워졌어도 이 주소와 auth_error 가 붙은 주소에서는 시도하지 않습니다. |
브라우저 저장소를 읽지 못하면 "이미 시도했다" 로 간주합니다. 사생활 보호
모드와 사이트 데이터 차단은 읽기에서 예외를 던지는데, 그것을 "아직 안 했다" 로
읽으면 곧 루프입니다. /api · /v1 · /health · /mcp · /momento 처럼 Server 가
직접 답하는 경로에서는 시도하지 않으며, 이미 세션이 있으면 시도하지 않습니다.
3. 리다이렉트가 생기는 자리는 관리자 설정에만 묶입니다
prompt=none 은 최상위 리다이렉트를 만들므로, 그것이 일어날 수 있는 자리는 누구나
붙일 수 있는 쿼리 문자열이 아니라 관리자의 결정에 묶여야 합니다.
auto_login이 꺼져 있으면?prompt=none을 붙여 불러도 Server 는 조용히
평범한 로그인으로 바꿉니다./api/v1/auth/methods의keycloak_auto_login은
Keycloak 이 실제로 로그인을 완료할 수 있는 상태(client secret 까지 갖춰진 상태)
에서만 참이 되므로, 완료할 수 없는 provider 로 리다이렉트만 만드는 일이 없습니다.- 거절을 인식하는 것은 이 Server 가
prompt=none으로 시작한 흐름에 한합니다.
oidc_flows에silent열이 더해져 흐름마다 그 사실을 기억하고, 콜백은 그
state를 한 번 쓰고 폐기하므로 재사용할 수 없습니다. 만료되었거나 이미 소비된
흐름, 이 Server 가 모르는state는 거절로 인정하지 않습니다. - 평범한 로그인의
login_required나access_denied같은 실제 오류는 전과 같이
KEYCLOAK_PROVIDER_REJECTED로 기록되고 사용자에게 안내됩니다.
새 항목은 openapi.yaml 에 반영되었습니다 — Keycloak 설정의 auto_login,
GET /api/v1/auth/methods 의 keycloak_auto_login, GET /api/v1/auth/keycloak/start
의 prompt=none, 그리고 콜백의 /?sso=none 리다이렉트. 관리자 가이드 16.2절이
설정, 동작 순서, 루프 방지 장치와 확인 방법을 설명합니다.
검증
- Server.
auto_login이 꺼진 동안prompt=none이 URL 에 들어가지 않고
켜면 들어가는 것,login_required가silent흐름에서만 거절로 인식되고 평범한
흐름에서는 실패로 기록되는 것, 거절 뒤state가 폐기되는 것, 깊은 주소가
return_to로 돌아오는 것, 거절 응답 네 종류(login_required·
interaction_required·consent_required·account_selection_required)만
인정되는 것을internal/auth와internal/httpapi의 테스트로 고정했습니다.
HTTP 계층 테스트는 가짜 provider 를 띄워 설정이 꺼진 상태·켜진 상태·거절·재시도
차단을 한 흐름으로 확인합니다. - 콘솔. 시도 여부를 정하는 함수는 브라우저 없이 시험할 수 있도록 순수하게
두었고, 설정 꺼짐·Server 경로·sso=none·auth_error·로그아웃 표시·이미 시도·
저장소 읽기 예외의 각 경우에 시도하지 않는 것과,return_to가 같은 오리진
경로만 받는 것을 vitest 로 고정했습니다. - 저장 모드 양쪽에서.
go test ./...가 SQLite fallback 과 실제
PostgreSQL(scripts/test-postgres.sh) 양쪽에서 전 패키지 통과하고,
go vet·go build가 통과합니다. 콘솔의 vitest 가 통과하고, 체크인된
server/internal/webui/dist는 React 빌드 결과와 같습니다. - 새 토글의 캡처는 가이드에 싣지 않았습니다. 캡처 스크립트·PNG·가이드 세 곳을
대조하는 테스트가 있어 다음 릴리즈의 과제로 남깁니다.
호환성
- 데이터베이스 마이그레이션 008 이
oidc_flows에silent열을 더합니다.
기본값이 있으므로 기동 시 자동으로 적용되며, 진행 중이던 로그인 흐름은 평범한
흐름으로 남습니다. 멀티 Pod 에서는 전과 같이 PostgreSQL advisory lock 아래에서
한 Pod 만 적용하므로 기동 순서를 맞출 필요가 없습니다. - 기본값은 꺼짐이고, 꺼져 있으면 동작 변경이 없습니다. 로그인 화면, Keycloak
로그인 흐름,/api/v1/auth/methods의 기존 필드가 이전과 같습니다. 새 필드
keycloak_auto_login하나가 더해질 뿐입니다. - 켜면 Keycloak 에 로그인한 사용자는 로그인 화면을 보지 않습니다. 로컬 계정으로
들어가야 하는 사용자는 Keycloak 에서 로그아웃하거나 콘솔에서 로그아웃한 뒤
같은 탭에서 로그인 화면을 쓰면 됩니다. 로그인 화면의 Keycloak으로 계속 은
설정과 무관하게 평범한 로그인을 시작합니다. - 기존 REST API, Query DSL, MCP 도구는 이번 릴리즈에서 바뀌지 않았습니다.
- Agent(Rust) 동작은 이번 릴리즈에서 바뀌지 않았습니다.