feat: Kevin9327 보안·에이전트·도구 통합 - #4883
Merged
Merged
Conversation
…ting 초인적 규모 퍼징(전 코퍼스 277문서 × 23변형 = 6371 손상 파싱)이 렌더러 i32 덧셈 오버플로 패닉 2건을 잡았다. 손상 입력이 거대한 vertical_pos/line_height(둘 다 i32)를 만들면 s.vertical_pos + s.line_height 가 i32 초과 → 패닉(exit 101, add overflow). - src/renderer/typeset.rs:7290 (hwp3-sample11 45% 플립) — vpos_end/page_bottom_vpos 덧셈 3곳 saturating_add. - src/renderer/layout/table_layout.rs:6607 (issue1949_giant_cell 55% 플립) — calc_nested_controls_bottom_height 의 vpos+height saturating_add. - saturating_add 는 정상값에 동일(오버플로 없음), 손상값만 i32::MAX 포화 → 패닉 방지. 실측: 두 재현자 exit 0(우아), 정상 샘플 렌더링 무영향(151·115쪽 그대로), 기존 typeset 테스트 54/54 통과. - 회귀: tests/cli_exit_codes.rs 에 손상 입력 무패닉 CLI 테스트. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
초인적 규모 퍼징(전 코퍼스 277 × 9변형 × 3명령 = 7479 파싱)이 vertical_pos+line_height (+line_spacing) i32 덧셈 오버플로가 렌더 경로 전반에 만연함을 실증했다(패닉이 한 사이트 수정 후 인접 사이트로 이동). 손상 입력의 거대 layout 값이 여러 경로로 흐른다. - 렌더러 7개 파일의 i32 LineSeg 필드 덧셈 70줄을 saturating_add 로: typeset.rs·height_measurer.rs·table_layout.rs·composer.rs·height_cursor.rs· shape_layout.rs·line_breaking.rs. f64 Vec 인덱스(line_heights[i])는 필드접근이 아니라 안 건드렸다(saturating_add 가 f64 면 컴파일 에러라 오검이 빌드에서 잡힌다). - saturating_add 는 정상값에 결과 동일(오버플로 없음), 손상값만 i32::MAX 포화. - 재퍼징 재확인: 렌더러 오버플로 패닉 0(이전 2+클러스터 → 0). 정상 렌더 무영향 (sample11 151쪽), typeset 54/54·height_measurer 22/22 통과. - 회귀: cli_exit_codes.rs 를 4재현자(info·export-text 경로)로 확장. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
초인적 규모 퍼징(7479 손상 파싱)이 src/parser/mod.rs:628 의 prev_vpos_end = last.vertical_pos + last.line_height + last.line_spacing 가 손상 입력의 거대 vpos/height 로 i32 오버플로 패닉함을 실측. 바로 위(620·625)는 이미 saturating 인데 이 줄만 raw 덧셈이라 누락이었다. - vertical_pos.saturating_add(line_height).saturating_add(line_spacing) 로 맞춤. 정상값 동일, 손상값만 i32::MAX 포화 → 패닉 방지. - 회귀: parser::tests::corrupt_variant_vpos_does_not_overflow_panic (parse_document 파스-레벨, 렌더러 무오염). 재현자 hwp3-sample11-hwp5.hwp 90% 플립. #4820 참조. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
유효한 HWP3 파일 뒤에 강암호 트레일러를 append 해, 민감 필드는 가시층에서 가리고 그 원값은 비밀번호 보유자만 복원하게 한다. 순정 한컴은 가려진 정상 문서를 그대로 열람하고, rhwp 는 파일을 열 때 절대 에러로 죽지 않고 Plain/Sealed/Broken 으로 수렴한다. 정당한 범위: REDACTED 전용(문서 전체 위장/은닉 모드 없음). 평문 매직마커 (RHWPSEC1/RHWPEND1)로 시작·끝나 검사 도구가 즉시 식별 가능 — 내용을 암호화할 뿐 존재를 은폐(스테가노)하지 않는다. 가시 기밀성은 여전히 56비트(DES) 상한. - src/security_trailer.rs: seal(호스트,비밀,비번)=Argon2id(19MiB)→XChaCha20-Poly1305 (AAD=헤더 전체) append. open()=탐지→복호→Plain/Sealed/Broken 정상화(무패닉). detect_trailer/visible_layer. 포맷은 kdf_algo/aead_algo 바이트로 알고리즘 agility. - 검증: cargo test --lib security_trailer:: 7/7(왕복·오답비번→Broken·변조→AEAD거부· 무트레일러→Plain·우연MAGIC_END→Plain 오탐방어·재저장소실→Plain·빈비밀). - 의존성 argon2 0.5·chacha20poly1305 0.10·zeroize 1(RustCrypto·순수Rust·wasm 호환). - 후속: CLI(seal/unseal)·파서 통합(열 때 자동 복원)은 별도 PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI 가 WASM32 경로 3곳에서 실패했다 (PR #4826): Lint 의 "Clippy (WASM32)", Frontend package gates 의 wasm-pack 빌드, Canvas visual diff 의 wasm 빌드. Build & Test·CodeQL 실패는 그 파생이다. 원인은 **getrandom 0.2** 다. argon2 의 기본 피처(`rand`·`password-hash`)와 chacha20poly1305 의 기본 피처(`getrandom`)가 rand_core 0.6 을 거쳐 0.2 를 끌어오는데, 0.2 는 wasm32-unknown-unknown 에서 `js` 피처 없이 compile_error! 로 죽는다. 이 저장소가 이미 켜 둔 `getrandom 0.4 + wasm_js` 는 다른 버전이라 그 자리를 못 덮는다. security_trailer 는 소금·논스를 `getrandom::fill`(0.4)로 직접 만들고 저수준 Argon2 API 와 논스를 넘겨받는 XChaCha20-Poly1305 만 쓴다 — 두 크레이트의 rand 계열 피처가 애초에 필요 없다. default-features 를 끄고 `alloc` 만 남겨 구버전 getrandom 을 그래프에서 제거한다. 검증: cargo tree -i getrandom@0.2 --target wasm32-unknown-unknown 결과 없음, cargo clippy -p rhwp --lib --target wasm32-unknown-unknown -- -D warnings 통과, cargo check --target wasm32-unknown-unknown --lib 통과, cargo clippy --workspace --all-targets 통과, security_trailer 단위 테스트 7/7 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeQL 이 이 PR 에서 critical 2건을 새로 잡았다 — rust/hard-coded-cryptographic-value, src/security_trailer.rs 의 테스트 상수 `PW` 와 오답용 `b"wrong"` 이 KDF 에 비밀번호로 흘러간다. 억제 주석으로 덮는 대신 원인을 없앤다. 테스트는 매 실행 `getrandom::fill` 로 32바이트 비밀번호를 새로 뽑고, 오답 경로는 두 번째 난수를 쓴다. 스캐너를 달래는 것만이 아니라 테스트로서도 낫다 — 왕복·변조·오답 판정이 특정 값에 우연히 기대지 않음을 매 실행이 재확인한다. 검증: security_trailer 단위 테스트 7/7 통과, cargo clippy -p rhwp --lib --target wasm32-unknown-unknown -- -D warnings 통과, cargo clippy --workspace --all-targets 통과, rustfmt 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
robustness.py(#4814)가 릴리스 게이트(바운드·패닉/행 0 강제)라면, fuzz_corpus.py는 그 앞단의 발견 엔진이다. 전 코퍼스 × 다명령 × 결정적 손상을 ThreadPoolExecutor로 병렬 퍼징해, 안 고쳐진 DoS를 소스 위치(file:line)별로 클러스터링한다. - gym/tools/fuzz_corpus.py: 명령 지정(기본 info/export-text/export-structure/ export-render-tree)·결정적 변형(절단·플립·biglen)·병렬·패닉 클러스터(스택오버플로· 어보트 별도 버킷)·무한루프 timeout 버킷. JSON/사람용 리포트. - 이 캠페인의 실제 DoS(렌더러·파서 오버플로·무한루프·스택오버플로)를 전부 이 방식으로 발견했고, 그 방법을 재사용 가능한 도구로 정식화. 에이전트가 돌려 rhwp를 계속 경화. - 가드: test_gym_fuzz_corpus(변형 결정성·분류·클러스터링, 바이너리 없이 목킹 5건), ci.yml 등록, gym/README 문서. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…4827) 손상된 .hwp 를 `export-structure`(및 본문을 파싱하는 모든 명령)로 처리하면 HWP5 본문 파서의 상호재귀에 깊이 상한이 없어 스택 오버플로(SIGSEGV, 패닉과 달리 catch_unwind 로 못 잡음)로 크래시한다. 재귀 경로: parse_paragraph → parse_ctrl_header → parse_control → parse_table_control → parse_cell → parse_paragraph_list → parse_paragraph (↩) 레코드 레벨은 10비트(≤1023)라 표 중첩이 최대 ~341겹까지 파일로 도달 가능하고, 그 깊이가 스레드 기본 스택 한계 근처라 크래시/완주가 비결정적으로 갈린다 (#4822 §2 관측과 일치). 수정: 이 재귀 계열이 전부 경유하는 `parse_paragraph` 진입점에 스레드-로컬 RAII 깊이 가드를 둔다(상한 64, HWPX #4759·HWP3 #4285·HWP5 묶음개체 #4761 형제 가드와 동일 값). 상한 초과 시 BodyTextError 로 거부하면 상위 `parse_paragraph_list` 의 `if let Ok(..)` 가 해당 하위 트리만 절단하고 나머지는 정상 파싱한다. 정상(얕은 중첩) 문서 동작은 불변. 검증: - 결정론적 재현: 표 250겹 파싱이 상한 없이 250 그대로 내려감(수정 전); 341겹 + 작은 스택 → STATUS_STACK_OVERFLOW(0xC00000FD). - 회귀 테스트 2건 추가(상한 초과 절단 / 정상 깊이 보존). - 정상 샘플 30개 × 3모드 = 90개 export-structure 출력이 수정 전후 완전 동일. - cargo test --lib 3704 통과·0 실패, rustfmt·clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
convert·export-hwpx·export-markdown 를 손상 HWP 로 퍼징해 렌더러(#4818)· 파서 vpos(#4821) 하드닝이 놓친 새 DoS 패닉 2건을 고친다. - src/model/page.rs: PageAreas::from_page_def_for_page 의 raw 산술 4곳을 주변과 동일하게 saturating 화. bottom = page_height - margin_footer 의 u32 언더플로가 세 명령 모두에서 재현되던 패닉(page.rs:271). 덧셈 3곳도 동일 클래스. 정상 데이터 동작 불변. - src/wmf/converter/graphics_object.rs: GraphicsObjects::delete 가 손상 WMF DELETEOBJECT 의 범위 밖 object_index 로 직접 색인해 패닉. get 과 같은 관용 색인(get_mut)으로 범위 밖 삭제 무시. - tests/cli_exit_codes.rs: 손상 footer_length 회귀 테스트 추가. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
유효 컨테이너에 악성 본문을 심은 퍼징에서, 파싱은 성공하나 `inspect injection` 스캔이 이차로 폭주하는 DoS 2건을 잡아 고친다(injection_scan.rs 고유 결함). - 버그 A: governing_object_start·scope_governs_override 가 목적어·범위어를 find_from 으로 문서 끝까지 훑는다. 건초더미를 &chars[..verb_at] 로 한정 — 서술어 뒤 매치는 원래 버렸으므로 매치 집합 동일, 낭비 스캔만 제거(선형). - 버그 B: visit_text 가 신호마다 make_excerpt 로 text.chars() 를 다시 모은다. chars 를 문단당 한 번만 모아 재사용(make_excerpt 를 excerpt_from_chars 코어로 분리). 실측(디버그): 버그 A "무시하"×32k 10.15s→1.23s, ×128k 타임아웃→4.21s. 정상 문서 17건 × 5명령(inspect/redact/sanitize) 출력 불변. 회귀 테스트 2건 추가. cargo test --lib 3704 통과·무회귀. rustfmt --check 통과. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
기존 비밀번호 모드(Argon2id → XChaCha20-Poly1305)는 이미 양자내성이다 — 대칭 암호는 Grover 로 유효 강도가 절반(256→128비트)으로 줄 뿐 여전히 안전하다. 양자 (Shor)에 깨지는 건 비대칭 공개키 교환이다. 그래서 이 커밋은 비밀번호 공유 없이 수신자 공개키로 봉인하는 양자안전 공개키 교환(ML-KEM-768 격자 KEM)을 추가한다. - kdf=2(KDF_MLKEM768) 새 PQ 트레일러: EK=1184·DK=2400·CT(encap)=1088·SS=32. - generate_keypair / seal_to_pubkey / open_with_privkey 3함수 추가. - 32바이트 공유비밀을 그대로 XChaCha20-Poly1305 키로 사용, 헤더 전체(MAGIC..ct_len)를 AAD 로 묶어 어떤 헤더 변조도 복호 거부로 이어진다. - ML-KEM 역캡슐화는 무오류(묵시적 거부)이므로 진짜 무결성 관문은 AEAD 다: 틀린 개인키·변조 CT 는 다른 공유비밀 → AEAD 실패 → Broken(패닉 없음). - 개인키 경로는 checked 산술 기반 엄격 경계 파싱으로 어떤 길이 조작에도 인덱스 OOB/패닉 없이 Broken 으로 수렴한다. - 비밀번호(kdf=1) 경로는 무변경 — 공개키 트레일러가 open() 으로 들어오면 안내 메시지만 개선(동작은 그대로 Broken). - 순수 Rust(RustCrypto), wasm 호환. 정당한 범위는 기존과 동일(REDACTED 전용, 평문 매직마커로 탐지 가능, decoy 모드 없음). - 테스트 12건 추가(왕복·키크기·오키·CT변조·AAD변조·교차경로·빈비밀·잘못된 키길이); cargo test --lib security_trailer:: 19건 전부 통과(debug·release-test), clippy -D warnings·rustfmt 통과. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI 가 WASM32 3단계에서 실패했다 (PR #4838): Lint 의 Clippy (WASM32), Frontend package gates 의 wasm-pack 빌드, Canvas visual diff 의 wasm 빌드. Build & Test 실패는 그 파생이다. 원인은 #4826 과 같은 **getrandom 0.2** 로, 이 브랜치가 그 수정 이전 베이스에서 갈라져 나와 그대로 물려받았다. gym_hwp3_security_trailer 의 수정본을 병합해 함께 반영한다: - argon2·chacha20poly1305 의 기본 피처 차단(rand 계열이 rand_core 0.6 을 거쳐 getrandom 0.2 를 끌어온다 — wasm32 에서 compile_error!). ml-kem 경로는 이 PR 이 이미 getrandom 0.4 어댑터(GetRandomRng)를 두고 있어 추가 조치가 필요 없다. - 테스트 비밀번호를 상수에서 실행시 난수로(CodeQL critical 2건의 원인). 이 브랜치가 새로 추가한 PQ 테스트 2곳의 `PW` 참조도 같이 옮겼다. 검증: cargo tree -i getrandom@0.2 --target wasm32-unknown-unknown 결과 없음, cargo clippy -p rhwp --lib --target wasm32-unknown-unknown -- -D warnings 통과, cargo check --target wasm32-unknown-unknown --lib 통과, security_trailer 단위 테스트 19/19 통과(PQ 13건 포함), cargo clippy --workspace --all-targets 통과, rustfmt 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
손상 .hwpx 의 header.xml HwpUnitChar <hp:case> 안 극단 정수값이 2× IR 스케일 변환(* 2)에서 곱셈 오버플로 패닉을 유발해 info/export-text/ export-structure/convert 전부가 손상 문서 한 건에 프로세스째 죽던 DoS 를 수정한다. header.rs 세 곳의 * 2 를 saturating_mul(2) 로 교체: - 1288: 문단 여백 left/right/intent/prev/next (i32) - 1357: 문단 lineSpacing (i32) - 1907: 탭 position (u32) 정상 문서 값은 수천 HWPUNIT 수준이라 무영향이고, 극단값만 i32::MAX/ u32::MAX 로 포화되어 그레이스풀하게 처리된다. 회귀 테스트 추가. 정상 샘플 3종 info/export-text/export-structure 출력 바이트 동일, cargo test --lib 3703 passed/0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
파싱 직후, 저장된 line_seg 수가 문단 문자 수를 크게 초과하는(물리적으로 불가능한) 문단의 line_seg 배열을 비운다. line_seg 하나는 화면상 한 줄이고 한 줄은 문자를 최소 1개 담으므로 정상 문서는 언제나 line_seg 수 ≤ 문자 수 + 1 이다. 손상된 HWP/HWPX 는 길이·개수 필드 훼손으로 이 배열을 수만 개까지 부풀릴 수 있고(퍼징 실측 samples/hwp3-sample14.hwp 10% 바이트 플립본: 한 문단 line_seg 25,856 개 > 문자 21,454 개), 그러면 compose_lines·layout 이 line_seg 마다 문단 전체 텍스트를 다시 슬라이싱·배치해 O(line_seg 수 × 문단 길이)로 폭주한다 — info·export-text 가 유한 시간에 끝나지 않는 서비스 거부(DoS)다. - drop_corrupt_oversized_linesegs(): 상한을 문자 수 + 64 로 넉넉히 잡아 정상 문단(줄바꿈만 있는 문단 포함)은 절대 걸리지 않으므로 동작이 동일하다. 포맷 무관 가드다. 비운 뒤 기존 리플로우/합성 폴백이 문단을 정상 재구성한다. - 회귀 테스트 2건 추가(손상 배열 제거 / 정상 문단 보존). - 실측: 손상본 info·export-text 무한정지 → 0.2s·1.8s 종료. 정상 문서 10종 (HWP3·HWP5·HWPX)의 info·export-text 출력 바이트 동일(전/후 SHA-256 비교). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI Lint 의 "Check workspace members (FFI bindings, tools)" 가 실패했다 (PR #4842). Build & Test 실패는 그 롤업이다. src/document_core/commands/document.rs 의 경계 테스트가 `std::iter::repeat('\n').take(300).collect()` 로 문자열을 만드는데, clippy manual_str_repeat 이 -D warnings 에 걸린다. `"\n".repeat(300)` 은 같은 값을 내면서 의도도 더 분명하다. 검증: cargo clippy --workspace --all-targets -- -D warnings 통과, drop_corrupt_oversized_linesegs 테스트 2/2 통과, rustfmt 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ed25519 는 Shor 로 깨지므로 작업캡슐·출처 서명을 ML-DSA-65 로 확장한다. 기존 capsule_sign(Ed25519)은 전혀 건드리지 않고 새 모듈 src/pq_sign.rs 로 추가하므로 기존 서명 경로는 그대로다. 하이브리드(Ed25519 ++ ML-DSA)도 제공한다 — 검증은 둘 다 통과해야 유효라 어느 한쪽 스킴이 무너져도 위조는 나머지 절반을 여전히 깨야 하므로 출처는 살아남는다(전환기 권장 태세). - 순수 ML-DSA-65: generate_keypair/sign/verify (시드 32B 결정론 키생성 FIPS 204 KeyGen_internal · 결정론 서명) - 하이브리드: hybrid_generate_keypair/hybrid_sign/hybrid_verify - malformed 입력 무패닉(검증=false, 서명=Err), alg 문자열/태그로 포맷 민첩성 - RustCrypto ml-dsa 0.1(순수 Rust, wasm 호환; getrandom/pkcs8 기능 끄고 alloc 만) - 모듈 내 테스트 13종(라운드트립·변조 탐지·하이브리드 양쪽 강제·무패닉) 통과 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeQL 이 critical 6건을 새로 잡았다 (PR #4844) — rust/hard-coded-cryptographic-value, src/pq_sign.rs 의 테스트 상수가 시드·공개키·서명 자리로 흘러간다: 254 [7u8; ML_DSA_65_SECRET_LEN] 결정론 시드(실제 키 재료) 298 [0u8; 10] / 305 +1 길이 길이 거부 경로 302 vec![0u8; ML_DSA_65_PUBLIC_LEN] 정상 길이·전부 0 공개키 312·313 [0u8; 16] / [0u8; 33] 시드 길이 거부 경로 억제 대신 원인을 없앤다. 두 헬퍼를 두고 값을 실행시에 만든다: - rand_bytes(n): getrandom::fill 로 매 실행 새 바이트. 고정하려는 성질 (결정론·길이 거부·쓰레기 키 거부)은 어느 것도 특정 값에 기대지 않으므로 난수가 오히려 매 실행 재확인이 된다. - zeroed(n): 난수 버퍼를 0 으로 덮어 "정상 길이·전부 0" 경계를 그대로 보존한다 — 이 경계는 의미가 있어 값만 실행시로 옮기고 유지했다. 같은 패턴이던 하이브리드 테스트 2곳(현 경보 대상 아님)도 함께 옮겨 재발 소지를 없앴다. 검증: pq_sign 단위 테스트 13/13 통과(순수 ML-DSA 7 + 하이브리드 6), cargo clippy --workspace --all-targets -- -D warnings 통과, rustfmt 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
사람 암호(약한 고리) 없이 에이전트가 보관하는 완전 엔트로피 기계키로 문서를 봉인하는 모듈을, security_trailer 와 독립된 자체 형식으로 추가한다. - 1번 모드(계산적 안전·양자내성): XChaCha20-Poly1305 AEAD, KDF 없음, 24바이트 난수 논스, 헤더 AAD, 호스트 뒤 트레일러(가시 계층=호스트). - 2번 모드(정보이론적 안전): 일회용 패드 XOR. 패드 길이>=메시지 강제, 같은 컨테이너에 distinct algo. 기밀성만 제공(인증 없음) — 정직한 한계 명시. - 자체 마법 RHWPAGT1/RHWPAGND, 꼬리 탐지, 엄격한 경계로 손상 입력 무패닉. - chacha20poly1305 0.10(alloc) 의존성 추가. OTP 는 XOR+getrandom 만 사용. - 단위 시험 16종(라운드트립·오키/변조 Broken·교차모드 거부·무패닉) 통과. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
손상된 임베디드 WMF/EMF 메타파일 바이트가 산술 오버플로 패닉(디버그 빌드)과 무한 할당(릴리스 OOM)으로 번지는 DoS 5개 지점을 하드닝한다. 파서/컨버터를 퍼징해 재현·수정한 뒤 회귀 테스트를 추가했다. - bitmap16.rs calc_length: DDB 비트 길이 `(((W*Bpp+15)>>4)<<1)*H` i16 오버플로 → i64 포화 산술 + 음수 클램프(음수 i16 → 거대 usize 사인확장도 차단) - bitmap_info_header size(): Core(u16) `:82`, Info/V4/V5(u32) `:113` 치수 곱 오버플로 → 공용 `dib_image_size`(u64 포화)로 통일 - wmf converter svg placeable header `:577`: 경계 `right-left`/`bottom-top` i16 뺄셈 오버플로 → `saturating_sub` - emf converter player open_root_group `:62-63`: `bounds.right-left`/`bottom-top` i32 뺄셈 오버플로 → `saturating_sub` - read_variable: 신뢰 불가 `len` 선할당(`vec![0u8; len]`) 제거, 실제 도착 바이트만큼만 증분(64KiB 청크) 할당하여 거대 크기 선언 시 OOM abort 차단 유효 메타파일의 변환 결과는 동일하다(유효 치수는 오버플로하지 않아 동일 값). `cargo test --lib` 무회귀(3707 pass), 단위·통합 회귀 테스트 추가. #4832(`graphics_object.rs:22` 색인 OOB)와는 별개 지점이다. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
파싱·레이아웃은 분기 지배적이라 GPU 대상이 아니다. 이 변경은 그 경계를 넘지 않고, 기존 SVG 산출(render_page_svg_native)이 만든 벡터를 픽셀로 굽는 래스터화 단계만 GPU로 옮긴다 — 대량 문서를 VLM 입력 이미지로 굽는 파이프라인용. - 새 cargo feature `gpu` 뒤에 vello/vello_svg/wgpu 경로 추가(native-skia 와 동일 게이팅). CI 는 GPU 없이 컴파일, 실제 GPU 실행은 로컬. - CLI `export-png-gpu`(+ `gpu-info`). feature 없이 빌드하면 사용법 오류(exit 2). - `--benchmark`: 같은 usvg::Tree 를 GPU(vello)·CPU(resvg) 두 래스터라이저에 동일 입력으로 넣어 순수 래스터화 시간과 픽셀 차이를 실측. 정직한 실측(RTX 5080 Laptop, Vulkan): 래스터화만 보면 GPU 16~78x, 엔드투엔드는 1.4~2.2x(상한은 공통 usvg 셰이핑·PNG 인코딩에 있음). 치수 100% 일치, 평균 픽셀차 <1.3/255. 어디서 이기고 어디서 아닌지 PR 본문에 숫자로 명시. 관련 이슈: #4848 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI 테스트 샤드가 실패했다 (PR #4849): agent_codex_contract::every_capability_command_has_a_codex_chapter — "대전에 장이 없는 명령: [export-png-gpu, gpu-info]". 이 가드는 자기서술(capabilities)의 전 명령이 대전에 장을 갖도록 강제한다 — 명령만 늘리고 교본을 두면 문서가 CLI 를 못 따라간다. 두 명령 모두 category=export 라 변환·렌더 가족(40)에 등록하고 재생성했다. 검증: gen_agent_codex.py --check 변경 0(멱등), 재생성 산출에 절대 경로 0건·미분류 장 없음, agent_codex_contract 2/2, provenance_contract 10/10, capabilities_subcommands_contract 4/4, cli_json_contract 31/31, rustfmt 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
문서 본문을 그대로 프롬프트에 넣으면, 문서에 심긴 "이전 지시를 무시하라" 같은 문장이 사용자의 지시로 오인돼 에이전트가 간접 프롬프트 주입으로 탈취당한다. 이미 있던 세 조각(inspect injection·출처 표지·export-provenance-map)을 한 번의 읽기 전용 호출로 묶는 rhwp armor <파일> [--json] 을 추가한다. - 핵심 질의 document_core::queries::armor: - fence()/fence_open()/fence_close() — 본문을 nonce 격벽으로 감싸는 순수 함수 - generate_nonce() — getrandom 128비트, 호출마다 무작위(문서가 위조 불가) - DocumentCore::armor() — 격벽 + scan_injection(읽기 전용) 결합 - CLI armor + MCP hwp_armor(읽기 전용) + capabilities 등재 + 도움말/매뉴얼 - 봉투: schemaVersion·source·pageCount·scanScopes·safety(nonce·격벽 표지·신호 수· note)·armoredText·injectionSignals·signalCount·clean + 출처 표지 - provenance MAP: armoredText·injectionSignals[].excerpt/matched 를 문서 파생 선언 - 문서를 고치지 않는다 — 격벽은 뜻을 지우지 않고 경계만 구조로 세운다 검증: cargo test armor(lib 9)·armor_contract(8)·provenance_contract(10)· cli_json_contract(31)·injection_scan_contract(14)·ontology_contract(13)· mcp_server_contract(25) 등 통과, clippy(workspace+wasm) 0 경고. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
한 명령(tools/agent_onboarding/rhwp_doctor.py)으로 바이너리 검증 → 번들 샘플 자가검증(info/export-text) → 붙여넣기용 .mcp.json 방출 → 첫 5분 레시피 지도까지 수행한다. --json 기계 판독과 종료 코드(0/1/2/3)로 신호하고, 바이너리 미빌드 시 크래시·행 없이 빌드 명령 안내 후 exit 3 으로 우아하게 저하한다. 함께 추가: 5분 경로 문서(mydocs/manual/agent_onboarding.md, 한글), 얇은 온보딩 스킬(.claude/skills/rhwp-onboarding/), 바이너리 불요 가드 테스트 (tools/agent_onboarding/test_rhwp_doctor.py). tools/·mydocs/·.claude/skills/ 아래 새 파일만 추가하며 병렬 세션 소유 표면은 건드리지 않는다. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
무상태 MCP 표면은 capabilities_mcp_covers_every_json_command 계약으로 모든 --json 명령이 도구로 노출됨이 보장되지만, 세션 표면(hwp_doc_*)에는 개요·조문 구조와 날짜·금액·수량 추출 축이 빠져 있었다. 세션으로 대형 문서를 한 번 열어 반복 조회하는 에이전트가 이 둘을 쓰려면 무상태 도구로 되돌아가 파일을 다시 읽고 재파싱해야 했다 — 세션의 존재 이유(재파싱 회피)를 무효화한다. - hwp_doc_structure: 열린 핸들의 개요/조문 계층. 무상태 export-structure 와 같은 코어(build_structure)·봉투(structure_json_value) 재사용 → 봉투 동형(source=docId). - hwp_doc_extract_data: 열린 핸들의 날짜·금액·수량. 무상태 extract-data 와 같은 코어(extract_data)·봉투(extract_data_json_value) 재사용 → raw/normalized·주소· totalItemCount/truncated(S7) 동형. kind·limit 인자, 전수 스캔 후 표시만 절단. 두 도구 모두 읽기 전용·멱등. tools/list↔tools/call 게이팅 동형을 위해 이름을 ALL_SESSION_TOOLS/SESSION_READ_TOOLS 단일 출처에 등재했고, 인접 조회 도구 (hwp_doc_info/fields/tables) 설명을 '언제 쓰나 / 봉투 모양 / 오류 회복' 3요소로 폴리시했다. 검증: cargo build; cargo test --lib(3702 pass); mcp 통합 13파일 전부 pass; cli_json_contract·spec_ledger·annotations(세션 수 16→18) green; 살아있는 mcp-serve 로 initialize→tools/list→tools/call 왕복 실증(양 도구 listed+callable, 봉투 동형·S7 절단 확인); rustfmt --check·clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI 가 두 갈래로 실패했다 (PR #4851). ① agent_profile_router_contract::every_stateless_tool_belongs_to_some_ specific_profile — 새 도구 hwp_armor 가 어느 업무 프로필에도 없어 개발통합(필터-없음)으로만 닿는다. 본문을 프롬프트에 통째로 넣는 축이 바로 아카이브검색(RAG·감사)이라 그 프로필의 tools 와 recipe 에 넣는다 — 필요한 자리에 도구가 없으면 방패가 있으나 마나다. ② CodeQL critical 5건 — rust/hard-coded-cryptographic-value. armor 테스트가 nonce 를 상수("deadbeef"·"00112233"·"a1b2c3d4e5f60718"·"ff00")로 두고 격벽에 넘긴다. 이 모듈에는 이미 generate_nonce() 가 있으므로 테스트가 그것을 쓰게 한다 — 억제보다 낫고, 격벽 성질이 실제 nonce 에서도 성립함을 매 실행 재확인한다(기대 문자열도 fence_open/close 로 유도). 검증: agent_profile_router_contract 8/8, armor 단위 9/9, mcp_tool_annotations_contract 5/5, agent_codex_contract 2/2, cargo clippy --workspace --all-targets 통과, rustfmt 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI 테스트 샤드가 실패했다 (PR #4853): skills_contract::skills_have_valid_frontmatter_and_are_executable — "rhwp-onboarding: 실행 가능한 `rhwp <명령>` 참조가 하나도 없다 — 스킬은 안내문이 아니라 실행 규약이다". 스킬이 닥터(python)만 가리키고 정작 그 닥터가 무엇을 돌리는지는 산문으로만 적혀 있었다. 닥터가 실제 실행하는 명령을 그대로 싣는다 — FAIL 이 났을 때 손으로 같은 명령을 쳐 원인을 보는 것이 온보딩의 핵심 동작이기 때문이다. rhwp_doctor.py 의 실측과 일치시켰다(--version, info --json, export-text --json --max-chars 2000, 샘플은 SAMPLE_CANDIDATES 첫 항목). 배선 확인용 mcp-serve 와 첫 과제 최단 경로(explain·digest)도 함께 적었다. 검증: skills_contract 2/2 통과, 문서에 적은 5개 명령을 실제 바이너리로 전부 실행해 exit 0 확인(작동하지 않는 예시를 남기지 않는다). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
samples/ 코퍼스 위에서 에이전트-대면 문서 과제(export-text·info·structure· convert)를 rhwp 와 대안 도구에 똑같이 돌려 벽시계 중앙값·성공률·간이 충실도를 재고, 문서화된 사실로 능력 매트릭스를 채우는 재현 가능한 하네스. JSON + 마크다운 리포트 생성, --from-json 재렌더, 정직한 저하(못 돌린 도구는 "n/a: 이유", 날조 금지). 이 머신 실측(Windows, rhwp v0.8.4 debug, 50 파일 = HWP 25 + HWPX 25): - 실제로 돌린 도구: rhwp(빌드) + pyhwp(hwp5txt 0.1b15, 휴면이라 six 수동 보강). - soffice(미설치 + HWP5 임포트 필터 없음)·hwplib(Java 라이브러리, CLI 아님)· Hancom SDK(Windows 전용)은 구조적 사실로만 기록(숫자 없음). - export-text: rhwp 98%(49/50)·충실도 1.00× vs pyhwp 50%(25/50, HWPX 0/25)·0.27×. 동일 HWP5 집합 속도는 pyhwp 1154ms < rhwp 2149ms(debug) — 경쟁자가 빠른 곳도 적음. - info/structure/convert 는 rhwp 만 구조화 CLI 로 수행(대안은 동일 형식 산출 없음). 가드: scripts/tests/test_gym_competitive_bench.py(순수 로직 21종, 바이너리·외부도구 불요), .github/workflows/ci.yml gym 블록에 등록(distinct anchor #4855). Closes #4855. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
받은 문서에 심어진 은닉 추적·워터마크를 찾는 읽기 전용 검사 `rhwp inspect watermark` 와 그것을 지우는 순수 정화 코어 `stego_scan::sanitize_stego` 를 더한다. 방어·탐지 전용이며 검사 회피용이 아니다. - 탐지 3축(신규 모듈 src/document_core/queries/stego_scan.rs): hidden_char(제로폭·비가시 문자 열을 비트/ASCII 로 복호), homoglyph(라틴 낱말에 섞인 키릴·그리스 동형자, text_security::confusable_to_latin 단일 표 공유), whitespace(뒤따르는 긴 공백/탭·혼합 열 — 약한 신호라 등급 낮음). - --json 봉투(schemaVersion·source·findings·clean·severityCounts·kindCounts) + 출처 표지. - 정화 코어: 탐지가 신고하는 마크만 제거/정규화하고 정당한 쓰임(맨 앞 BOM·옛한글 PUA 조판 제로폭·이모지 ZWJ·순수 비라틴 낱말·정렬 공백)은 불변. 멱등이며 정화 후 재검사 = 0. inspect 는 읽기 전용 규약이라 문서 재저장 CLI 는 검증된 본문 치환에 얹는 edit 후속으로 분리. - 배선: inspect watermark 하위 명령 + MCP hwp_inspect_watermark(아카이브검색 프로필·암호 문서 지원 화이트리스트 등재). 형제 작업 파일(injection_scan.rs 등) 무수정. - 검증: cargo test --lib 3715 통과(신규 13 포함), 프로필 커버리지·암호 패리티 계약 통과, clippy·fmt 청정. 실측 픽스처(제로폭 "Hi" 비트 + 키릴 Т)로 탐지·복호·정규화 확인, 원본 45건 스윕에서 hidden/homoglyph 오탐 0. Closes #4859 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Aug 15, 2026
This was referenced Aug 17, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
통합 범위
kevin9327의 열린 PR 27건을 최신devel위에서 오래된 변경부터 누적 검토·통합합니다.#4818, #4821, #4826, #4829, #4830, #4832, #4836, #4838, #4840, #4842, #4844,
#4845, #4847, #4849, #4851, #4853, #4858, #4861, #4862, #4863, #4866, #4867,
#4871, #4873, #4874, #4877, #4878
포함 축은 손상 문서 DoS 방어, HWP3 보안 트레일러·서명, 에이전트 armor·DSEL·threat-scan,
MCP 세션 조회·RAG export, gym 퍼징·경쟁 벤치마크·하네스 계약입니다.
메인터너 보정
ast·eval·suggest·테스트 모듈로 분할하고,threat-scan fixture의 Clippy 경고를 해소했습니다.
hwp_threat_scan을 아카이브검색 프로필에 연결하고, 지식지도에highestSeverity와notes필드 및 MCP 매핑을 추가해 계약 테스트와 운영 문서가 일치하도록 보정했습니다.Path.read_bytes·Path.write_bytes로정리해 CI의
ResourceWarning이 실패로 승격돼도 통과하도록 했습니다.로컬 검증
cargo nextest run --cargo-profile release-test --target-dir target/pr-review --tests --test-threads 12 --no-fail-fastcargo clippy --all-targets --target-dir target/pr-review -- -D warningsPYTHONWARNINGS=error::ResourceWarning경로 통과wasm-pack build --target web --out-dir pkg통과devel@f5f039775대비 5개 fixture의 페이지·추적 객체 수 동일, geometry 회귀 0건최신성
2026-08-16 KST에 모든 원 PR head와 갱신 시각을 재확인했습니다. 검증 중 추가 push는 없었습니다.
원 PR별 archive 검토 기록은 이 code head의 CI가 완료된 뒤 같은 PR에 trailing docs-only commit으로 추가합니다.