haechi 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ # Haechi 1.0 구현 범위
2
+
3
+ - 상태: Draft 0.2 (설계 — 아직 미구현; 2026-06-11 3-렌즈 적대적 보안 리뷰 후 강화)
4
+ - 날짜: 2026-06-11
5
+ - 목표 버전: 1.0.0 (0.9.0 다음)
6
+ - 유형: 안정 API 계약 + 플러그인 샌드박스 (첫 번째 안정 릴리스)
7
+
8
+ ## 1. 릴리스 목표
9
+
10
+ 1.0은 **첫 번째 안정 릴리스**다: (a) 지원 중단(deprecation)/마이그레이션 정책과 장기 감사 스키마를 갖춘 **안정적인 공개 API 계약을 동결**하고, (b) 0.1부터 의도적으로 유지해온 선을 넘는다 — **외부 플러그인 코드의 동적 로딩** — 단, **비대칭 서명, 기능(capability) 게이트, `worker_threads` 격리, 감사**가 갖춰진 샌드박스를 통해서만, 그리고 우선 **`authProvider`** 계약에 한해서만.
11
+
12
+ **범위 결정 (2026-06-11, 메인테이너 확인):**
13
+
14
+ 1. **샌드박스/로딩 모델:** 동적 로딩은 **서명(Ed25519, 비대칭)**되고, **기능(capability) 매니페스트 allowlist + 운영자 pin/revocation 체크**를 통과하며, **`node:worker_threads` 격리** 경계에서 실행되고, 전체 **라이프사이클 감사**를 갖춘 플러그인에 한해서만 활성화된다. `createRuntime(config, providers)` **주입(injection)은 기본이자 권장 경로로 유지된다**.
15
+ 2. **플러그인 범위:** 1.0에서는 **`authProvider` 전용**. Classifier/filter 및 crypto 플러그인은 1.x까지 주입 전용으로 유지.
16
+ 3. **API 동결:** **엄격** — 핵심 공개 API, **provider 계약**, **감사 이벤트 스키마**(중첩 하위 스키마 포함), **config 스키마**가 엄격한 semver와 지원 중단 정책 하에 동결된다.
17
+ 4. **릴리스 형태:** **단계적** — 1.0.0은 API 동결 + 서명된 플러그인 계약/적합성(conformance)/서명 + worker 격리 `authProvider` 샌드박스 MVP를 출시한다. 더 강력한 기능 **강제(enforcement)**(child-process + Node 권한 모델), 더 많은 플러그인 종류, 라이브 revocation 피드, 레지스트리는 1.x.
18
+
19
+ Core는 **zero runtime dependency**를 유지한다 — 샌드박스는 `node:worker_threads` + `node:crypto`(Ed25519 sign/verify는 `node:crypto` 내장) 위에 구축된다. `packages/policy-bundle`은 재사용하지 **않는다**(그것은 대칭 HMAC다 — §2.2 참조).
20
+
21
+ ### 정직한 보안 모델 (먼저 읽을 것)
22
+
23
+ **`node:worker_threads`는 악성 코드에 대한 보안 샌드박스가 아니다.** worker는 프로세스를 공유하며 파일시스템, 네트워크, `process.env`에 여전히 접근할 수 있다; 격리는 **V8 힙 전용**이다(Node의 권한 모델은 프로세스 전체에 걸쳐 적용되며 worker별로 적용되지 않는다; `SharedArrayBuffer`/transferable은 공유 메모리 채널을 다시 열 수도 있으므로 와이어 형식은 일반 JSON 문자열이다 — §2.3). 따라서 1.0 샌드박스는 다음을 제공한다:
24
+
25
+ - **메모리 격리** — 별도의 V8 힙; 플러그인은 호스트 메모리, 암호화 키, 토큰 볼트, 감사 싱크를 읽거나 오염시킬 수 없다(타입이 지정된 메시지 채널만이 경계를 넘는다).
26
+ - **크래시/행(hang) 격리 + 리소스 제한** — `resourceLimits`(힙 상한) + 각 호출에 **worker를 종료시키는 타임아웃**이 버그가 있거나 폭주하는 플러그인을 억제한다; 행(hang)은 fail-closed(거부)로 처리된다.
27
+ - **데이터 최소화** — 호스트는 worker에게 **크리덴셜 슬라이스**(`Authorization` 헤더 / bearer 토큰)만 전송하며, **요청 바디와 암호화 키는 절대 전달하지 않는다**; worker는 **raw 클레임**을 반환하고, **호스트**가 `buildExternalIdentity`를 통해 PII-safe identity를 구축한다(keyed-HMAC 키는 호스트를 벗어나지 않는다).
28
+ - **좁고 감사된 타입이 지정된 계약** — worker는 `authProvider` 메시지 프로토콜만 사용하며; 모든 로드/거부/종료 결정이 감사된다(§2.4).
29
+
30
+ 1.0에서 worker 경계가 보장하지 **않는** 것 — 이것들은 **수용된 잔여 위험으로, worker가 아닌 서명/검증 신뢰 모델에 의해서만 게이트된다**(§6):
31
+
32
+ - **악성 *서명된* 플러그인은 여전히 OS를 사용할 수 있다** — `fetch`, `fs`, `process.env`는 차단되지 않는다. 매니페스트의 `networkEgress: false`는 *선언*이며, 1.0에서 강제된 통제가 아니다.
33
+ - **악성 *서명된* auth 플러그인은 합법적으로 수신하는 라이브 크리덴셜을 유출할 수 있다**(bearer 토큰), 사실상 네트워크 egress를 갖기 때문이다. 1.0에는 **기술적 장벽이 없다** — 신뢰 게이트만 있을 뿐이다.
34
+
35
+ 진정한 플러그인별 기능 **강제**(fs/net 차단, 크리덴셜 봉쇄)는 **child-process 격리와 Node 권한 모델**(`--permission --allow-fs-read=…`)이 필요하며, 이는 문서화된 **1.x** 경로다. 이것이 주입이 기본으로 유지되고 신뢰 게이트(비대칭 서명 + 운영자 allowlist + pin + revocation)가 핵심인 이유다.
36
+
37
+ ## 2. 범위
38
+
39
+ ### 2.1 API 안정성 동결 (1.0 계약)
40
+
41
+ **동결된 공개 표면 (명시적 IN/OUT 테이블이 오늘날의 모호한 "0.x는 preview" 표현을 대체한다).** 모든 `package.json` `exports` 서브패스와 CLI가 분류된다:
42
+
43
+ | 표면 | 1.0 상태 |
44
+ |---|---|
45
+ | `haechi` / `haechi/core` (`createRuntime`, `createHaechi().protectJson`, `collectStringEntries`), `haechi/auth` (`authProvider` 계약, `buildExternalIdentity`, `buildIdentity`, `validateLabels`), `haechi/crypto` (`cryptoProvider` 계약, `assertCryptoProviderConformance`, `canonicalize`), `haechi/audit` (이벤트 스키마, `verifyAuditChain`, `sanitizeAudit`, `FORBIDDEN_KEYS`), `haechi/policy`, `haechi/filter` (룰 형태), `haechi/token-vault`, `haechi/runtime` (`normalizeConfig` 형태), `haechi/protocol-adapters`, `haechi/plugin` (매니페스트 + 신규 샌드박스) | **동결** (파괴적 변경 = major) |
46
+ | `haechi/proxy`, `haechi/mcp-stdio`, `haechi/stream-filter`, `haechi/policy-bundle`, `haechi/privacy-profiles`, 그리고 **CLI** (`bin/haechi.mjs`) | **동작 + wire/계약 동결**; 사람이 읽는 CLI/로그 **텍스트**는 여전히 변경 가능(계약 대상 아님) |
47
+ | `api-stability.md §3`에 아직 실험적으로 표시된 항목 | **졸업**(§3에서 제거)되거나 명시된 이유와 함께 **1.0 이후에도 명시적으로 preview로 유지** — 묵시적 모호함 없음 |
48
+
49
+ - **1.0부터 엄격한 semver** (파괴적 변경→major, 가산적 변경→minor, 수정→patch). core에 대한 "0.x minor는 파괴적 변경 가능" 여유가 끝난다.
50
+ - **지원 중단 정책.** 지원 중단된 export/필드/옵션은 **≥1 minor** 동안 유지되며, 문서화된 마이그레이션 노트와 **안정적인 `code` 접두사 `HAECHI_DEPRECATION_*`**가 있는 일회성 런타임 `process.emitWarning`을 방출하고(code/텍스트 자체도 계약의 일부), **다음 major**에서만 제거된다. **보안 예외(허용된 단 하나의 minor 내 파괴적 변경):** *공개된* 취약점을 닫기 위한 변경은 보안 권고문 + 마이그레이션 경로와 함께 **minor 내에서** 파괴적 변경/제거가 가능하다(기존 "안전하지 않은 config 차단은 패치에서 강화될 수 있다" 여유를 반영).
51
+ - **감사 이벤트 스키마 — 중첩 하위 스키마를 포함하여 동결**, 열거됨(최상위 레벨만이 아님): 최상위 `{id, timestamp, protocol, operation, identity, profile, mode, enforced, blocked, payloadShapeHash, detections, summary, auditIntegrity}`; `detections[].{type, ruleId, path, kind, confidence, action, enforced}`; **`identity.{id, type, subjectHash, issuerHash, provider}`**(PII-safe 프로젝션 — `scopes`/`labels`/raw subject는 감사 identity에 **포함되지 않음**); `summary.{byType, byAction, detectionCount}`; `auditIntegrity.{alg, canonicalization, sequence, previousHash, eventHash}`. **새 필드는 가산적으로만 추가되며 기존 필드의 정규화에 절대 영향을 미치지 않으므로**, 1.x 이벤트는 1.0 `verifyAuditChain`으로도 검증된다(이는 `canonicalize`가 리터럴 객체를 해시하고 검증기가 *동일하게 저장된 객체*를 재계산하기 때문에 유효하다 — 보장의 의미는 "미래에 가산적으로 추가된 필드가 새 레코드를 읽는 구버전 검증기를 깨뜨리지 않는다"는 것으로, 이전의 두루뭉술한 표현보다 정확하게 명시됨). 정규화 변경은 새 `canonicalization` 태그 + 리더 마이그레이션 경로와 함께 **major** 이벤트 스키마 bump다. 소비자가 파싱 없이 분기할 수 있도록 명시적 최상위 **`schemaVersion`**을 추가한다(리더 대면; 가산적).
52
+ - **Config 스키마 동결 단위:** config **키 존재 + 형태**가 동결됨; **기본값은 여전히 강화될 수 있음**(더 안전한 기본값은 파괴적 변경이 아님). 알 수 없는 키는 여전히 throw(fail-closed).
53
+
54
+ ### 2.1a 위성 호환성 전제 조건 (core 1.0.0 bump 전에 반드시 완료)
55
+
56
+ 네 위성 모두 `"haechi": ">=0.8.0 <1.0.0"`을 pin하고 있다 — 그리고 `<1.0.0`은 `1.0.0`을 **제외한다**(심지어 `1.0.0-rc.x`도). core를 1.0.0으로 bump하면 **모든 위성의 peer dependency를 충족 불가 상태로 만든다**(ERESOLVE / unmet peer). `haechi-auth-oidc`도 크로스-위성 동일 문제가 있다(`"haechi-auth-jwt": ">=0.2.0 <1.0.0"`). 따라서 **PR0**(어떤 core bump보다도 먼저):
57
+
58
+ - 모든 위성의 peer 범위를 다음 minor가 아닌 core **major**를 추적하도록 확장: `"haechi": ">=0.8.0 <2.0.0"`(동결의 정의상 유효 — ≥0.8로 빌드된 위성은 전체 1.x 라인에서 동작함), 그리고 `haechi-auth-oidc`의 `"haechi-auth-jwt": ">=0.2.0 <2.0.0"`. 네 위성 모두 패치 릴리스(`auth-jwt 0.2.x`, `crypto-kms 0.2.x`, `dashboard 0.1.x`, `auth-oidc 0.1.x`) + lockfile 재생성(workspace-lockfile 규칙 적용).
59
+ - `release:preflight` **게이트** 추가: 모든 `satellites/*/package.json` peer 범위를 파싱하여 발행할 core 버전에 대해 `semver.satisfies(coreVersion, range)`를 단언 — 미래의 core major가 위성이 여전히 제외하는 상태에서 출시되는 일을 방지.
60
+ - `api-stability.md §5`에 문서화: 위성 peer **상한은 core MAJOR를 추적**하며, 다음 minor 미만으로 pin되지 않는다.
61
+
62
+ ### 2.2 비대칭 서명 플러그인 계약 (Ed25519) + 핀닝 + revocation + 적합성(conformance)
63
+
64
+ **서명은 비대칭(Ed25519)이며, 대칭 `policy-bundle` HMAC이 아니다.** `policy-bundle`은 로컬 AES 키 파일로 keyed된 HMAC으로 서명한다 — 검증기가 서명하는 것과 동일한 비밀을 보유하므로 "제3자 저자가 서명하고 운영자가 공개 키로 검증한다"는 표현을 할 수 없다. 1.0은 **`node:crypto` Ed25519** 서명 매니페스트 프리미티브를 추가한다(새 의존성 없음): **저자가 Ed25519 개인 키를 보유**; **운영자가 Ed25519 공개 키를 신뢰 앵커(trust anchor)로 allowlist**. (플러그인 서명에 `policy-bundle`을 재사용하지 말 것.)
65
+
66
+ - **서명 봉투는 경로가 아닌 콘텐츠를 커버한다.** 서명 바이트는 `canonicalize({ pluginId, kind, version, capabilities, coreVersionRange, entrySha256, notBefore, notAfter })` — 즉 서명은 **정확한 엔트리 바이트의 sha256**, **kind**, **선언된 capabilities**, **호환 가능한 core 범위**, **유효 기간**을 바인딩한다. 경로에 서명하거나(`entrySha256`/`kind`/`capabilities`를 생략하는 것)는 교체(swap)/capability 다운그레이드 공격이 되므로 거부된다.
67
+ - **신뢰 앵커 전용 키 해석 (kid-by-claim 없음).** 검증 키는 **오직** 운영자의 `trustAnchors` allowlist에서만 해석된다; `manifest.signerKeyId`가 allowlist된 앵커에 없으면 **검증 전에 거부**한다. 알고리즘은 앵커별로 Ed25519로 고정된다(alg 민첩성 없음, HS/RS 혼동 없음). 플러그인 신뢰 앵커 세트는 **별도의 큐레이션된 목록**이며, AES 로테이션 키 파일과 절대 혼용하지 않는다(만료/로테이션된 AES kid가 서명자 앵커가 되어서는 안 됨).
68
+ - **핀닝 (악성 업데이트 방지 / 롤백 방지).** 운영자 config `plugin.pin = { version?, entrySha256?, manifestSha256? }`: 로더는 로드된 매니페스트 버전/엔트리 해시가 pin과 일치하지 않으면 fail-closed. **`pluginId`별 버전 플로어**는 이전 서명된 아티팩트로의 롤백을 거부한다. 따라서 *신뢰된 서명자*도 pin/플로어를 트리거하지 않고는 동일 앵커 하에 새(또는 구버전 취약한) 엔트리를 조용히 출시할 수 없다.
69
+ - **Revocation + 최신성.** 운영자 denylist `plugin.revokedSignerKeyIds` + `plugin.revokedEntrySha256`은 로드 시 확인됨(fail-closed: 취소된 서명자 또는 해시는 절대 로드되지 않음). 서명된 `notBefore`/`notAfter` 기간은 로드 시 강제됨. **메모리 내 revocation 동작 (1.0, 솔직하게 명시):** revocation은 **다음 로드/재시작 시**에 적용된다; **전역 kill-switch** (`plugins.enabled: false` 및 플러그인별 disable)로 운영자가 **라이브 플러그인을 즉시 강제 제거**할 수 있다. 라이브 CRL/피드는 1.x.
70
+ - **매 재시작(respawn)마다 재검증.** worker는 타임아웃-종료 후 지연 재시작되므로, **전체 게이트(서명 + 앵커 + pin + revocation + capability allowlist)가 최초 생성뿐 아니라 매 spawn마다 재실행**된다.
71
+ - **Capability allowlist (운영자 측).** `plugins.allowCapabilities`; 그 밖의 capability를 요청하는 매니페스트는 거부됨. `readsCredentials`는 `kind: authProvider`에서 **필수**다(bearer 토큰을 봄). `networkEgress`/`readsPlaintext`는 1.0에서 **선언되고 감사되지만 worker에 의해 강제되지 않는다**(§1 잔여 — 노출됨, 신뢰됨 아님).
72
+ - **적합성(conformance)은 정확성 게이트이지 악의 스크린이 아니다.** `assertAuthProviderConformance(provider, { now, vectors })`는 **샌드박스된** 플러그인을 열거된 보안 동작으로 실행한다: 크리덴셜 없음 → `null`; 형식 불량 크리덴셜 → `null`; 만료/아직 유효하지 않음(`now`를 통해 주입된 시각) → `null`; 내부 **throw는 호출자에게 `null`로 표면화**(절대 전파하지 않음); 반환된 identity는 반드시 `subjectHash`/`issuerHash`를 가져야 하며 raw 입력 subject/issuer와 동일한 필드를 **포함해서는 안 된다**(PII 안전성); 거부는 동일 입력에 대해 **결정론적**; 유효 크리덴셜 → 올바르게 형성된 PII-safe identity. 로더는 **이에 실패하는 플러그인의 연결을 거부한다**. 그러나 서명된 플러그인은 고정된 테스트를 감지하고 동작을 바꿀 수 있으므로: 적합성은 **로드마다 예측 불가능한 무작위 벡터**를 사용하며, — 핵심 — **호스트는 매 호출마다 PII 안전성을 재검증**한다(`buildExternalIdentity` + 아래 sanitizer가 요청별로 실행됨, 로드 시에만이 아님). **적합성 통과가 신뢰성을 의미하지 않는다**(그것은 서명+검증 게이트다); 테스트/프로덕션 분기는 수용된 잔여 위험이다(§6).
73
+
74
+ ### 2.3 `worker-isolated` `authProvider` 샌드박스 (MVP)
75
+
76
+ `createSandboxedAuthProvider({ manifestPath, trustAnchors, allowCapabilities, pin, revoked, cryptoProvider, auditSink, timeoutMs, maxPendingCalls, maxMessageBytes, resourceLimits, now })`는 동결된 계약을 만족하는 **호스트 측 `authProvider`**를 반환한다 — 따라서 **기존** 주입 심(seam)과 새 `auth.provider: "plugin"` config 경로를 통해 연결된다.
77
+
78
+ - **로드 시퀀스 (모든 단계에서 fail-closed, 각 단계 감사됨):** 매니페스트 검증(`worker-isolated` + `kind: authProvider`) → `signerKeyId`를 **`trustAnchors`에서만** 앵커 해석(아니면 거부) → **엔트리 바이트를 메모리로 읽어** sha256하고 **`entrySha256`을 포함하는 정규 봉투에 대해 Ed25519 서명 검증** → `notBefore/notAfter`, revocation denylist, pin/version-floor, capabilities ⊆ allowlist 확인 → **검증된 인메모리 소스에서** Worker를 spawn(`new Worker(code, { eval: true, resourceLimits, workerData: <비밀 없음> })`), **검증 후 경로를 재해석하지 않음**(TOCTOU 없음; symlink된 엔트리 거부) → 샌드박스된 provider에 대해 `assertAuthProviderConformance` 실행 → 그 이후에만 라이브 provider 반환. 실패 시 생성에서 throw하고 `plugin.load.refused{reason}`을 방출함(§2.4).
79
+ - **요청별 프로토콜 (데이터 최소화, correlation-id 적용):** `authenticate(request)`는 **크리덴셜 슬라이스**(`Authorization` 헤더/토큰 — 바디 절대 아님)만 추출하고, **고유한 correlation id**로 래핑하여 **MessagePort를 통해 JSON 문자열**로 post한다(structured-clone 객체 없음, `SharedArrayBuffer`/transferable 없음 → 공유 메모리 또는 객체 그래프 밀수 없음). `maxMessageBytes`가 와이어를 제한한다. worker는 크리덴셜을 검증하고(JWKS egress는 auth 플러그인에 고유) **raw 클레임** `{ subject, issuer, type, scopes, labels }` 또는 거부를 반환한다.
80
+ - **호스트 측 클레임 sanitizer (`buildExternalIdentity` 전에):** JSON 응답은 **null-prototype 객체**로 파싱된다(`JSON.parse` + `Object.create(null)`로 재구성); **고정된 own-enumerable 키 allowlist**만 허용됨; `__proto__`/`constructor`/`prototype` 제거됨; 배열 크기와 전체 identity 크기 제한됨; 모든 값은 경계에서 타입 검증/강제됨. 이후 **호스트**가 PII-safe identity를 구축한다(`buildExternalIdentity({ provider: "plugin:<pluginId>", subject, issuer, type, scopes, labels }, cryptoProvider)`) — keyed-HMAC 키는 worker에 진입하지 않으며, 적대적 클레임 객체가 prototype을 오염시키거나 raw 값을 밀수할 수 없다.
81
+ - **동시성 모델 (호출자 간 누출 없음 / 종료 경쟁 없음):** 각 in-flight 호출은 **correlation id**로 응답과 매칭됨; 일치하지 않는/중복된/늦은 응답은 **삭제됨**. worker는 **단일 점유(single-occupancy)**(하나의 in-flight 호출) — 따라서 호출별 타임아웃-종료는 *형제* 호출을 절대 죽이지 않음; 대기 호출 **상한(`maxPendingCalls`)**이 동시성을 제한함(초과 → 거부). 종료 후 재시작은 **single-flight**으로 보호됨. 플러그인은 **호출 간 무상태(stateless)**여야 하며; 잔여 크로스-요청 상태 위험은 §6 잔여다.
82
+ - **타임아웃 + 리소스 제한 (fail-closed):** 각 호출은 `timeoutMs`(필수 양의 정수 — 무한 기본값 없음)로 제한됨; 타임아웃 시 호스트는 worker를 **종료**(`plugin.worker.terminated{cause: timeout}`)하고 `null`을 반환하며 지연 재시작함(전체 게이트 재실행). `resourceLimits`가 힙을 제한함. (CPU/fd/소켓은 1.0에서 제한되지 않음 — §6 잔여.)
83
+ - **Config (`auth.provider: "plugin"`) — 열거형 fail-closed `normalizeConfig` 규칙** (`keys`/`tokenVault` 엄격함과 일치): `plugin.manifestPath`(비어 있지 않은 로컬 경로) 필수; `plugin.trustAnchors` 비어 있지 않은 `{ keyId: string, publicKey: string (Ed25519) }` 배열; `plugin.allowCapabilities` `CAPABILITY_KEYS ∪ {readsCredentials}` 부분집합인 배열(알 수 없는 것 거부); `kind: authProvider`에 `readsCredentials` 존재; `plugin.timeoutMs` 양의 정수; `resourceLimits.maxOldGenerationSizeMb` 양의 정수; 선택적 `plugin.pin`/`plugin.revoked*`/version-floor 올바르게 형성됨; `plugins.enabled` 준수(kill-switch). 모든 위반은 로드 시 throw. `createRuntime`은 호스트 측 identity 구축을 위해 주입된 `cryptoProvider`를 여전히 필요로 한다.
84
+
85
+ ### 2.4 플러그인 라이프사이클 감사 (보안 제품은 서드파티 코드 로딩을 반드시 기록해야 함)
86
+
87
+ 기존 해시-체인 `auditSink`를 재사용하여(`recordProxyDecision`/`auth_denied`가 이미 사용하는 동일 심), 샌드박스는 **PII-safe** 이벤트를 방출한다 — id/해시/카운트만:
88
+
89
+ - `plugin.load.accepted` `{ pluginId, version, entrySha256, signerKeyId, capabilitiesGranted }`
90
+ - `plugin.load.refused` `{ reason ∈ missing-signature | unknown-signer | tampered-entry | revoked | below-version-floor | pin-mismatch | expired-window | capability-not-allowlisted | conformance-failed | manifest-invalid, pluginId?, signerKeyId? }`
91
+ - `plugin.authenticate.deny` `{ pluginId, reason ∈ deny | invalid-claims | timeout | over-capacity | oversized }`
92
+ - `deny` — 플러그인이 일반 거부를 반환함 (worker 하네스가 변환한 내부 throw 포함)
93
+ - `invalid-claims` — 호스트 측 클레임 sanitize 또는 `buildExternalIdentity` 거부 (이전 `non-pii-safe-identity` 레이블 통합)
94
+ - `timeout` — 호출별 타임아웃 만료; worker 종료 및 재시작
95
+ - `over-capacity` — `maxPendingCalls` 초과; worker 큐에 진입하기 전에 호출 거부
96
+ - `oversized` — 크리덴셜 메시지가 `maxMessageBytes`를 초과; worker에 전송되지 않음
97
+ - `plugin.worker.terminated` `{ pluginId, cause ∈ timeout | oom | crash }`
98
+
99
+ `FORBIDDEN_KEYS`는 플러그인/클레임 표면(`claims`, `subject`, `issuer`, `credential`, `authorization`, `signature`, `entry`)으로 **확장**된다 — 심층 방어로서, 미래의 플러그인 이벤트가 raw 클레임/토큰/서명자 비밀을 체인 로그에 절대 누출하지 못하도록(위의 이벤트는 이미 id/해시만 운반함). 테스트는 거부된 로드와 worker 타임아웃이 각각 정확히 하나의 체인 이벤트를 방출함을 단언하고, raw 클레임이 있는 합성 플러그인 이벤트가 `sanitizeAudit`에 의해 제거됨을 단언한다.
100
+
101
+ ### 2.5 실제 환경 검증 종료 기준
102
+
103
+ - **충족됨:** 2026-06-11 실제 자체 호스팅 vLLM + Ollama([[2026-06-11-real-environment-validation]]) + `haechi-dashboard` 관측가능성에 대한 라이브 검증.
104
+ - **잔여 (문서화됨, 1.0 게이팅 아님):** (1) **라이브 KMS 백엔드 검증** (실제 AWS/GCP/Azure/Vault)은 CI 밖; (2) **worker 플러그인 샌드박스 자체는 실제 적대적 플러그인에 대해 미검증** — 보안은 신뢰 게이트 + §6 잔여에 기반하며, fail-closed/데이터 최소화 테스트로 검증됨(적대적 서드파티 플러그인 레드팀이 아닌 — 이상적으로는 child-process+permission 강제와 함께 1.x 과제).
105
+
106
+ ## 3. 명시적 비범위 (1.x로 연기)
107
+
108
+ - 악성 서명된 플러그인에 대한 **Capability *강제*** (fs/net 차단, 크리덴셜 봉쇄) — child-process 격리와 Node 권한 모델이 필요.
109
+ - **Classifier/filter 및 crypto 플러그인 로딩** — 1.0에서는 `authProvider`만.
110
+ - **라이브 revocation 피드 / CRL**, 플러그인 **레지스트리 / 마켓플레이스**, multi-origin, 핫 리로드, **미서명 dev 로더** (신뢰 게이트를 훼손하게 됨 — 개발은 주입을 사용).
111
+ - **Python SDK.**
112
+
113
+ ## 4. 하위 호환 & 1.0 안정성 계약
114
+
115
+ 기존 동작은 **불변** — 모든 provider 계약, config와 (이제 중첩 열거된) 감사 스키마, zero-dependency 자세가 0.9와 정확히 동일하다; 이것들이 **동결로 선언된다**. 플러그인 샌드박스는 **순수 가산적이며 opt-in**이다(`auth.provider: "plugin"`; 기본값은 `none`/`bearer`/`external`로 유지). 하나의 동작적 core 변경은 **가산적 `FORBIDDEN_KEYS` 확장**(§2.4)과 **`schemaVersion`** 필드(가산적)다. **위성 peer 범위 확장(§2.1a)은 전제 조건**으로, 네 위성이 core 1.0.0에 대해 설치를 유지하도록 한다.
116
+
117
+ ## 5. 1.0 관계 / 1.0이 닫는 것
118
+
119
+ 1.0은 두 오랜 1.0 게이트를 닫는다 — **API 안정성 동결**(§2.1)과 **플러그인 샌드박스 + 동적 로딩 스토리**(§2.2–2.4: 비대칭 서명 + 격리 + 감사 + auth 전용) — 그리고 **실제 환경 검증** 종료 기준이 문서화된 잔여와 함께 충족됨을 기록한다(§2.5). Haechi를 개발자 preview에서 안정적인 자체 호스팅 보안 게이트웨이로 졸업시키면서 core 약속을 유지한다: 작고 zero-dependency인 core, 모든 곳에서 fail-closed, "컴포넌트를 교체해도 동일한 보안 테스트가 통과된다."
120
+
121
+ ## 6. 위협 모델 & 리스크 레지스터 델타 (구체적)
122
+
123
+ | 신규 표면 (1.0) | 통제 | 잔여 |
124
+ |---|---|---|
125
+ | **악성/손상된 서명된 플러그인** 동적 로딩 | `entrySha256`+kind+capabilities에 대한 Ed25519 서명, 신뢰 앵커 전용 키 해석, pin + version-floor + revocation denylist, conformance 게이트, worker 메모리/크래시 격리, 전체 라이프사이클 감사 | **서명된 플러그인 자체의 fs/net/`process.env`는 차단되지 않으며, 수신하는 크리덴셜을 유출할 수 있다** — 서명/검증 신뢰 모델에 의해서만 게이트됨; 진정한 강제는 1.x child-process+permission 경로 |
126
+ | **플러그인으로의 PII/비밀 누출** | 크리덴셜 슬라이스만 전달됨(바디/키 절대 아님); JSON-string 와이어; null-proto sanitizer; 호스트가 keyed-HMAC identity 구축 | auth 플러그인이 합법적으로 검증하는 크리덴셜은 그것에 가시적임(위 행 참조) |
127
+ | **경계 간 객체/proto 밀수** | JSON-string 와이어(structured clone / SAB / transferable 없음) + `buildExternalIdentity` 전 null-proto allowlist sanitizer | 실질적 잔여 없음 |
128
+ | **엔트리 교체 / TOCTOU** | `entrySha256` 서명; 인메모리 읽기 + 해시 + 검증 + 인메모리 소스에서 spawn; 경로 재해석 없음; symlink 거부 | 실질적 잔여 없음 |
129
+ | **서명자 키 혼동 / 다운그레이드 / 롤백 / 악성 업데이트** | 신뢰 앵커 전용 해석, 알고리즘 고정, pin/version-floor, revocation | 운영자가 앵커/pin을 큐레이션해야 함 |
130
+ | **플러그인 DoS** | 호출별 `timeoutMs` 종료, 힙 `resourceLimits`, `maxPendingCalls`, `maxMessageBytes`, 단일 점유 worker | 서명된 플러그인이 타임아웃 내 할당된 CPU를 소진할 수 있음(CPU/fd는 1.0에서 제한되지 않음) |
131
+ | **미감사 코드 로드** | `plugin.load.*` / `authenticate.deny` / `worker.terminated` 감사 이벤트; 확장된 `FORBIDDEN_KEYS` | — |
132
+ | **적합성 테스트/프로덕션 분기** | 로드마다 무작위화된 벡터 + 호출별 호스트 PII 안전성 재검증 | 악성 플러그인이 적합성을 통과한 후 오동작 가능(서명+검증으로 커버되며 적합성으로 아님) |
133
+ | **API/감사 스키마 드리프트** | 엄격한 semver + 지원 중단 기간(+ 보안 예외) + 가산적 전용 중첩 열거 감사 스키마 + `schemaVersion` | major bump는 설계상 파괴적 변경 가능(문서화된 마이그레이션) |
134
+
135
+ 제안 리스크 ID: **P1-SEC-024**(동적 플러그인 실행 / 샌드박스 신뢰 모델 — P1-SEC-004의 매니페스트 전용 입장을 새 통제 하에 수퍼세드, 해제됨), **P1-SEC-025**(플러그인 서명/신뢰 앵커/revocation 라이프사이클), **P2-API-001**(안정적 계약 동결 + 지원 중단 정책), **P2-OPS-006**(위성 peer 범위 / major 추적 게이트). 신규 §4 제외: 악성 서명된 플러그인에 대한 capability 강제, 크리덴셜 봉쇄, classifier/crypto 플러그인 로딩, 미서명 dev 로더, 라이브 CRL.
136
+
137
+ ## 7. 테스트 기준 (PR 분해에 매핑)
138
+
139
+ ### 7.1 PR0 — 위성 peer 범위 확장 + preflight 게이트
140
+
141
+ - 네 위성의 `haechi` peer 범위가 `>=0.8.0 <2.0.0`으로 확장됨(그리고 auth-oidc의 `haechi-auth-jwt`는 `<2.0.0`); lockfile 재생성; `release:preflight`가 위성의 범위가 `!semver.satisfies(coreVersionToPublish, satelliteRange)`이면 실패. 테스트가 core `1.0.0`을 시뮬레이션하고 모든 위성 범위가 충족됨을 단언.
142
+
143
+ ### 7.2 PR1 — API 안정성 동결 (문서 + 계약 테스트)
144
+
145
+ - `api-stability.md`(+ko)가 IN/OUT 테이블, 엄격한 semver + 지원 중단 정책(`HAECHI_DEPRECATION_*` 런타임 경고 계약 및 보안 예외 포함), 위성 major 추적 규칙을 담음.
146
+ - **계약/스냅샷 테스트**가 서브패스별 동결된 export + **non-null `identity`와 하나의 `detections[]` 항목을 포함하는 전체 감사 이벤트**(중첩 하위 스키마가 최상위 레벨만이 아닌 것으로 보호됨) + config 스키마 키 세트 + `schemaVersion`을 pin함. 가산적 필드는 통과; 제거/이름 변경된 필드(최상위 또는 중첩)는 실패. `verifyAuditChain`이 동결 스키마 픽스처를 검증하고 합성 가산 필드가 있어도 여전히 검증함.
147
+
148
+ ### 7.3 PR2 — Ed25519 서명 플러그인 계약 + 핀닝/revocation + 적합성 하네스
149
+
150
+ - `packages/plugin`이 Ed25519 봉투와 함께 `worker-isolated`+`authProvider` 매니페스트를 수락; **거부**(각각 `plugin.load.refused{reason}`를 방출하는 별개의 fail-closed 테스트): 누락/무효 서명; `trustAnchors`에 없는 서명자(kid-not-allowlisted, **검증 전**에 해석됨); **서명 후 엔트리 바이트 변조, 경로 변경 없음**; revoked 서명자 / revoked entryHash; version-floor 미달; pin 불일치; `notBefore/notAfter` 외부; capability allowlist에 없음; alg ≠ Ed25519.
151
+ - `assertAuthProviderConformance` 존재; 참조 provider 통과; 깨진 것(throw / raw-subject identity 반환 / 만료 크리덴셜 수락 / 비결정론적)이 각 경우마다 **실패**(네거티브 테스트). 벡터는 실행마다 무작위화됨.
152
+ - `FORBIDDEN_KEYS` 확장 테스트: `claims`/`credential`/`signature`가 있는 합성 플러그인 이벤트가 `sanitizeAudit`에 의해 제거됨; 체인이 유효한 상태로 유지됨.
153
+
154
+ ### 7.4 PR3 — `worker-isolated` authProvider 샌드박스
155
+
156
+ - 참조 **서명된** auth 플러그인이 로드되어 worker 내에서 적합성 통과, 유효 bearer/JWT를 **호스트가 구축한 PII-safe identity**로 인증함; 단언: worker는 **크리덴셜 슬라이스만** 수신함(계측된 echo-plugin이 바디/감사 싱크/토큰 볼트/키를 절대 받지 못했음을 증명), raw subject가 감사에 나타나지 않음, `plugin.load.accepted`가 해석된 `entrySha256`/`signerKeyId`와 함께 방출됨.
157
+ - **Fail-closed + 격리 매트릭스:** 미서명/잘못된 서명자/변조/revoked/pin 불일치/capability-not-allowlisted → 생성 throw + `load.refused`; **타임아웃 → `null` + worker 종료 + `worker.terminated{timeout}`**; throw → `null`; `__proto__`/추가 키가 있는 클레임 객체 → sanitize됨(prototype 오염 없음, 추가 키 제거됨) 및 PII-safe; 별개의 correlation id를 가진 두 동시 호출이 절대 응답을 교차하지 않음; 하나의 호출 종료가 형제를 죽이지 않음(단일 점유); `maxPendingCalls`/`maxMessageBytes` 강제됨; `plugins.enabled:false`(kill-switch)가 로드를 거부함.
158
+ - `normalizeConfig` `auth.provider:"plugin"` 열거형 fail-closed 테스트(각 잘못된 옵션이 throw); `createRuntime` + proxy auth 게이트를 통한 end-to-end(요청이 플러그인을 통해 인증됨; identity keyed-HMAC; 감사에 raw subject/크리덴셜 없음).
159
+
160
+ ### 7.5 전체
161
+
162
+ - Core가 zero runtime dependency를 유지함(`node:`만 — Ed25519는 `node:crypto`); `check:packaging` + `check:satellite-packaging` 통과; 동결된 계약 스냅샷 테스트 + peer 범위 preflight 게이트가 미래 PR을 보호함.
163
+
164
+ ## 8. 제안 PR 분해 (스택)
165
+
166
+ 1. **PR0 — 위성 peer 범위 확장 + preflight 게이트** (전제 조건; 네 위성 패치 릴리스). → §7.1
167
+ 2. **API 동결** — `api-stability.md`(+ko) IN/OUT 테이블 + 지원 중단/보안 예외 정책 + 중첩 스키마 계약/스냅샷 테스트 + `schemaVersion`. → §7.2
168
+ 3. **Ed25519 서명 플러그인 계약 + 적합성** — 비대칭 프리미티브(`node:crypto`), 서명 봉투(entryHash/kind/capabilities/기간), 신뢰 앵커 전용 해석, pin/version-floor/revocation, `assertAuthProviderConformance`, `FORBIDDEN_KEYS` 확장. → §7.3
169
+ 4. **Worker 격리 authProvider 샌드박스** — `createSandboxedAuthProvider`(인메모리 검증 spawn, JSON-string 와이어, null-proto sanitizer, correlation-id 단일 점유 동시성, 타임아웃/종료, kill-switch), `auth.provider:"plugin"` config 분기 + 라이프사이클 감사, 참조 서명된 플러그인 + §7.4 매트릭스. → §7.4
170
+ 5. **1.0.0 릴리스 컷** — core를 **1.0.0**으로 bump; docs EN/KO (이 범위 문서, 위협 모델 + §6 ID + 목표 버전 bump와 함께 리스크 레지스터 델타, 실제 환경 종료 기준 + 잔여); wiki ingest(`[[plugin-sandbox]]` 페이지 + `[[packaging-and-distribution]]`/`[[identity-and-auth]]`/`release-roadmap` 업데이트); README "Current Scope". Core는 `v*` 태그를 재사용; 첫 번째 안정 `haechi@1.0.0`이 증명(attested)과 함께 발행됨. (PR0이 이미 머지되고 위성이 재발행되어 1.0.0에 대해 설치 가능해야 함.)
@@ -0,0 +1,164 @@
1
+ # Haechi 1.0 Implementation Scope
2
+
3
+ - Status: Draft 0.2 (design — not yet implemented; hardened after a 3-lens adversarial security review, 2026-06-11)
4
+ - Date: 2026-06-11
5
+ - Target version: 1.0.0 (after 0.9.0)
6
+ - Type: stable API contract + plugin sandbox (the first stable release)
7
+
8
+ ## 1. Release Goal
9
+
10
+ 1.0 is the **first stable release**: it (a) **freezes a stable public API contract** with a deprecation/migration policy and a long-term audit schema, and (b) crosses the line the project has deliberately held since 0.1 — **dynamic loading of external plugin code** — but only through an **asymmetrically-signed, capability-gated, `worker_threads`-isolated, audited** sandbox, and only for the **`authProvider`** contract to start.
11
+
12
+ **Scope decisions (2026-06-11, confirmed with the maintainer):**
13
+
14
+ 1. **Sandbox/loading model:** dynamic loading is enabled **only** for plugins that are **signed (Ed25519, asymmetric)**, pass a **capability-manifest allowlist + operator pin/revocation checks**, and run in a **`node:worker_threads` isolation** boundary with full **lifecycle auditing**. `createRuntime(config, providers)` **injection remains the default and recommended path**.
15
+ 2. **Plugin scope:** **`authProvider` only** in 1.0. Classifier/filter and crypto plugins stay injection-only until 1.x.
16
+ 3. **API freeze:** **strict** — the core public API, the **provider contracts**, the **audit event schema** (including nested sub-schemas), and the **config schema** are frozen under strict semver with a deprecation policy.
17
+ 4. **Release shape:** **staged** — 1.0.0 ships the API freeze + the signed-plugin contract/conformance/signing + the worker-isolated `authProvider` sandbox MVP. Stronger capability **enforcement** (child-process + the Node permission model), more plugin kinds, a live revocation feed, and a registry are 1.x.
18
+
19
+ Core stays **zero runtime dependency** — the sandbox is built on `node:worker_threads` + `node:crypto` (Ed25519 sign/verify is a `node:crypto` builtin). It does **not** reuse `packages/policy-bundle` (that is symmetric HMAC — see §2.2).
20
+
21
+ ### The honest security model (read this first)
22
+
23
+ **`node:worker_threads` is NOT a security sandbox against malicious code.** A worker shares the process and can still touch the filesystem, the network, and `process.env`; isolation is **V8-heap-only** (Node's permission model is process-wide, not per-worker; `SharedArrayBuffer`/transferables would even reopen a shared-memory channel, so the wire format is a plain JSON string — §2.3). The 1.0 sandbox therefore provides:
24
+
25
+ - **Memory isolation** — separate V8 heap; the plugin cannot read/corrupt host memory, the crypto keys, the token vault, or the audit sink (only the typed message channel crosses).
26
+ - **Crash/hang isolation + resource limits** — `resourceLimits` (heap cap) + a per-call **timeout that terminates the worker** contain a buggy/runaway plugin; a hang fails closed (deny).
27
+ - **Data minimization** — the host sends the worker **only the credential slice** (the `Authorization` header / bearer token), **never the request body** and **never the crypto key**; the worker returns **raw claims**, and the **host** builds the PII-safe identity via `buildExternalIdentity` (the keyed-HMAC key never leaves the host).
28
+ - **A narrow, audited, typed contract** — the worker speaks only the `authProvider` message protocol; every load/deny/terminate decision is audited (§2.4).
29
+
30
+ What the worker boundary does **NOT** give you in 1.0 — these are **accepted residuals, gated only by the signing/vetting trust model**, not by the worker (§6):
31
+
32
+ - **A malicious *signed* plugin can still use the OS** — `fetch`, `fs`, `process.env` are not blocked. `networkEgress: false` in the manifest is a *declaration*, not an enforced control in 1.0.
33
+ - **A malicious *signed* auth plugin can exfiltrate the live credential** it legitimately receives (the bearer token), because it has de-facto network egress. There is **no technical barrier** in 1.0 — only the trust gate.
34
+
35
+ True per-plugin capability **enforcement** (block fs/net, contain the credential) requires **child-process isolation under the Node permission model** (`--permission --allow-fs-read=…`), the documented **1.x** path. This is why injection stays the default and why the trust gate (asymmetric signature + operator allowlist + pin + revocation) is load-bearing.
36
+
37
+ ## 2. Scope
38
+
39
+ ### 2.1 API stability freeze (the 1.0 contract)
40
+
41
+ **Frozen public surface (an explicit IN/OUT table replaces today's vague "0.x is preview").** Every `package.json` `exports` subpath and the CLI is classed:
42
+
43
+ | Surface | 1.0 status |
44
+ |---|---|
45
+ | `haechi` / `haechi/core` (`createRuntime`, `createHaechi().protectJson`, `collectStringEntries`), `haechi/auth` (`authProvider` contract, `buildExternalIdentity`, `buildIdentity`, `validateLabels`), `haechi/crypto` (`cryptoProvider` contract, `assertCryptoProviderConformance`, `canonicalize`), `haechi/audit` (event schema, `verifyAuditChain`, `sanitizeAudit`, `FORBIDDEN_KEYS`), `haechi/policy`, `haechi/filter` (rule shape), `haechi/token-vault`, `haechi/runtime` (`normalizeConfig` shape), `haechi/protocol-adapters`, `haechi/plugin` (manifest + the new sandbox) | **FROZEN** (breaking change = major) |
46
+ | `haechi/proxy`, `haechi/mcp-stdio`, `haechi/stream-filter`, `haechi/policy-bundle`, `haechi/privacy-profiles`, and the **CLI** (`bin/haechi.mjs`) | **FROZEN BEHAVIOR + wire/contract**; human-readable CLI/log **text** may still change (not part of the contract) |
47
+ | anything still marked experimental in `api-stability.md §3` | must be **graduated** (and removed from §3) or **explicitly kept preview past 1.0** with a stated reason — no silent ambiguity |
48
+
49
+ - **Strict semver from 1.0** (breaking→major, additive→minor, fix→patch). The "0.x minor may break" latitude ends for core.
50
+ - **Deprecation policy.** A deprecated export/field/option is kept **≥1 minor**, emits a documented migration note and a one-time runtime `process.emitWarning` with a **stable `code` prefix `HAECHI_DEPRECATION_*`** (the code/text are themselves part of the contract), and is removed only at the **next major**. **Security exception (the one sanctioned in-minor break):** a change required to close a *disclosed* vulnerability may break/remove within a **minor**, shipped with a security advisory + a migration path (mirroring the existing "blocking unsafe config may tighten in a patch" latitude).
51
+ - **Audit event schema — frozen including nested sub-schemas**, enumerated (not just the top level): top-level `{id, timestamp, protocol, operation, identity, profile, mode, enforced, blocked, payloadShapeHash, detections, summary, auditIntegrity}`; `detections[].{type, ruleId, path, kind, confidence, action, enforced}`; **`identity.{id, type, subjectHash, issuerHash, provider}`** (the PII-safe projection — `scopes`/`labels`/raw subject are **NOT** part of the audit identity); `summary.{byType, byAction, detectionCount}`; `auditIntegrity.{alg, canonicalization, sequence, previousHash, eventHash}`. **New fields are additive-only and never change the canonicalization of existing fields**, so a 1.x event still verifies under a 1.0 `verifyAuditChain` (this holds because `canonicalize` hashes the literal object and the verifier recomputes over the *same* stored object — the guarantee is "a future-additive field doesn't break an old verifier reading a new record", which is sound; the doc states this precisely rather than the earlier hand-wave). A canonicalization change is a **major** event-schema bump with a new `canonicalization` tag + a reader-migration path. An explicit top-level **`schemaVersion`** is added (reader-facing; additive) so consumers branch on it without parsing `auditIntegrity`.
52
+ - **Config schema freeze unit:** config **key presence + shape** is frozen; **default values may still be hardened** (a safer default is not a breaking change). Unknown keys still throw (fail-closed).
53
+
54
+ ### 2.1a Satellite compatibility prerequisite (must land BEFORE the core 1.0.0 bump)
55
+
56
+ All four satellites pin `"haechi": ">=0.8.0 <1.0.0"` — and `<1.0.0` **excludes `1.0.0`** (and even `1.0.0-rc.x`). Bumping core to 1.0.0 makes **every satellite's peer dependency unsatisfiable** (ERESOLVE / unmet peer). `haechi-auth-oidc` has the same problem cross-satellite (`"haechi-auth-jwt": ">=0.2.0 <1.0.0"`). So **PR0** (before any core bump):
57
+
58
+ - Widen every satellite peer range to track the core **major**, not the next minor: `"haechi": ">=0.8.0 <2.0.0"` (valid by definition of the freeze — a satellite built against ≥0.8 works through the whole 1.x line), and `haechi-auth-oidc`'s `"haechi-auth-jwt": ">=0.2.0 <2.0.0"`. Patch-release all four (`auth-jwt 0.2.x`, `crypto-kms 0.2.x`, `dashboard 0.1.x`, `auth-oidc 0.1.x`) + regenerate the lockfile (the workspace-lockfile gotcha applies).
59
+ - Add a **`release:preflight` gate** that parses every `satellites/*/package.json` peer range and asserts `semver.satisfies(coreVersion, range)` for the core version about to publish — so a future core major can never ship while a satellite still excludes it.
60
+ - Document in `api-stability.md §5`: the satellite peer **upper bound tracks the core MAJOR**, never pinned below the next minor.
61
+
62
+ ### 2.2 Asymmetric signed-plugin contract (Ed25519) + pinning + revocation + conformance
63
+
64
+ **Signing is asymmetric (Ed25519), NOT the symmetric `policy-bundle` HMAC.** `policy-bundle` signs with HMAC keyed off the local AES key file — the verifier holds the same secret that signs, so it cannot express "a third-party author signed; the operator verifies with a public key." 1.0 adds a **`node:crypto` Ed25519** signed-manifest primitive (zero new dependency): the **author holds the Ed25519 private key**; the **operator allowlists the Ed25519 public key** as a trust anchor. (Do not reuse `policy-bundle` for plugin signing.)
65
+
66
+ - **The signed envelope covers content, not a path.** The signed bytes are `canonicalize({ pluginId, kind, version, capabilities, coreVersionRange, entrySha256, notBefore, notAfter })` — i.e. the signature binds the **sha256 of the exact entry bytes**, the **kind**, the **declared capabilities**, the **compatible core range**, and a **validity window**. Signing a path (or omitting `entrySha256`/`kind`/`capabilities`) is a swap / capability-downgrade attack and is rejected.
67
+ - **Trust-anchor-only key resolution (no kid-by-claim).** The verification key is resolved **only** from the operator's `trustAnchors` allowlist; if `manifest.signerKeyId` is not an allowlisted anchor, **refuse before any verify**. The algorithm is pinned to Ed25519 per anchor (no alg agility, no HS/RS confusion). The plugin trust-anchor set is a **separate curated list**, never the AES rotation key file (retired/rotated AES kids must not become signer anchors).
68
+ - **Pinning (anti malicious-update / rollback).** Operator config `plugin.pin = { version?, entrySha256?, manifestSha256? }`: the loader fails closed if the loaded manifest version / entry hash does not match the pin. A **per-`pluginId` version floor** rejects rollback to an older signed artifact. So a *trusted signer* cannot silently ship a new (or old-vulnerable) entry under the same anchor without tripping the pin/floor.
69
+ - **Revocation + freshness.** Operator denylists `plugin.revokedSignerKeyIds` + `plugin.revokedEntrySha256` checked at load (fail-closed: a revoked signer or hash never loads). The signed `notBefore`/`notAfter` window is enforced at load. **In-memory behavior on revocation** (1.0, stated honestly): revocation takes effect at the **next load/restart**; a **global kill-switch** (`plugins.enabled: false`, and a per-plugin disable) lets an operator **force-drop a live plugin** immediately. A live CRL/feed is 1.x.
70
+ - **Re-verify on every respawn.** Because workers are lazily respawned after a timeout-terminate, the **full gate (signature + anchor + pin + revocation + capability allowlist) re-runs on every spawn**, not only at first construction.
71
+ - **Capability allowlist (operator-side).** `plugins.allowCapabilities`; a manifest requesting a capability outside it → refused. `readsCredentials` is **required** for `kind: authProvider` (it sees the bearer token). `networkEgress`/`readsPlaintext` are **declared and audited but not enforced** by the worker in 1.0 (the §1 residual — surfaced, not trusted).
72
+ - **Conformance is a CORRECTNESS gate, not a malice screen.** `assertAuthProviderConformance(provider, { now, vectors })` runs the **sandboxed** plugin through enumerated security behaviors: missing credential → `null`; malformed credential → `null`; expired / not-yet-valid (clock injected via `now`) → `null`; an internal **throw surfaces as `null`** to the caller (never propagates); a returned identity **MUST** carry `subjectHash`/`issuerHash` and **MUST NOT** contain a field equal to the raw input subject/issuer (PII-safety); deny is **deterministic** for identical input; a valid credential → a well-formed PII-safe identity. The loader **refuses to wire a plugin that fails**. But a signed plugin can detect a fixed test and behave, so: conformance uses **unpredictable per-load randomized vectors**, and — load-bearing — the **host re-validates PII-safety on every call** (`buildExternalIdentity` + the sanitizer below run per request), not just at load. **Conformance-pass does not imply trustworthiness** (that is the signing+vetting gate); test/prod divergence is an accepted residual (§6).
73
+
74
+ ### 2.3 The `worker-isolated` `authProvider` sandbox (the MVP)
75
+
76
+ `createSandboxedAuthProvider({ manifestPath, trustAnchors, allowCapabilities, pin, revoked, cryptoProvider, auditSink, timeoutMs, maxPendingCalls, maxMessageBytes, resourceLimits, now })` returns a **host-side `authProvider`** satisfying the frozen contract — so it wires through the **existing** injection seam and the new `auth.provider: "plugin"` config path.
77
+
78
+ - **Load sequence (fail-closed at every step, each step audited):** validate manifest (`worker-isolated` + `kind: authProvider`) → resolve the anchor by `signerKeyId` **from `trustAnchors` only** (else refuse) → read the **entry bytes into memory**, sha256, and **verify the Ed25519 signature over the canonical envelope incl. `entrySha256`** → check `notBefore/notAfter`, the revocation denylists, the pin/version-floor, and capabilities ⊆ allowlist → spawn the Worker **from the in-memory verified source** (`new Worker(code, { eval: true, resourceLimits, workerData: <no secrets> })`), **never re-resolving the path** after verification (no TOCTOU; refuse a symlinked entry) → run `assertAuthProviderConformance` against the sandboxed provider → only then return the live provider. Any failure throws at construction and emits `plugin.load.refused{reason}` (§2.4).
79
+ - **Per-request protocol (data-minimized, correlation-id'd):** `authenticate(request)` extracts **only** the credential slice (the `Authorization` header / token — never the body), wraps it with a **unique correlation id**, and posts it as a **JSON string over the MessagePort** (no structured-clone objects, no `SharedArrayBuffer`/transferables → no shared-memory or object-graph smuggling). `maxMessageBytes` bounds the wire. The worker validates the credential (JWKS egress is inherent to an auth plugin) and returns **raw claims** `{ subject, issuer, type, scopes, labels }` or a deny.
80
+ - **Host-side claims sanitizer (before `buildExternalIdentity`):** the JSON reply is parsed into a **null-prototype object** (`JSON.parse` + reconstruct onto `Object.create(null)`); only a **fixed allowlist of own-enumerable keys** is accepted; `__proto__`/`constructor`/`prototype` are stripped; array sizes and total identity size are bounded; every value is type-validated/coerced at the boundary. Then the **host** builds the PII-safe identity (`buildExternalIdentity({ provider: "plugin:<pluginId>", subject, issuer, type, scopes, labels }, cryptoProvider)`) — the keyed-HMAC key never enters the worker, and a hostile claims object cannot pollute the prototype or smuggle a raw value.
81
+ - **Concurrency model (no cross-caller leakage / no terminate races):** each in-flight call is matched to its reply **by correlation id**; unmatched / duplicate / late replies are **dropped**. The worker is **single-occupancy** (one in-flight call) — a per-call timeout-terminate can therefore never kill a *sibling* call; a pending-call **cap (`maxPendingCalls`)** bounds concurrency (excess → deny). Respawn after terminate is guarded **single-flight**. Plugins are required to be **stateless across calls**; any residual cross-request state risk is a §6 residual.
82
+ - **Timeout + resource bound (fail-closed):** each call is bounded by `timeoutMs` (a **required positive integer — no unbounded default**); on timeout the host **terminates the worker** (`plugin.worker.terminated{cause: timeout}`) and returns `null`, respawning lazily (re-running the full gate). `resourceLimits` caps the heap. (CPU/fd/socket are *not* bounded in 1.0 — §6 residual.)
83
+ - **Config (`auth.provider: "plugin"`) — enumerated fail-closed `normalizeConfig` rules** (matching the keys/tokenVault rigor): require `plugin.manifestPath` (non-empty local path); `plugin.trustAnchors` a non-empty array of `{ keyId: string, publicKey: string (Ed25519) }`; `plugin.allowCapabilities` an array ⊆ `CAPABILITY_KEYS ∪ {readsCredentials}` (reject unknown); `readsCredentials` present for `kind: authProvider`; `plugin.timeoutMs` a positive integer; `resourceLimits.maxOldGenerationSizeMb` a positive integer; optional `plugin.pin`/`plugin.revoked*`/version-floor well-formed; `plugins.enabled` honored (kill-switch). Any violation throws at load. `createRuntime` still requires the injected `cryptoProvider` for the host-side identity build.
84
+
85
+ ### 2.4 Audit of the plugin lifecycle (a security product MUST record loading third-party code)
86
+
87
+ Reusing the existing hash-chained `auditSink` (the same seam `recordProxyDecision`/`auth_denied` already uses), the sandbox emits **PII-safe** events — ids/hashes/counts only:
88
+
89
+ - `plugin.load.accepted` `{ pluginId, version, entrySha256, signerKeyId, capabilitiesGranted }`
90
+ - `plugin.load.refused` `{ reason ∈ missing-signature | unknown-signer | tampered-entry | revoked | below-version-floor | pin-mismatch | expired-window | capability-not-allowlisted | conformance-failed | manifest-invalid, pluginId?, signerKeyId? }`
91
+ - `plugin.authenticate.deny` `{ pluginId, reason ∈ deny | invalid-claims | timeout | over-capacity | oversized }`
92
+ - `deny` — plugin returned a plain deny (including a worker-harness-converted internal throw)
93
+ - `invalid-claims` — host-side claims sanitize or `buildExternalIdentity` rejection (subsumes the earlier `non-pii-safe-identity` label)
94
+ - `timeout` — per-call timeout expired; worker terminated and respawned
95
+ - `over-capacity` — `maxPendingCalls` exceeded; call rejected before entering the worker queue
96
+ - `oversized` — credential message exceeds `maxMessageBytes`; not sent to the worker
97
+ - `plugin.worker.terminated` `{ pluginId, cause ∈ timeout | oom | crash }`
98
+
99
+ `FORBIDDEN_KEYS` is **extended** with the plugin/claims surface (`claims`, `subject`, `issuer`, `credential`, `authorization`, `signature`, `entry`) as defense-in-depth so a future plugin event can never leak a raw claim/token/signer secret into the chained log (the events above already carry only ids/hashes). Tests assert a refused load and a worker timeout each emit exactly one chained event, and that a synthetic plugin event with raw claims is stripped by `sanitizeAudit`.
100
+
101
+ ### 2.5 Real-environment validation exit criterion
102
+
103
+ - **Met:** the 2026-06-11 live validation against real self-hosted vLLM + Ollama ([[2026-06-11-real-environment-validation]]) + `haechi-dashboard` observability.
104
+ - **Residuals (documented, not gating 1.0):** (1) **live KMS-backend validation** (real AWS/GCP/Azure/Vault) is out-of-CI; (2) **the worker plugin sandbox itself is unproven against a real hostile plugin** — its security rests on the trust gate + the §6 residuals, validated by the fail-closed/data-minimization tests, not by an adversarial third-party-plugin red-team (a 1.x exercise, ideally alongside the child-process+permission enforcement).
105
+
106
+ ## 3. Explicit non-scope (deferred to 1.x)
107
+
108
+ - **Capability *enforcement*** against a malicious signed plugin (block fs/net, contain the credential) — needs child-process isolation under the Node permission model.
109
+ - **Classifier/filter and crypto plugin loading** — `authProvider` only in 1.0.
110
+ - **A live revocation feed / CRL**, a plugin **registry / marketplace**, multi-origin, hot-reload, and an **unsigned dev loader** (which would undermine the trust gate — development uses injection).
111
+ - **Python SDK.**
112
+
113
+ ## 4. Backward compatibility & the 1.0 stability contract
114
+
115
+ Existing behavior is **unchanged** — every provider contract, the config and (now nested-enumerated) audit schemas, and the zero-dependency posture are exactly as in 0.9; they are **declared frozen**. The plugin sandbox is **purely additive and opt-in** (`auth.provider: "plugin"`; default stays `none`/`bearer`/`external`). The one behavioral core change is the **additive `FORBIDDEN_KEYS` extension** (§2.4) and the **`schemaVersion`** field (additive). The **satellite peer-range widening (§2.1a) is a prerequisite** so the four satellites keep installing against core 1.0.0.
116
+
117
+ ## 5. 1.0 relationship / what 1.0 closes
118
+
119
+ 1.0 closes the two long-standing 1.0 gates — **API-stability freeze** (§2.1) and the **plugin sandbox + dynamic-loading story** (§2.2–2.4: asymmetric-signed + isolated + audited + auth-only) — and records the **real-environment-validation** exit criterion as met with documented residuals (§2.5). It graduates Haechi from developer preview to a stable self-hosted security gateway while keeping the core promise: a small zero-dependency core, fail-closed everywhere, and "the same security tests pass when you swap a component."
120
+
121
+ ## 6. Threat-model & risk-register deltas (concrete)
122
+
123
+ | New surface (1.0) | Control | Residual |
124
+ |---|---|---|
125
+ | **Malicious/compromised signed plugin** loaded dynamically | Ed25519 signature over `entrySha256`+kind+capabilities, trust-anchor-only key resolution, pin + version-floor + revocation denylist, conformance gate, worker memory/crash isolation, full lifecycle audit | **A signed plugin's own fs/net/`process.env` is NOT blocked, and it CAN exfiltrate the credential it receives** — gated only by the signing/vetting trust model; true enforcement is the 1.x child-process+permission path |
126
+ | **PII/secret leak to a plugin** | Only the credential slice crosses (never the body/keys); JSON-string wire; null-proto sanitizer; host builds the keyed-HMAC identity | the credential the auth plugin legitimately validates is visible to it (see row above) |
127
+ | **Cross-boundary object/proto smuggling** | JSON-string wire (no structured clone / SAB / transferables) + null-proto allowlist sanitizer before `buildExternalIdentity` | none material |
128
+ | **Swap / TOCTOU on the entry** | Sign `entrySha256`; read-into-memory + hash + verify + spawn from in-memory source; no path re-resolution; reject symlinks | none material |
129
+ | **Signer-key confusion / downgrade / rollback / malicious update** | Trust-anchor-only resolution, pinned algorithm, pin/version-floor, revocation | operator must curate anchors/pins |
130
+ | **Plugin DoS** | Per-call `timeoutMs` terminate, heap `resourceLimits`, `maxPendingCalls`, `maxMessageBytes`, single-occupancy worker | a signed plugin can burn its allotted CPU within the timeout (CPU/fd not bounded in 1.0) |
131
+ | **Unaudited code-load** | `plugin.load.*` / `authenticate.deny` / `worker.terminated` audit events; extended `FORBIDDEN_KEYS` | — |
132
+ | **Conformance test/prod divergence** | Randomized per-load vectors + per-call host re-validation of PII-safety | a malicious plugin can pass conformance then misbehave (covered by signing+vetting, not conformance) |
133
+ | **API/audit-schema drift** | Strict semver + deprecation windows (+ security exception) + additive-only nested-enumerated audit schema + `schemaVersion` | a major bump can break by design (documented migration) |
134
+
135
+ Proposed risk IDs: **P1-SEC-024** (dynamic plugin execution / sandbox trust model — supersedes P1-SEC-004's manifest-only stance, lifted under the new controls), **P1-SEC-025** (plugin signing/trust-anchor/revocation lifecycle), **P2-API-001** (stable-contract freeze + deprecation policy), **P2-OPS-006** (satellite peer-range / major-tracking gate). New §4 exclusions: capability enforcement vs a malicious signed plugin, credential containment, classifier/crypto plugin loading, unsigned dev loader, live CRL.
136
+
137
+ ## 7. Test criteria (mapped to the PR breakdown)
138
+
139
+ ### 7.1 PR0 — satellite peer-range widening + the preflight gate
140
+ - All four satellites' `haechi` peer range widened to `>=0.8.0 <2.0.0` (and auth-oidc's `haechi-auth-jwt` to `<2.0.0`); lockfile regenerated; `release:preflight` fails if `!semver.satisfies(coreVersionToPublish, satelliteRange)` for any satellite. A test simulates core `1.0.0` and asserts every satellite range is satisfied.
141
+
142
+ ### 7.2 PR1 — API stability freeze (docs + contract test)
143
+ - `api-stability.md`(+ko) carries the IN/OUT table, strict-semver + deprecation policy (incl. the `HAECHI_DEPRECATION_*` runtime-warning contract and the security exception), and the satellite major-tracking rule.
144
+ - A **contract/snapshot test** pins the frozen exports per subpath + a **full audit event including a non-null `identity` and one `detections[]` entry** (so the nested sub-schemas are guarded, not just the top level) + the config-schema key set + `schemaVersion`. An additive field passes; a removed/renamed field (top-level OR nested) fails. `verifyAuditChain` verifies a frozen-schema fixture and still verifies it with a synthetic additive field.
145
+
146
+ ### 7.3 PR2 — Ed25519 signed-plugin contract + pinning/revocation + conformance harness
147
+ - `packages/plugin` accepts a `worker-isolated`+`authProvider` manifest with the Ed25519 envelope; **refuses** (distinct fail-closed tests, each emitting `plugin.load.refused{reason}`): missing/invalid signature; signer not in `trustAnchors` (kid-not-allowlisted, resolved **before** verify); **entry bytes mutated after signing, path unchanged**; revoked signer / revoked entryHash; below version-floor; pin mismatch; outside `notBefore/notAfter`; capability not allowlisted; alg ≠ Ed25519.
148
+ - `assertAuthProviderConformance` exists; a reference provider passes; a broken one (throws / returns a raw-subject identity / accepts an expired credential / non-deterministic) **fails** each case (negative tests). Vectors are randomized per run.
149
+ - `FORBIDDEN_KEYS` extension test: a synthetic plugin event with `claims`/`credential`/`signature` is stripped by `sanitizeAudit`; the chain stays valid.
150
+
151
+ ### 7.4 PR3 — the `worker-isolated` authProvider sandbox
152
+ - A reference **signed** auth plugin loads, passes conformance in the worker, authenticates a valid bearer/JWT into a **host-built PII-safe identity**; assertions: the worker received **only** the credential slice (an instrumented echo-plugin proves it never got the body / audit sink / token vault / key), the raw subject never appears in the audit, `plugin.load.accepted` is emitted with the resolved `entrySha256`/`signerKeyId`.
153
+ - **Fail-closed + isolation matrix:** unsigned/wrong-signer/tampered/revoked/pin-mismatch/capability-not-allowlisted → construction throws + `load.refused`; **timeout → `null` + worker terminated + `worker.terminated{timeout}`**; throw → `null`; a claims object with `__proto__`/extra keys → sanitized (no prototype pollution, extras dropped) and PII-safe; two concurrent calls with distinct correlation ids never cross responses; a terminate of one call cannot kill a sibling (single-occupancy); `maxPendingCalls`/`maxMessageBytes` enforced; `plugins.enabled:false` (kill-switch) refuses load.
154
+ - `normalizeConfig` `auth.provider:"plugin"` enumerated fail-closed tests (each bad option throws); end-to-end through `createRuntime` + the proxy auth gate (a request authenticates via the plugin; identity keyed-HMAC; audit carries no raw subject/credential).
155
+
156
+ ### 7.5 All
157
+ - Core stays zero runtime dependency (`node:` only — Ed25519 is `node:crypto`); `check:packaging` + `check:satellite-packaging` green; the frozen-contract snapshot test + the peer-range preflight gate guard future PRs.
158
+
159
+ ## 8. Suggested PR breakdown (stacked)
160
+ 1. **PR0 — satellite peer-range widening + preflight gate** (prerequisite; patch-release the four satellites). → §7.1
161
+ 2. **API freeze** — `api-stability.md`(+ko) IN/OUT table + deprecation/security-exception policy + the nested-schema contract/snapshot test + `schemaVersion`. → §7.2
162
+ 3. **Ed25519 signed-plugin contract + conformance** — the asymmetric primitive (`node:crypto`), the signed envelope (entryHash/kind/capabilities/window), trust-anchor-only resolution, pin/version-floor/revocation, `assertAuthProviderConformance`, the `FORBIDDEN_KEYS` extension. → §7.3
163
+ 4. **Worker-isolated authProvider sandbox** — `createSandboxedAuthProvider` (in-memory verified spawn, JSON-string wire, null-proto sanitizer, correlation-id single-occupancy concurrency, timeout/terminate, kill-switch), the `auth.provider:"plugin"` config branch + lifecycle audit, a reference signed plugin + the §7.4 matrix. → §7.4
164
+ 5. **1.0.0 release cut** — bump core to **1.0.0**; docs EN/KO (this scope doc, threat-model + risk-register deltas with the §6 IDs + target bump, the real-env exit criterion + residuals); wiki ingest (a `[[plugin-sandbox]]` page + `[[packaging-and-distribution]]`/`[[identity-and-auth]]`/`release-roadmap` updates); README "Current Scope". Core reuses the `v*` tag; the first stable `haechi@1.0.0` publishes attested. (PR0 must already be merged + the satellites republished so they install against 1.0.0.)
@@ -2,7 +2,7 @@
2
2
 
3
3
  - 문서 상태: Draft 0.4
4
4
  - 작성일: 2026-06-11
5
- - 기준 버전: 0.9.0
5
+ - 기준 버전: 1.0.0
6
6
  - 기준 브랜치: `main`
7
7
 
8
8
  ## 1. 현재 판단
@@ -25,7 +25,8 @@
25
25
  | G1 | GitHub pre-release | P0 코드 리스크 해결, production-ready 표현 없음 | Pass |
26
26
  | G2 | npm developer preview | P0 해결, preflight/SBOM/provenance 경로 준비, npm auth 확인 | Pass (`haechi@0.3.2` 2026-06-10 배포) |
27
27
  | G3 | npm stable | P1 운영 reference, stream-aware enforcement, API stability 강화 | Blocked |
28
- | G4 | 0.9.0 observability + interactive-auth 위성 컷 | P1-SEC-009 (0.9) / P1-OPS-005 (0.9) mitigated 및 P2-CRYPTO-001 (0.9) accepted; `haechi-dashboard` + `haechi-auth-oidc` + `haechi-crypto-kms@0.2.0` 테스트 통과; 위성 tarball zero-dep; core 0.9.0 bump(추가적 FORBIDDEN_KEYS audit 강화만) | Pass |
28
+ | G4 | 0.9.0 observability + interactive-auth 위성 컷 | P1-SEC-026 / P1-OPS-009 mitigated 및 P2-CRYPTO-001 accepted; `haechi-dashboard` + `haechi-auth-oidc` + `haechi-crypto-kms@0.2.0` 테스트 통과; 위성 tarball zero-dep; core 0.9.0 bump(추가적 FORBIDDEN_KEYS audit 강화만) | Pass |
29
+ | G5 | 1.0.0 stable API contract + signed-plugin sandbox | P1-SEC-024 / P1-SEC-025 mitigated, P2-API-001 / P2-OPS-006 resolved; API freeze + deprecation policy + `tests/api-contract.test.mjs` 통과; Ed25519 signed-plugin contract + `assertAuthProviderConformance` + worker-isolated `authProvider` sandbox 테스트 통과; PR0 위성 peer-range를 `>=0.8.0 <2.0.0`로 확대 및 `check-satellite-peer-ranges.mjs` preflight 게이트 통과; core는 zero runtime dependency 유지; core 1.0.0 bump | Pass |
29
30
 
30
31
  ## 3. P0 배포 차단 리스크 상태
31
32
 
@@ -45,7 +46,7 @@
45
46
  | P1-SEC-001 | KMS/HSM/Vault 미지원 | Resolved for OSS core | `createRuntime(config, { cryptoProvider })` 외부 crypto provider injection, external provider 없으면 fail-closed |
46
47
  | P1-SEC-002 | TokenVault 권한 모델 부족 | Resolved | `revealPolicy: "disabled"` 기본값, `--allow-dev-reveal`, metadata export, retention/purge timestamp |
47
48
  | P1-SEC-003 | audit 무결성 부족 | Resolved | JSONL audit SHA-256 hash chain 및 `verifyAuditChain` |
48
- | P1-SEC-004 | plugin runtime 없음 | Resolved by gating | dynamic runtime 거부, `manifest-only` plugin만 통과 |
49
+ | P1-SEC-004 | plugin runtime 없음 | Resolved by gating (P1-SEC-024이 대체) | dynamic runtime 거부, `manifest-only` plugin만 통과. **1.0에서 P1-SEC-024(§5.4)이 대체:** 1.0은 manifest-only-only 입장을 의도적으로 해제하고, 새 신뢰 통제 하에 signed·capability-gated·worker-isolated·audited `authProvider` plugin에 한해 **좁게** 동적 로딩을 허용 |
49
50
  | P1-SEC-005 | policy conflict 처리 부족 | Resolved | preset block 등 강한 action을 약한 action으로 낮추면 conflict fail-closed |
50
51
  | P1-SEC-006 | regex 중심 필터 정확도 한계 | Resolved for preview | KR RRN checksum, Luhn, unsafe custom regex 제한. ML/classifier plugin은 stable backlog |
51
52
  | P1-SEC-007 | AAD/replay/stream 확장 부족 | Resolved for preview | AAD hash mismatch 명시, streaming 기본 차단. stream sequence/replay cache는 stream support 도입 시 필요 |
@@ -103,9 +104,20 @@ base64/인코딩 값 디코딩 검사, query string 검사, audit tail truncatio
103
104
 
104
105
  | ID | 리스크 | 상태 | 해소 증거 |
105
106
  |---|---|---|---|
106
- | P1-SEC-009 (0.9) | OIDC broker 세션/로그인 보안: `haechi-auth-oidc`의 login CSRF, authorization-code injection, open-redirect, session fixation, mix-up(잘못된 IdP/RP) | Mitigated | `satellites/auth-oidc/index.mjs`: state-first `/auth/callback`(pre-auth 쿠키 바인딩 pending record를 atomic `take()` + egress 이전 constant-time `state` 비교), PKCE S256, callback에서 새 세션 id 발급(fixation 없음), `returnToAllowlist`(open-redirect 없음), issuer/endpoint pinning + RFC 9207 `iss` 검사 + 공유 `createJwtVerifier` 경유 ID-token `aud`/`azp` 프로파일(mix-up), CSRF-gated non-GET logout. `satellites/auth-oidc/auth-oidc.test.mjs`가 각 deny 케이스 검증; scope §6 adversarial review. **잔여:** multi-origin IdP는 범위 외 |
107
- | P1-OPS-005 (0.9) | Dashboard audit 노출: `haechi-dashboard`의 `detections[].path` stored XSS, 미래 필드 audit leak, localhost 뷰어 DNS-rebinding 읽기, remote bind 시 인증 없는 읽기 | Mitigated | `satellites/dashboard/index.mjs` + `assets.mjs`: 엄격 CSP(`require-trusted-types-for 'script'`) + `textContent`-only 렌더링(XSS), `FORBIDDEN_KEYS` 위 재귀적 key-by-key allowlist projection(필드 leak), 요청별 anti-rebinding `Host` allowlist + CORP/COOP same-origin(rebinding), `sessionGuard` **및** TLS 종단을 요구하는 fail-closed remote bind(인증 없는 remote 읽기). `satellites/dashboard/dashboard.test.mjs`; scope §6 adversarial review. **잔여:** remote bind 시 운영자가 TLS 종단을 책임 |
108
- | P2-CRYPTO-001 (0.9) | KMS backend egress: `haechi-crypto-kms@0.2.0` Vault/GCP/Azure backend가 key material이나 provider/key-path 상세를 유출하거나 의도치 않은(metadata) 엔드포인트에 도달 가능 | Accepted | `satellites/crypto-kms/{vault,gcp,azure}.mjs`: optional-peer + injected-client 모델과 faithful-mock `assertCryptoProviderConformance`(cross-key·corrupted-blob 거부, HMAC determinism/domain-separation), Vault `fetch`의 satellite-local `isBlockedAddress` SSRF 가드(dev-only `satellites/crypto-kms/ssrf-parity.test.mjs`로 auth-jwt와 parity 유지), generic fail-closed provider-error 매핑(audit에 provider/key-ARN 없음). `{vault,gcp,azure}.test.mjs` + `crypto-kms.test.mjs`; scope §6 adversarial review. **수용된 잔여:** 실제 Vault/GCP/Azure live-backend 검증은 CI 외부; 발행 tarball은 zero runtime dependency 유지 |
107
+ | P1-SEC-026 | OIDC broker 세션/로그인 보안: `haechi-auth-oidc`의 login CSRF, authorization-code injection, open-redirect, session fixation, mix-up(잘못된 IdP/RP) | Mitigated | `satellites/auth-oidc/index.mjs`: state-first `/auth/callback`(pre-auth 쿠키 바인딩 pending record를 atomic `take()` + egress 이전 constant-time `state` 비교), PKCE S256, callback에서 새 세션 id 발급(fixation 없음), `returnToAllowlist`(open-redirect 없음), issuer/endpoint pinning + RFC 9207 `iss` 검사 + 공유 `createJwtVerifier` 경유 ID-token `aud`/`azp` 프로파일(mix-up), CSRF-gated non-GET logout. `satellites/auth-oidc/auth-oidc.test.mjs`가 각 deny 케이스 검증; scope §6 adversarial review. **잔여:** multi-origin IdP는 범위 외 |
108
+ | P1-OPS-009 | Dashboard audit 노출: `haechi-dashboard`의 `detections[].path` stored XSS, 미래 필드 audit leak, localhost 뷰어 DNS-rebinding 읽기, remote bind 시 인증 없는 읽기 | Mitigated | `satellites/dashboard/index.mjs` + `assets.mjs`: 엄격 CSP(`require-trusted-types-for 'script'`) + `textContent`-only 렌더링(XSS), `FORBIDDEN_KEYS` 위 재귀적 key-by-key allowlist projection(필드 leak), 요청별 anti-rebinding `Host` allowlist + CORP/COOP same-origin(rebinding), `sessionGuard` **및** TLS 종단을 요구하는 fail-closed remote bind(인증 없는 remote 읽기). `satellites/dashboard/dashboard.test.mjs`; scope §6 adversarial review. **잔여:** remote bind 시 운영자가 TLS 종단을 책임 |
109
+ | P2-CRYPTO-001 | KMS backend egress: `haechi-crypto-kms@0.2.0` Vault/GCP/Azure backend가 key material이나 provider/key-path 상세를 유출하거나 의도치 않은(metadata) 엔드포인트에 도달 가능 | Accepted | `satellites/crypto-kms/{vault,gcp,azure}.mjs`: optional-peer + injected-client 모델과 faithful-mock `assertCryptoProviderConformance`(cross-key·corrupted-blob 거부, HMAC determinism/domain-separation), Vault `fetch`의 satellite-local `isBlockedAddress` SSRF 가드(dev-only `satellites/crypto-kms/ssrf-parity.test.mjs`로 auth-jwt와 parity 유지), generic fail-closed provider-error 매핑(audit에 provider/key-ARN 없음). `{vault,gcp,azure}.test.mjs` + `crypto-kms.test.mjs`; scope §6 adversarial review. **수용된 잔여:** 실제 Vault/GCP/Azure live-backend 검증은 CI 외부; 발행 tarball은 zero runtime dependency 유지 |
110
+
111
+ ## 5.4 1.0.0 Stable API Contract + Signed-Plugin Sandbox 리스크 상태
112
+
113
+ 이 ID들은 1.0.0 stable 컷(API freeze + Ed25519 signed, worker-isolated `authProvider` plugin sandbox)에 한정된다. 권위 있는 threat 행과 범위는 `docs/current/release-1.0-implementation-scope.md` §6이며, 증거는 PR(#46–#49), core 소스, 그리고 테스트 스위트다.
114
+
115
+ | ID | 리스크 | 상태 | 해소 증거 |
116
+ |---|---|---|---|
117
+ | P1-SEC-024 | 동적 plugin 실행 / sandbox 신뢰 모델: worker sandbox에 로딩된 signed `authProvider` plugin이 host(`fs`/`net`/`process.env`)를 악용하거나 받은 credential을 exfiltrate할 수 있음. **P1-SEC-004의 manifest-only 입장을 대체** — 1.0이 의도적으로 해제하고 새 통제 하에 좁게 동적 로딩 허용 | Mitigated | `packages/plugin/sandbox.mjs` `createSandboxedAuthProvider`(PR #49): `node:worker_threads` memory/crash 격리, 메모리 내 검증된 spawn(경로 재해석/TOCTOU 없음), data-minimized JSON-string wire(credential slice만 전달; host가 keyed-HMAC identity 구성), null-proto claims sanitizer, single-occupancy + correlation-id 동시성, 필수 `timeoutMs` terminate + `resourceLimits`/`maxPendingCalls`/`maxMessageBytes`, kill-switch(`plugins.enabled:false`), 매 respawn마다 전체 게이트 재실행. lifecycle audit(`plugin.load.*`/`authenticate.deny`/`worker.terminated`) + 확장된 `FORBIDDEN_KEYS`; audit identity는 frozen 5 키 `{id,type,subjectHash,issuerHash,provider}`로 projection. 테스트: §7.4 fail-closed + 격리 매트릭스, `auth.provider:"plugin"` `normalizeConfig` fail-closed 테스트, `createRuntime` + proxy auth end-to-end. **잔여:** `node:worker_threads`는 memory/crash 격리 + data-minimization이지 capability sandbox가 아님 — 악의적 signed plugin의 `fs`/`net`/`process.env`는 차단되지 않고 받은 credential을 exfiltrate할 수 있음; 오직 signing/vetting 신뢰 모델로만 통제. 진짜 집행(child-process + Node permission model)은 1.x; worker sandbox는 실제 hostile plugin에 대해 미검증(1.x red-team) |
118
+ | P1-SEC-025 | plugin signing / trust-anchor / revocation lifecycle: signer-key confusion/downgrade/rollback, swap(TOCTOU)된 entry, 또는 revoked/expired signer의 코드 로딩 | Mitigated | `packages/plugin/signing.mjs` `verifySignedPlugin`(PR #48): `canonicalize({pluginId, kind, version, capabilities, coreVersionRange, entrySha256, notBefore, notAfter})`에 대한 Ed25519(asymmetric, `node:crypto`) 서명 — `entrySha256` 바인딩(anti-swap), **trust-anchor-only** 키 해석(`signerKeyId` ∉ allowlist면 verify 이전 거부; 알고리즘 Ed25519 고정; signer 집합은 AES rotation 키 파일과 분리), pin + `pluginId`별 version-floor(anti-rollback/malicious-update) + `revokedSignerKeyIds`/`revokedEntrySha256` denylist + `notBefore`/`notAfter` window, 모두 load 시 fail-closed이며 매 respawn마다 재검증. `assertAuthProviderConformance`(`haechi/auth`, `assertCryptoProviderConformance`의 auth 대응)는 load별 randomized vectors를 쓰는 정합성 게이트; host가 call별 PII-safety 재검증. 테스트: §7.3 reason별 거부 매트릭스(각각 `plugin.load.refused{reason}` 방출), conformance negative 테스트, `FORBIDDEN_KEYS` 확장 `sanitizeAudit` 테스트. **잔여:** 운영자가 trust anchor/pin을 curate해야 함; live revocation feed / CRL은 1.x(revocation은 다음 load에 적용; kill-switch가 live plugin을 force-drop) |
119
+ | P2-API-001 (1.0) | stable-contract freeze + deprecation policy: 불안정 public API / audit-schema drift가 major bump이나 마이그레이션 경로 없이 consumer를 깨뜨림 | Resolved | `docs/current/api-stability.md`(+ko)(PR #47): IN/OUT surface 표, 1.0부터 strict semver, deprecation policy(≥1-minor 유지 + `HAECHI_DEPRECATION_*` runtime-warning 계약 + disclosed-vulnerability in-minor security exception), nested sub-schema 포함 frozen audit event schema + additive `schemaVersion`, config-schema freeze unit(key presence/shape 동결; 더 안전한 default는 허용). `tests/api-contract.test.mjs`가 freeze 가드: subpath별 exports + 전체 audit event(non-null `identity` + `detections[]` 1건) + config key set + `schemaVersion`을 pin; additive 필드는 통과, 제거/개명(top-level OR nested)은 실패, `verifyAuditChain`은 synthetic additive 필드가 있어도 frozen-schema fixture를 검증. **잔여:** major bump은 설계상 깨질 수 있음(문서화된 마이그레이션); disclosed-vulnerability security exception은 advisory + 마이그레이션 경로와 함께 sanctioned in-minor break 허용 |
120
+ | P2-OPS-006 (1.0) | satellite peer-range / major-tracking 게이트: core를 1.0.0으로 bump하면 모든 위성의 `>=0.8.0 <1.0.0` peer가 unsatisfiable(ERESOLVE)되어 위성 설치가 깨짐 | Resolved | PR0(#46)이 네 위성의 `haechi` peer range를 `>=0.8.0 <2.0.0`로 확대(버전 auth-jwt 0.2.1, crypto-kms 0.2.1, dashboard 0.1.2, auth-oidc 0.1.2; auth-oidc의 `haechi-auth-jwt`도 `<2.0.0`)하고 lockfile 재생성(workspace-lockfile 갭). `scripts/check-satellite-peer-ranges.mjs`는 모든 위성에 대해 `semver.satisfies(coreVersionToPublish, range)`를 단언하는 `release:preflight` 게이트로 core `1.0.0`을 시뮬레이션. `api-stability.md §5`에 위성 peer 상한이 core MAJOR를 추종함을 문서화. **잔여:** core 1.0.0 출시 전에 위성을 재발행해야 1.0.0에 대해 설치됨 |
109
121
 
110
122
  ## 6. P2 제품/문서 리스크 상태
111
123
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  - Status: Draft 0.4
4
4
  - Date: 2026-06-11
5
- - Target version: 0.9.0
5
+ - Target version: 1.0.0
6
6
  - Branch: `main`
7
7
 
8
8
  ## 1. Current Assessment
@@ -25,7 +25,8 @@
25
25
  | G1 | GitHub pre-release | P0 code risks resolved, no production-ready language | Pass |
26
26
  | G2 | npm developer preview | P0 resolved, preflight/SBOM/provenance paths ready, npm auth confirmed | Pass (`haechi@0.3.2` published 2026-06-10) |
27
27
  | G3 | npm stable | P1 production reference, stream-aware enforcement, API stability hardened | Blocked |
28
- | G4 | 0.9.0 observability + interactive-auth satellite cut | P1-SEC-009 (0.9) / P1-OPS-005 (0.9) mitigated and P2-CRYPTO-001 (0.9) accepted; `haechi-dashboard` + `haechi-auth-oidc` + `haechi-crypto-kms@0.2.0` tests green; satellite tarballs zero-dep; core bumped to 0.9.0 (only an additive FORBIDDEN_KEYS audit hardening) | Pass |
28
+ | G4 | 0.9.0 observability + interactive-auth satellite cut | P1-SEC-026 / P1-OPS-009 mitigated and P2-CRYPTO-001 accepted; `haechi-dashboard` + `haechi-auth-oidc` + `haechi-crypto-kms@0.2.0` tests green; satellite tarballs zero-dep; core bumped to 0.9.0 (only an additive FORBIDDEN_KEYS audit hardening) | Pass |
29
+ | G5 | 1.0.0 stable API contract + signed-plugin sandbox | P1-SEC-024 / P1-SEC-025 mitigated, P2-API-001 / P2-OPS-006 resolved; the API freeze + deprecation policy + `tests/api-contract.test.mjs` green; the Ed25519 signed-plugin contract + `assertAuthProviderConformance` + the worker-isolated `authProvider` sandbox tests green; PR0 satellite peer-ranges widened to `>=0.8.0 <2.0.0` and the `check-satellite-peer-ranges.mjs` preflight gate green; core stays zero runtime dependency; core bumped to 1.0.0 | Pass |
29
30
 
30
31
  ## 3. P0 Distribution-Blocking Risk Status
31
32
 
@@ -45,7 +46,7 @@
45
46
  | P1-SEC-001 | KMS/HSM/Vault not supported | Resolved for OSS core | `createRuntime(config, { cryptoProvider })` external crypto provider injection; fails closed if no external provider is supplied |
46
47
  | P1-SEC-002 | TokenVault permission model insufficient | Resolved | `revealPolicy: "disabled"` is the default; `--allow-dev-reveal`, metadata export, retention/purge timestamps added |
47
48
  | P1-SEC-003 | Audit integrity insufficient | Resolved | JSONL audit SHA-256 hash chain and `verifyAuditChain` |
48
- | P1-SEC-004 | No plugin runtime | Resolved by gating | Dynamic runtime is rejected; only `manifest-only` plugins pass |
49
+ | P1-SEC-004 | No plugin runtime | Resolved by gating (superseded by P1-SEC-024) | Dynamic runtime is rejected; only `manifest-only` plugins pass. **Superseded in 1.0 by P1-SEC-024 (§5.4):** 1.0 deliberately lifts the manifest-only-only stance, enabling dynamic loading **narrowly** for a signed, capability-gated, worker-isolated, audited `authProvider` plugin under the new trust controls |
49
50
  | P1-SEC-005 | Policy conflict handling insufficient | Resolved | Downgrading a stronger action (e.g., preset block) to a weaker one fails closed on conflict |
50
51
  | P1-SEC-006 | Regex-based filter accuracy limited | Resolved for preview | KR RRN checksum, Luhn, and unsafe custom regex restrictions added. ML/classifier plugin is in the stable backlog |
51
52
  | P1-SEC-007 | AAD/replay/stream extension insufficient | Resolved for preview | AAD hash mismatch is explicit; streaming is blocked by default. Stream sequence/replay cache required when stream support is introduced |
@@ -103,9 +104,20 @@ These IDs are scoped to the 0.9.0 satellite cut (`haechi-dashboard`, `haechi-aut
103
104
 
104
105
  | ID | Risk | Status | Resolution evidence |
105
106
  |---|---|---|---|
106
- | P1-SEC-009 (0.9) | OIDC broker session/login security: login CSRF, authorization-code injection, open-redirect, session fixation, and mix-up (wrong IdP/RP) in `haechi-auth-oidc` | Mitigated | `satellites/auth-oidc/index.mjs`: state-first `/auth/callback` (atomic `take()` of a pre-auth-cookie-bound pending record + constant-time `state` compare before any egress), PKCE S256, fresh session id minted at callback (no fixation), `returnToAllowlist` (no open-redirect), issuer/endpoint pinning + RFC 9207 `iss` check + ID-token `aud`/`azp` profile via the shared `createJwtVerifier` (mix-up), CSRF-gated non-GET logout. `satellites/auth-oidc/auth-oidc.test.mjs` exercises each deny case; adversarial review in scope §6. **Residual:** multi-origin IdP out of scope |
107
- | P1-OPS-005 (0.9) | Dashboard audit exposure: stored XSS via `detections[].path`, future-field audit leak, DNS-rebinding read of a localhost viewer, and unauthenticated read on remote bind in `haechi-dashboard` | Mitigated | `satellites/dashboard/index.mjs` + `assets.mjs`: strict CSP (`require-trusted-types-for 'script'`) + `textContent`-only rendering (XSS), recursive key-by-key allowlist projection over `FORBIDDEN_KEYS` (field leak), per-request anti-rebinding `Host` allowlist + CORP/COOP same-origin (rebinding), fail-closed remote bind requiring `sessionGuard` **and** TLS termination (unauthenticated remote read). `satellites/dashboard/dashboard.test.mjs`; adversarial review in scope §6. **Residual:** operator must terminate TLS for remote bind |
108
- | P2-CRYPTO-001 (0.9) | KMS backend egress: the `haechi-crypto-kms@0.2.0` Vault/GCP/Azure backends could leak key material or provider/key-path detail or reach an unintended (metadata) endpoint | Accepted | `satellites/crypto-kms/{vault,gcp,azure}.mjs`: optional-peer + injected-client model with faithful-mock `assertCryptoProviderConformance` (cross-key + corrupted-blob rejection, HMAC determinism/domain-separation), satellite-local `isBlockedAddress` SSRF guard on the Vault `fetch` (kept honest by the dev-only `satellites/crypto-kms/ssrf-parity.test.mjs` vs auth-jwt), generic fail-closed provider-error mapping (no provider/key-ARN in audit). `{vault,gcp,azure}.test.mjs` + `crypto-kms.test.mjs`; adversarial review in scope §6. **Residual accepted:** live-backend (real Vault/GCP/Azure) validation is out of CI; the published tarball stays zero runtime dependency |
107
+ | P1-SEC-026 | OIDC broker session/login security: login CSRF, authorization-code injection, open-redirect, session fixation, and mix-up (wrong IdP/RP) in `haechi-auth-oidc` | Mitigated | `satellites/auth-oidc/index.mjs`: state-first `/auth/callback` (atomic `take()` of a pre-auth-cookie-bound pending record + constant-time `state` compare before any egress), PKCE S256, fresh session id minted at callback (no fixation), `returnToAllowlist` (no open-redirect), issuer/endpoint pinning + RFC 9207 `iss` check + ID-token `aud`/`azp` profile via the shared `createJwtVerifier` (mix-up), CSRF-gated non-GET logout. `satellites/auth-oidc/auth-oidc.test.mjs` exercises each deny case; adversarial review in scope §6. **Residual:** multi-origin IdP out of scope |
108
+ | P1-OPS-009 | Dashboard audit exposure: stored XSS via `detections[].path`, future-field audit leak, DNS-rebinding read of a localhost viewer, and unauthenticated read on remote bind in `haechi-dashboard` | Mitigated | `satellites/dashboard/index.mjs` + `assets.mjs`: strict CSP (`require-trusted-types-for 'script'`) + `textContent`-only rendering (XSS), recursive key-by-key allowlist projection over `FORBIDDEN_KEYS` (field leak), per-request anti-rebinding `Host` allowlist + CORP/COOP same-origin (rebinding), fail-closed remote bind requiring `sessionGuard` **and** TLS termination (unauthenticated remote read). `satellites/dashboard/dashboard.test.mjs`; adversarial review in scope §6. **Residual:** operator must terminate TLS for remote bind |
109
+ | P2-CRYPTO-001 | KMS backend egress: the `haechi-crypto-kms@0.2.0` Vault/GCP/Azure backends could leak key material or provider/key-path detail or reach an unintended (metadata) endpoint | Accepted | `satellites/crypto-kms/{vault,gcp,azure}.mjs`: optional-peer + injected-client model with faithful-mock `assertCryptoProviderConformance` (cross-key + corrupted-blob rejection, HMAC determinism/domain-separation), satellite-local `isBlockedAddress` SSRF guard on the Vault `fetch` (kept honest by the dev-only `satellites/crypto-kms/ssrf-parity.test.mjs` vs auth-jwt), generic fail-closed provider-error mapping (no provider/key-ARN in audit). `{vault,gcp,azure}.test.mjs` + `crypto-kms.test.mjs`; adversarial review in scope §6. **Residual accepted:** live-backend (real Vault/GCP/Azure) validation is out of CI; the published tarball stays zero runtime dependency |
110
+
111
+ ## 5.4 1.0.0 Stable API Contract + Signed-Plugin Sandbox Risk Status
112
+
113
+ These IDs are scoped to the 1.0.0 stable cut (the API freeze + the Ed25519 signed, worker-isolated `authProvider` plugin sandbox). The authoritative threat rows and scope are `docs/current/release-1.0-implementation-scope.md` §6; evidence is the PRs (#46–#49), the core source, and the test suites.
114
+
115
+ | ID | Risk | Status | Resolution evidence |
116
+ |---|---|---|---|
117
+ | P1-SEC-024 | Dynamic plugin execution / sandbox trust model: a signed `authProvider` plugin loaded into the worker sandbox could abuse the host (`fs`/`net`/`process.env`) or exfiltrate the credential it receives. **Supersedes P1-SEC-004's manifest-only stance** — 1.0 deliberately lifts it, enabling dynamic loading narrowly under new controls | Mitigated | `packages/plugin/sandbox.mjs` `createSandboxedAuthProvider` (PR #49): `node:worker_threads` memory/crash isolation, in-memory verified spawn (no path re-resolution / TOCTOU), data-minimized JSON-string wire (only the credential slice crosses; the host builds the keyed-HMAC identity), null-proto claims sanitizer, single-occupancy + correlation-id concurrency, required `timeoutMs` terminate + `resourceLimits`/`maxPendingCalls`/`maxMessageBytes`, kill-switch (`plugins.enabled:false`), and the full gate re-run on every respawn. Lifecycle audit (`plugin.load.*`/`authenticate.deny`/`worker.terminated`) + extended `FORBIDDEN_KEYS`; the audit identity is projected to the frozen 5 keys `{id,type,subjectHash,issuerHash,provider}`. Tests: the §7.4 fail-closed + isolation matrix, the `auth.provider:"plugin"` `normalizeConfig` fail-closed tests, and the `createRuntime` + proxy auth end-to-end. **Residual:** `node:worker_threads` is memory/crash isolation + data-minimization, NOT a capability sandbox — a malicious signed plugin's `fs`/`net`/`process.env` is not blocked and it CAN exfiltrate the credential it receives; gated only by the signing/vetting trust model. True enforcement (child-process + Node permission model) is 1.x; the worker sandbox is unproven against a real hostile plugin (a 1.x red-team) |
118
+ | P1-SEC-025 | Plugin signing / trust-anchor / revocation lifecycle: signer-key confusion/downgrade/rollback, a swapped (TOCTOU) entry, or a revoked/expired signer loading code | Mitigated | `packages/plugin/signing.mjs` `verifySignedPlugin` (PR #48): Ed25519 (asymmetric, `node:crypto`) signature over `canonicalize({pluginId, kind, version, capabilities, coreVersionRange, entrySha256, notBefore, notAfter})` — binding `entrySha256` (anti-swap), **trust-anchor-only** key resolution (refuse before verify if `signerKeyId` ∉ allowlist; algorithm pinned to Ed25519; signer set is separate from the AES rotation key file), pin + per-`pluginId` version-floor (anti-rollback/malicious-update) + `revokedSignerKeyIds`/`revokedEntrySha256` denylists + `notBefore`/`notAfter` window, all fail-closed at load and re-verified on every respawn. `assertAuthProviderConformance` (`haechi/auth`, the auth analog of `assertCryptoProviderConformance`) is a correctness gate with per-load randomized vectors; the host re-validates PII-safety per call. Tests: the §7.3 per-reason refusal matrix (each emits `plugin.load.refused{reason}`), the conformance negative tests, and the `FORBIDDEN_KEYS`-extension `sanitizeAudit` test. **Residual:** the operator must curate trust anchors/pins; a live revocation feed / CRL is 1.x (revocation takes effect at next load; the kill-switch force-drops a live plugin) |
119
+ | P2-API-001 (1.0) | Stable-contract freeze + deprecation policy: an unstable public API / audit-schema drift that breaks consumers without a major bump or a migration path | Resolved | `docs/current/api-stability.md`(+ko) (PR #47): the IN/OUT surface table, strict semver from 1.0, the deprecation policy (≥1-minor retention + `HAECHI_DEPRECATION_*` runtime-warning contract + the disclosed-vulnerability in-minor security exception), the frozen audit event schema including nested sub-schemas + an additive `schemaVersion`, and the config-schema freeze unit (key presence/shape frozen; safer defaults still allowed). `tests/api-contract.test.mjs` is the freeze guard: it pins the per-subpath exports + a full audit event (non-null `identity` + a `detections[]` entry) + the config key set + `schemaVersion`; an additive field passes, a removed/renamed field (top-level OR nested) fails, and `verifyAuditChain` still verifies a frozen-schema fixture with a synthetic additive field. **Residual:** a major bump can break by design (documented migration); the disclosed-vulnerability security exception permits a sanctioned in-minor break with an advisory + migration path |
120
+ | P2-OPS-006 (1.0) | Satellite peer-range / major-tracking gate: bumping core to 1.0.0 makes every satellite's `>=0.8.0 <1.0.0` peer unsatisfiable (ERESOLVE), breaking satellite installs | Resolved | PR0 (#46) widened all four satellites' `haechi` peer range to `>=0.8.0 <2.0.0` (versions auth-jwt 0.2.1, crypto-kms 0.2.1, dashboard 0.1.2, auth-oidc 0.1.2; auth-oidc's `haechi-auth-jwt` likewise to `<2.0.0`) and regenerated the lockfile (the workspace-lockfile gotcha). `scripts/check-satellite-peer-ranges.mjs` is a `release:preflight` gate that asserts `semver.satisfies(coreVersionToPublish, range)` for every satellite, simulating core `1.0.0`. `api-stability.md §5` documents that the satellite peer upper bound tracks the core MAJOR. **Residual:** the satellites must be republished before core 1.0.0 ships so they install against it |
109
121
 
110
122
  ## 6. P2 Product/Documentation Risk Status
111
123