Skip to content

Releases: hkjang/invenqor

Invenqor v0.2.43

Choose a tag to compare

@hkjang hkjang released this 04 Oct 23:22

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 를 거쳐 이 경로가 선언하지 않은 500 INTERNAL_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 가 실행되기 전에
    400 INVALID_RELATION 으로 돌아갑니다.
  • DELETE 는 relationId 를 봅니다. 정규형이 아니면 UPDATE 에 닿지 않고
    404 RELATION_NOT_FOUND 로 돌아갑니다.
  • 응답 코드 집합을 넓히지 않았습니다. POST 는 이미 선언된 400 으로,
    DELETE 는 이미 선언된 404 로 거절합니다. 두 방언이 이제 같은 코드를 답하며,
    그 코드는 모두 openapi.yaml 이 이미 적어 둔 것입니다.
  • 정규화한 값만 저장합니다. INSERT 와 감사 로그가 모두 소문자 36자 표기를
    쓰므로, 대문자로 보낸 요청도 행과 audit_logs.after_json 이 같은 글자를
    가리킵니다. relation.delete 의 resource_id 도 정규형입니다.
  • 409 의 뜻이 좁아졌습니다. 이전에는 PostgreSQL 에서 잘못된 모양의 id 도
    409 RELATION_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

Choose a tag to compare

@hkjang hkjang released this 04 Oct 00:37

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 를 거쳐 계약에 없는 500 INTERNAL_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 는 404 ASSET_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: 가 500 INTERNAL_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

Choose a tag to compare

@hkjang hkjang released this 02 Oct 18:19

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 를 거쳐
    계약에 없는 500 INTERNAL_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_ids metadata, 감사
    기록이 모두 지역 변수 primaryID · secondaryIDs 만 읽습니다. 호출자가
    보낸 철자가 저장소에 남는 자리는 더 이상 없으므로, 대문자로 병합해도 MCP
    merged_into 가 primary 를 가리킵니다.
  • missingAssetID 의 모양 검사를 지웠습니다. 호출자가 하나뿐이고 그
    호출자가 호출 전에 정규화를 끝내므로 더는 닿지 않는 코드였습니다. 그 전제를
    주석으로 적어 두었습니다.
  • 폴백의 동작은 바뀌지 않습니다. 오류 코드는 SQLite fallback 이 이미
    답하던 400 INVALID_MERGE 를 그대로 두었으므로, 달라지는 것은 PostgreSQL
    쪽의 500 과 잘못된 200 뿐입니다.

검증

  • 고치기 전에 실패를 먼저 확인했습니다. scripts/test-postgres.sh 로
    PostgreSQL 에서 urn:uuid: 가 500 INTERNAL_ERROR 로, 중괄호·하이픈 없는
    32자 표기가 200 으로 병합을 수행하는 것, 대문자 철자가 PostgreSQL 에서만
    병합되는 것을 실제로 재현한 뒤 고쳤습니다. SQLite fallback 에서는 같은
    테스트 일부가 고치기 전에도 통과합니다 — 방언 차이가 그대로 드러나는
    자리라, scripts/test-postgres.sh 없이는 이 결함의 재현이 반쪽입니다.
  • 회귀 테스트 4건을 더했습니다. 대역 없이 실제 라우터로
    POST /api/v1/assets/merge 를 부릅니다 — 비정규형 primary_id, 비정규형
    secondary_ids(urn:uuid: · 중괄호 · 하이픈 없음 각각), 대문자 id 의
    정규형 접힘, 그리고 대문자로 병합한 뒤 MCP asset_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 에는 대문자가 남아 있고, 그
    과거 행을 고치지는 않습니다 — MCP merged_into 안내는 assets 테이블을
    보므로 과거 병합에도 영향이 없습니다.
  • 알려진 제한. 트랜잭션 안에서 오류를 보고하면 SQLite fallback 이
    교착하는 문제는 그대로 남아 있습니다. 이번 수정은 트랜잭션을 열기 전에
    거절하므로 merge 의 입력 검증 경로가 그 길로 들어가지 않게 되었을 뿐이고,
    mergeAssets 의 트랜잭션 안쪽과 ingest 등 다른 핸들러의 internalError 는
    v0.2.40 과 같습니다. 운영 기본값인 PostgreSQL 에서는 500 이 되며, fallback 을
    쓰는 평가·개발 환경에만 해당합니다.
  • 콘솔이 바뀌지 않았습니다. 임베디드 정적 파일은 v0.2.40 과 같습니다.
  • Agent 의 동작 변경이 없습니다. 설정 파일, 큐, 전송 프로토콜은 이전과
    같고 버전 문자열만 올라갑니다. 기존 Agent 는 서두르지 않아도 Server 와의
    호환에 영향이 없습니다.

Invenqor v0.2.40

Choose a tag to compare

@hkjang hkjang released this 01 Oct 03:44

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 를 거쳐 계약에 없는 500 INTERNAL_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 이 이미
    답하던 400 INVALID_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 가 아닌 두
    입력이 500 INTERNAL_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

Choose a tag to compare

@hkjang hkjang released this 28 Sep 16:29

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 어느 것도
    건드리지 않은 채 400 INVALID_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

Choose a tag to compare

@hkjang hkjang released this 19 Sep 22:22

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_at RFC 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

Choose a tag to compare

@hkjang hkjang released this 18 Sep 06:44

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 을 바꾸고
    질의 실행 을 누르면 offset 0 에서 다시 시작합니다.
  • 끝을 지난 페이지와 0건을 구분합니다. 두 번 누르는 사이에 자산이 지워지거나
    병합되어 페이지가 비면 "이 페이지에는 자산이 없습니다" 로 안내하고 이전 으로
    돌아갈 수 있습니다. 조건에 맞는 자산이 처음부터 없으면 예전처럼 "조건에 맞는
    자산이 없습니다" 입니다.
  • 거부된 질의는 건수를 보이지 않습니다. 구문 오류나 권한 거부로 실행이
    실패하면 제목은 "결과" 로만 남고 이전 결과의 수가 남아 있지 않습니다.

사용자 가이드 12.5 와 Server 설치 가이드 19.4 의 "이 화면에는 다음 페이지가
없으므로" 라는 안내를 고쳤고, query-result.png 를 새 화면으로 다시 찍었습니다.

검증

  • 렌더 단위 테스트. 결과 패널을 QueryResultPanel 로 떼어 브라우저 없이
    Server 응답만으로 그려지는 글자와 버튼을 고정했습니다 — 첫 실행 전, 한 페이지에
    다 들어가는 결과, 한도 너머 1,200건의 첫 페이지, 마지막 페이지의 범위와 버튼
    상태, 끝을 지난 빈 페이지와 0건의 구분, 거부된 실행의 6개입니다. 콘솔의 vitest
    는 150개 통과합니다.
  • 실제 Server 에서 걸어 보았습니다. SQLite fallback 의 Server 에 자산 7개를
    만들고 재빌드한 임베디드 콘솔을 headless Chrome 으로 열어 limit 3 으로
    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

Choose a tag to compare

@hkjang hkjang released this 17 Sep 01:35

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-targets 105개 통과. 호스트와 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

Choose a tag to compare

@hkjang hkjang released this 14 Sep 06:53

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

Choose a tag to compare

@hkjang hkjang released this 12 Sep 22:16

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 이 켜져 있으면 다음 순서로 동작합니다.

  1. 콘솔은 /api/v1/auth/me 와 /api/v1/auth/methods 의 답을 둘 다 기다립니다.
    세션이 없고 keycloak_auto_login 이 참이면 로그인 화면을 그리지 않고
    /api/v1/auth/keycloak/start?prompt=none&return_to=<현재 주소> 로 최상위
    이동
    합니다. 숨은 iframe 이 아니므로 서드파티 쿠키 차단이나 Keycloak 의 프레임
    정책과 무관합니다.
  2. Keycloak 은 prompt=none 에 화면을 그리지 않습니다. 세션이 있으면 인가 코드가
    바로 돌아와 평소 로그인 흐름을 타고, 사용자는 return_to 로 돌아갑니다.
    return_to 는 / 로 시작하고 // 로 시작하지 않는 같은 오리진 경로만 받으며
    그 밖의 값은 / 가 됩니다.
  3. 세션이 없으면 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) 동작은 이번 릴리즈에서 바뀌지 않았습니다.