haechi 1.1.2 → 1.3.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.
Files changed (39) hide show
  1. package/README.ko.md +46 -11
  2. package/README.md +46 -11
  3. package/SECURITY.md +7 -1
  4. package/docs/README.md +2 -0
  5. package/docs/current/compliance-mapping.ko.md +53 -0
  6. package/docs/current/compliance-mapping.md +53 -0
  7. package/docs/current/config-version.ko.md +30 -0
  8. package/docs/current/config-version.md +51 -0
  9. package/docs/current/configuration.ko.md +165 -9
  10. package/docs/current/configuration.md +165 -9
  11. package/docs/current/operations-runbook.ko.md +155 -0
  12. package/docs/current/operations-runbook.md +241 -0
  13. package/docs/current/release-process.ko.md +5 -1
  14. package/docs/current/release-process.md +5 -1
  15. package/docs/current/risk-register-release-gate.ko.md +5 -3
  16. package/docs/current/risk-register-release-gate.md +13 -3
  17. package/docs/current/security-whitepaper.ko.md +102 -0
  18. package/docs/current/security-whitepaper.md +102 -0
  19. package/docs/current/shared-responsibility.ko.md +2 -2
  20. package/docs/current/shared-responsibility.md +2 -2
  21. package/docs/current/threat-model.ko.md +4 -2
  22. package/docs/current/threat-model.md +4 -2
  23. package/examples/local-proxy-demo/README.md +51 -0
  24. package/examples/local-proxy-demo/demo.mjs +144 -0
  25. package/examples/local-proxy-demo/demo.tape +19 -0
  26. package/examples/local-proxy-demo/live-demo.mjs +121 -0
  27. package/examples/local-proxy-demo/live-demo.tape +25 -0
  28. package/haechi.config.example.json +20 -3
  29. package/package.json +7 -2
  30. package/packages/audit/index.mjs +26 -2
  31. package/packages/cli/bin/haechi.mjs +57 -10
  32. package/packages/cli/runtime.mjs +402 -10
  33. package/packages/core/index.mjs +143 -8
  34. package/packages/filter/index.mjs +975 -12
  35. package/packages/metrics/index.mjs +181 -0
  36. package/packages/privacy-profiles/index.mjs +72 -3
  37. package/packages/protocol-adapters/index.mjs +99 -1
  38. package/packages/proxy/index.mjs +525 -40
  39. package/packages/stream-filter/index.mjs +69 -7
@@ -0,0 +1,102 @@
1
+ # Haechi 보안 백서
2
+
3
+ - 문서 상태: Living document (WS6 — reliability-hardening-track §WS6)
4
+ - 이 문서의 성격: **통제 매핑 + 구조화된 자체 평가**이며, 인증이나 독립 감사가 아닙니다.
5
+ - 출처: `packages/*` 코드, `docs/current/threat-model.md`, `docs/current/risk-register-release-gate.md`. 이 백서는 그 내용을 *매핑*할 뿐 그대로 재서술하지 않으며, 내용을 복제하는 대신 저장소 경로와 위험 ID를 인용합니다.
6
+
7
+ ## 0. 이 문서가 무엇이고 — 무엇이 아닌가
8
+
9
+ Haechi는 **AI 컨텍스트 집행 계층**입니다. OpenAI 호환 / MCP / vLLM / Ollama / llama.cpp JSON payload를 검사하여 PII와 비밀 정보를 탐지하고, 모델·도구·로그에 도달하기 전에 redact / mask / tokenize / encrypt / block 합니다.
10
+
11
+ 이 문서는 Haechi가 **실제로 출시한** 통제를, 저장소 전반에서 이미 인용하는 두 프레임워크 — **OWASP Top 10 for LLM Applications (2025)**와 **NIST AI Risk Management Framework (AI RMF 1.0)** — 에 매핑하고, reliability-hardening track이 잡아내고 수정한 적대적 발견을 **구조화된 자체 모의해킹(self-pentest)**으로 기록합니다.
12
+
13
+ 이 문서는 명시적으로 다음이 **아닙니다**:
14
+ - 컴플라이언스 인증·확약·보증 보고서(reliability-hardening-track §5 비목표 및 `SECURITY.md` Scope 참고)
15
+ - 독립 제3자 침투 시험
16
+ - 모든 OWASP-LLM / NIST-AI-RMF 항목이 *완전히* 완화되었다는 주장. 여러 항목은 **책임 공유**(`docs/current/shared-responsibility.md`)이거나 범위 밖(`docs/current/threat-model.md` §4)입니다. 아래에는 출시 코드에 실제로 존재하는 통제만 매핑하며, 희망 사항은 담지 않습니다.
17
+
18
+ ## 1. 통제 목록 (출시된 표면)
19
+
20
+ 아래 매핑된 모든 통제는 load-bearing하며 테스트로 뒷받침되는 동작입니다. 각 통제의 근거는 이 표가 아니라 코드 경로와 threat-model / risk-register 행입니다.
21
+
22
+ | # | 통제 | 위치 | 근거 |
23
+ |---|---|---|---|
24
+ | C1 | 탐지 + redact/mask/tokenize/encrypt/block 파이프라인 | `packages/core` (`protectJson`), `packages/filter`, `packages/policy` | threat-model §3 |
25
+ | C2 | fail-closed 집행(알 수 없는 policy/config/`target.type`은 throw; non-JSON/무효/압축/초과 응답은 fail-closed) | `packages/cli/runtime.mjs` (`normalizeConfig`), `packages/proxy` (`maybeProtectResponse`) | CLAUDE.md 불변식; P1-SEC-010 |
26
+ | C3 | audit SHA-256 hash chain + head anchoring(변조 + tail-truncation 증거) | `packages/audit` (`verifyAuditChain`, `audit.anchor`) | P1-SEC-003 |
27
+ | C4 | audit에 평문/PII 없음(`FORBIDDEN_KEYS`); keyed-HMAC identity 해시; 구조화 경로 키 해싱 | `packages/audit`, `packages/auth` (`buildIdentity`) | P1-SEC-012 |
28
+ | C5 | streaming 기본 차단; bounded·opt-in 검사(cross-frame sliding buffer) | `packages/proxy`, `packages/stream-filter`, `core` (`createStreamProtector`) | P1-SEC-007 |
29
+ | C6 | body 읽기 전 클라이언트 auth gate; named policy-profile 해석; identity별 rate limit | `packages/proxy` (`authorizeRequest`), `packages/auth` | SECURITY.md; threat-model §3 |
30
+ | C7 | 서명·sandbox된 `authProvider` 플러그인(Ed25519 + trust anchor + pin/floor/revocation + worker/process 격리) | `packages/plugin`, `packages/cli/runtime.mjs` | P1-SEC-024 / P1-SEC-027 |
31
+ | C8 | host-mediated fetch의 SSRF 가드; absolute-form proxy target 거부 | `packages/ssrf`, `packages/proxy` (`assertRelativeProxyTarget`) | P1-SEC-009 / P1-SEC-028 |
32
+ | C9 | token-vault reveal 거버넌스(`revealPolicy`), retention, token id 기준 audit된 reveal/purge | `packages/token-vault` | P1-SEC-002 |
33
+ | C10 | 정책은 강해지기만 함(`ACTION_STRENGTH`); privacy profile은 강화만 가능, 약화 불가 | `packages/policy`, `packages/privacy-profiles` | P1-SEC-005 |
34
+ | C11 | 탐지 정밀도 통제(`filters.minConfidence`, `filters.allowlist`); hard-block 타입은 **억제 불가** | `packages/filter`, `packages/core` | WS2c |
35
+ | C12 | 매칭 전 string leaf의 NFKC 정규화(full-width/혼동 문자 회피) | `packages/core` / `packages/filter` | WS2d |
36
+ | C13 | **proxy TLS / remote-bind 강화(이 WS):** remote bind는 Haechi가 직접 TLS 종단(`proxy.tls`)하거나, 신뢰 hop을 명시적으로 확인(`proxy.trustForwardedProto`, `X-Forwarded-Proto: https` 강제)해야 하며, 그렇지 않으면 기동 시 throw | `packages/proxy` (`assertSafeProxyTransport`, 서버 선택, forwarded-proto gate), `packages/cli/runtime.mjs` (`proxy.tls` 해석) | 본 문서 §3 |
37
+ | C14 | 기본 loopback 바인드; `--allow-remote-bind` 없이는 non-loopback 거부 | `packages/proxy` (`assertSafeProxyBind`) | SECURITY.md |
38
+ | C15 | bounded recursion-depth + byte/encoding 가드(fail-closed 4xx; non-UTF-8 거부) | `packages/core`, `packages/proxy` (`readBody`) | WS5 |
39
+ | C16 | 운영 fail-closed 신호(audit 쓰기 불가 시 `/__haechi/ready` 503; metrics에 PII 없음) | `packages/proxy` (`handleReady`), `packages/metrics` | WS4 |
40
+
41
+ ## 2. OWASP Top 10 for LLM Applications (2025) 매핑
42
+
43
+ 각 행은 OWASP-LLM 위험을 그 위험을 다루는 Haechi 통제에 매핑하고 정직한 잔여 위험을 기록합니다. 주로 운영자/모델의 책임인 위험은 완화로 주장하지 않고 **책임 공유 / 범위 밖**으로 표기합니다.
44
+
45
+ | OWASP-LLM (2025) | Haechi 통제 | 커버리지 & 정직한 잔여 위험 |
46
+ |---|---|---|
47
+ | **LLM01 Prompt Injection** | C5(응답/도구 결과 injection 탐지는 기본 report-only), C1 | **부분 / 설계상.** injection 탐지는 응답/도구 결과 방향에서 동작하며 명시적 격상 전에는 report-only입니다(CLAUDE.md 불변식). Haechi는 prompt injection을 "해결"하지 않습니다 — `threat-model.md` §4는 prompt-injection *예방*을 비목표로 둡니다. 도구 결과가 컨텍스트로 재유입되기 전 비밀/PII를 탐지해 폭발 반경을 줄입니다. |
48
+ | **LLM02 Sensitive Information Disclosure** | C1, C2, C9, C4 | **주 사용 사례.** detect→redact/mask/tokenize/encrypt/block 파이프라인과 응답 보호가 바로 이 통제입니다. 잔여: 탐지는 regex+validator이며 ML이 아닙니다(비목표); 문서화된 제외(base64/URL-query)는 유효합니다(threat-model §4). |
49
+ | **LLM03 Supply Chain** | C7, SBOM + SHA-pinned CI(P1-OPS-002/006) | **플러그인 경로에 대해 대응.** 동적 코드는 서명·trust-anchor·pin/floor/revocable한 `authProvider` 플러그인으로만 허용됩니다. 코어는 zero-runtime-dependency를 유지해 의존성 공격 표면을 줄입니다. |
50
+ | **LLM04 Data & Model Poisoning** | — | **범위 밖.** Haechi는 인라인 컨텍스트 필터이며 학습/데이터 파이프라인 통제가 아닙니다. 완화가 아니라 범위 밖으로 표기합니다. |
51
+ | **LLM05 Improper Output Handling** | C5, C2, C1 | **대응.** 응답/stream 보호가 모델 출력을 호출자/도구 도달 전에 검사하며, non-JSON/무효/압축/초과 응답은 fail-closed입니다. 잔여: block 전에 이미 방출된 streaming 바이트는 회수 불가(문서화). |
52
+ | **LLM06 Excessive Agency** | C6, C7, C9 | **부분.** auth gate + named policy profile + model allowlist가 누가 게이트웨이를 구동하고 어떤 모델/연산이 도달 가능한지 제약하며, token reveal은 거버넌스됩니다. 게이트웨이 너머의 에이전트 수준 인가는 운영자의 몫입니다. |
53
+ | **LLM07 System Prompt Leakage** | C1, C5, C4 | **부분.** prompt/응답에 박힌 비밀/PII는 탐지·보호되며, audit는 원문 prompt를 저장하지 않습니다(C4). 모델이 스스로 system prompt 텍스트를 방출하는 것은 막지 않습니다. |
54
+ | **LLM08 Vector & Embedding Weaknesses** | — | **범위 밖.** RAG/vector-store 구성요소 없음. |
55
+ | **LLM09 Misinformation** | — | **범위 밖.** 사실성 통제가 아닙니다. |
56
+ | **LLM10 Unbounded Consumption** | C6(rate limit), C15(byte/depth 한계), C16(backpressure/timeout) | **게이트웨이에서 대응.** identity별 rate limit, request byte + nesting-depth 상한, max-in-flight backpressure(503 + `Retry-After`), 튜닝된 timeout. 잔여: 내장 rate limiter는 단일 프로세스(다중 replica는 주입된 공유 limiter 필요 — shared-responsibility.md). |
57
+
58
+ ## 3. NIST AI RMF (AI RMF 1.0) 매핑
59
+
60
+ AI RMF 네 기능에 매핑합니다. Haechi는 운영자가 자신의 AI-RMF 프로그램 안에서 사용하는 *기술 통제 표면*이며, 운영자를 위한 거버넌스를 대신 구현하지 않습니다.
61
+
62
+ | AI RMF 기능 | Haechi 통제 | Haechi의 기여(그리고 경계) |
63
+ |---|---|---|
64
+ | **GOVERN** | C2, C10, C14, 문서화된 threat-model + 책임 공유 구분 | fail-closed 기본값, 강화-전용 정책 격자, 기본 loopback, Haechi가 하는 일과 운영자가 소유하는 일의 명시적 문서화. 거버넌스 *정책*은 운영자의 몫입니다. |
65
+ | **MAP** | C1, 탐지 타입 행렬(`configuration.md`), threat-model §1–2 | 민감 데이터가 prompt/응답/도구 호출을 통해 *어디로* 흐르고 *어떤* 범주가 탐지되는지 드러내, 운영자가 게이트웨이 경계에서 AI-시스템 데이터 위험을 매핑하도록 돕습니다. |
66
+ | **MEASURE** | C3, C4, C11, `bench:detection` precision/recall gate, `/__haechi/metrics` | 변조 증거 audit, CI gate로 묶인 precision/recall 측정 하니스, PII 없는 운영 metrics가 측정 가능한 신호를 제공합니다. 잔여: 탐지 metrics는 regex/validator 규칙만 측정합니다. |
67
+ | **MANAGE** | C2, C5, C6, C9, C13, C16, operations-runbook | 인라인 집행, streaming 봉쇄, auth gate, token-vault 거버넌스, TLS 강화 remote bind, readiness/backpressure, Day-2 runbook(rotation/retention)이 운영자가 프로덕션에서 위험을 관리하게 합니다. |
68
+
69
+ ## 4. 구조화된 자체 모의해킹(self-pentest)
70
+
71
+ 이것은 **자체 평가**입니다 — Haechi가 자신의 통제를 스스로 적대적으로 시험한 것이며, 독립 침투 시험이 아닙니다. 아래 각 발견은 테스트로 재현·수정되었고 지금은 회귀로 보호됩니다.
72
+
73
+ ### 4.1 방법론
74
+ - **실환경 검증.** proxy + 보호 파이프라인은 env-gated 통합 스위트(`tests/local-inference.integration.test.mjs`; P1-OPS-003)를 통해 실제 OpenAI 호환(vLLM) 및 Ollama 네이티브 엔드포인트에 대해 동작합니다. 모델 서버가 없으면 CI가 이를 건너뛰므로 mock-only가 아니라 opt-in입니다.
75
+ - **적대적 회귀 코퍼스.** 라벨링된 positive/hard-negative 탐지 코퍼스가 `npm run bench:detection`을 구동하며 CI gate(`scan:detection`)로 묶여, precision/recall 회귀가 빌드를 실패시킵니다(WS2a).
76
+ - **테스트 규율로서의 fail-closed 단언.** 모든 신규 경로(depth guard, readiness, backpressure, env overlay, §3의 TLS remote-bind guard)는 happy path뿐 아니라 *throw/deny* 방향을 단언하는 테스트와 함께 출시됩니다.
77
+
78
+ ### 4.2 잡아내고 수정한 발견 (선별)
79
+
80
+ | 발견 | 분류 | 무엇이 깨졌나 | 수정 + 가드 |
81
+ |---|---|---|---|
82
+ | **WS2d — Unicode 회피** | 탐지 우회 | full-width / 혼동 코드포인트가 비밀/PII 값을 모든 regex 규칙 앞에서 통과시켰습니다(string leaf를 정규화 전에 매칭). | 매칭 전 string leaf를 NFKC 정규화(C12); 탐지 코퍼스의 회피 fixture로 커버. |
83
+ | **WS2c — bearer-recall 회귀** | 탐지 정밀도 | "Bearer …" 산문 false positive를 줄이려 추가한 context anchor가 *실제* bearer-token 비밀까지 억제했습니다(코퍼스가 잡은 recall 회귀). | anchor를 재범위화해 hard-block `secret` 타입이 allowlist/anchor로 절대 억제되지 않도록 했습니다(C11 불변식); 정밀도 통제 테스트가 FP 절감과 recall 하한을 모두 고정. |
84
+ | **WS5 — deep-nesting 스택 오버플로** | 가용성(DoS) | `maxRequestBytes` 안의 깊게 중첩된 JSON이 재귀 스택을 넘쳐 uncaught crash가 났습니다. | 설정 가능한 `limits.maxNestingDepth` fail-closed 4xx(C15); deep-nesting 테스트. |
85
+ | **P1-SEC-009 — proxy SSRF / absolute target** | SSRF | absolute/protocol-relative request target이 upstream을 우회시킬 수 있었습니다. | origin-form 전용 target; upstream URL은 고정 upstream에 path+search만 결합(C8). |
86
+ | **WS6 — 평문 remote bind(이 WS)** | 기밀성 | non-loopback 바인드가 plain HTTP를 제공해 bearer token + payload를 평문 노출했습니다. | 이제 remote bind는 Haechi 종단 TLS 또는 `X-Forwarded-Proto: https`를 강제하는 명시적 `trustForwardedProto` hop을 요구하며, 그렇지 않으면 기동 시 throw(C13). `tests/proxy-tls.test.mjs`로 가드(TLS 없을 때 throw, https-over-TLS smoke, forwarded-proto 거부, fail-closed config). |
87
+
88
+ ### 4.3 수용된 잔여 위험 (정직하게)
89
+ - 서명된 플러그인 자신의 `fs`/`fetch`는 1.0 `worker_threads` 모드에서 차단되지 않습니다(메모리/크래시 격리만); opt-in 1.1 `process-isolated` 런타임에서만 집행됩니다(threat-model §3, P1-SEC-027).
90
+ - 단일 프로세스 rate limiter / audit chain / token vault: 다중 replica 안전성은 주입된 공유 저장소가 필요합니다(shared-responsibility.md; reliability-hardening-track §3).
91
+ - 탐지는 regex + validator로 유지됩니다(ML 없음); 문서화된 base64 / URL-query 제외는 유효합니다(threat-model §4).
92
+
93
+ ## 5. 공개(Disclosure)
94
+
95
+ 의심되는 취약점은 `SECURITY.md`와 `/.well-known/security.txt`에 따라 GitHub **private vulnerability reporting**(Security Advisories) <https://github.com/raeseoklee/haechi/security/advisories> 으로 신고하십시오. 신고에 실제 비밀, 프로덕션 prompt, 고객 데이터, 개인정보를 포함하지 마십시오.
96
+
97
+ ## 6. 상호 참조
98
+ - `docs/current/threat-model.md` — 권위 있는 위협 모델과 제외 항목.
99
+ - `docs/current/risk-register-release-gate.md` — 위험별 해결 상태와 release gate.
100
+ - `docs/current/compliance-mapping.md` — 통제-의무 매핑 + DSAR/retention 워크플로.
101
+ - `docs/current/shared-responsibility.md` — Haechi가 소유하는 것 vs. 운영자.
102
+ - `docs/current/operations-runbook.md` — Day-2 운영, rotation, retention.
@@ -0,0 +1,102 @@
1
+ # Haechi Security Whitepaper
2
+
3
+ - Status: Living document (WS6 — reliability-hardening-track §WS6)
4
+ - Scope of this document: a **control mapping + structured self-assessment**, not a certification or an independent audit.
5
+ - Source of truth: the code under `packages/*`, `docs/current/threat-model.md`, and `docs/current/risk-register-release-gate.md`. This whitepaper *maps* those; it does not restate them wholesale, and it cites repo paths and risk IDs rather than duplicating their content.
6
+
7
+ ## 0. What this is — and is not
8
+
9
+ Haechi is an **AI context enforcement layer**: it inspects and protects OpenAI-compatible / MCP / vLLM / Ollama / llama.cpp JSON payloads (detecting PII and secrets, then redacting / masking / tokenizing / encrypting / blocking them) before they reach models, tools, or logs.
10
+
11
+ This document maps the controls Haechi **actually ships** to two frameworks already cited across the repo — the **OWASP Top 10 for LLM Applications (2025)** and the **NIST AI Risk Management Framework (AI RMF 1.0)** — and records a **structured self-pentest** of the adversarial findings the reliability-hardening track caught and fixed.
12
+
13
+ It is explicitly **NOT**:
14
+ - a compliance certification, attestation, or assurance report (see the reliability-hardening-track §5 non-goal and `SECURITY.md` Scope);
15
+ - an independent third-party penetration test;
16
+ - a claim that every OWASP-LLM / NIST-AI-RMF item is *fully* mitigated. Several are **shared responsibility** (`docs/current/shared-responsibility.md`) or out of scope (`docs/current/threat-model.md` §4). Only controls that exist in the shipped code are mapped below; nothing here is aspirational.
17
+
18
+ ## 1. Control inventory (the shipped surface)
19
+
20
+ Every control mapped below is load-bearing, test-backed behavior. The authority for each is the code path and the threat-model / risk-register row, not this table.
21
+
22
+ | # | Control | Where it lives | Authority |
23
+ |---|---|---|---|
24
+ | C1 | Detection + redaction/mask/tokenize/encrypt/block pipeline | `packages/core` (`protectJson`), `packages/filter`, `packages/policy` | threat-model §3 |
25
+ | C2 | Fail-closed enforcement (unknown policy/config/`target.type` throws; non-JSON/invalid/compressed/oversized responses fail closed) | `packages/cli/runtime.mjs` (`normalizeConfig`), `packages/proxy` (`maybeProtectResponse`) | CLAUDE.md invariants; P1-SEC-010 |
26
+ | C3 | Audit SHA-256 hash chain + head anchoring (tamper + tail-truncation evidence) | `packages/audit` (`verifyAuditChain`, `audit.anchor`) | P1-SEC-003 |
27
+ | C4 | No plaintext/PII in audit (`FORBIDDEN_KEYS`); keyed-HMAC identity hashes; structured-path key hashing | `packages/audit`, `packages/auth` (`buildIdentity`) | P1-SEC-012 |
28
+ | C5 | Streaming blocked by default; bounded, opt-in inspection with a cross-frame sliding buffer | `packages/proxy`, `packages/stream-filter`, `core` (`createStreamProtector`) | P1-SEC-007 |
29
+ | C6 | Client auth gate before body-read; named policy-profile resolution; per-identity rate limit | `packages/proxy` (`authorizeRequest`), `packages/auth` | SECURITY.md; threat-model §3 |
30
+ | C7 | Signed, sandboxed `authProvider` plugin (Ed25519 + trust anchors + pin/floor/revocation + worker/process isolation) | `packages/plugin`, `packages/cli/runtime.mjs` | P1-SEC-024 / P1-SEC-027 |
31
+ | C8 | SSRF guard on host-mediated fetches; absolute-form proxy target rejection | `packages/ssrf`, `packages/proxy` (`assertRelativeProxyTarget`) | P1-SEC-009 / P1-SEC-028 |
32
+ | C9 | Token-vault reveal governance (`revealPolicy`), retention, audited reveal/purge by token id | `packages/token-vault` | P1-SEC-002 |
33
+ | C10 | Policies only get stronger (`ACTION_STRENGTH`); privacy profiles may strengthen, never weaken | `packages/policy`, `packages/privacy-profiles` | P1-SEC-005 |
34
+ | C11 | Detection precision controls (`filters.minConfidence`, `filters.allowlist`) that **cannot** suppress hard-block types | `packages/filter`, `packages/core` | WS2c |
35
+ | C12 | NFKC normalization of string leaves before matching (full-width/confusable evasion) | `packages/core` / `packages/filter` | WS2d |
36
+ | C13 | **Proxy TLS / remote-bind hardening (this WS):** a remote bind requires Haechi to terminate TLS (`proxy.tls`) or an explicit trusted-hop acknowledgement (`proxy.trustForwardedProto`, enforcing `X-Forwarded-Proto: https`); otherwise it throws at startup | `packages/proxy` (`assertSafeProxyTransport`, server selection, forwarded-proto gate), `packages/cli/runtime.mjs` (`proxy.tls` resolution) | this document §3 |
37
+ | C14 | Loopback bind by default; non-loopback refused without `--allow-remote-bind` | `packages/proxy` (`assertSafeProxyBind`) | SECURITY.md |
38
+ | C15 | Bounded recursion-depth + byte/encoding guards (fail-closed 4xx; non-UTF-8 rejected) | `packages/core`, `packages/proxy` (`readBody`) | WS5 |
39
+ | C16 | Operability fail-closed signals (`/__haechi/ready` 503 when audit not writable; metrics carry no PII) | `packages/proxy` (`handleReady`), `packages/metrics` | WS4 |
40
+
41
+ ## 2. OWASP Top 10 for LLM Applications (2025) mapping
42
+
43
+ Each row maps an OWASP-LLM risk to the Haechi control(s) that address it, with the honest residual. Where a risk is primarily the operator's / model's responsibility, it is marked **Shared / Out of scope** rather than claimed as mitigated.
44
+
45
+ | OWASP-LLM (2025) | Haechi control(s) | Coverage & honest residual |
46
+ |---|---|---|
47
+ | **LLM01 Prompt Injection** | C5 (response/tool-result injection detection is report-only by default), C1 | **Partial / by design.** Injection detection runs on the response/tool-result direction and is report-only unless explicitly escalated (CLAUDE.md invariant). Haechi does not "solve" prompt injection — `threat-model.md` §4 lists prompt-injection *prevention* as a non-goal. It reduces blast radius by detecting secrets/PII in tool results before they re-enter context. |
48
+ | **LLM02 Sensitive Information Disclosure** | C1, C2, C9, C4 | **Primary use case.** The detect→redact/mask/tokenize/encrypt/block pipeline plus response protection is exactly this control. Residual: detection is regex+validator, not ML (non-goal); documented exclusions (base64/URL-query) stand (threat-model §4). |
49
+ | **LLM03 Supply Chain** | C7, plus SBOM + SHA-pinned CI (P1-OPS-002/006) | **Addressed for the plugin path.** Dynamic code is admitted only as a signed, trust-anchored, pinned/floored/revocable `authProvider` plugin. Core stays zero-runtime-dependency, shrinking the dependency attack surface. |
50
+ | **LLM04 Data & Model Poisoning** | — | **Out of scope.** Haechi is an inline context filter, not a training/data-pipeline control. Marked out of scope, not mitigated. |
51
+ | **LLM05 Improper Output Handling** | C5, C2, C1 | **Addressed.** Response/stream protection inspects model output before it reaches the caller/tools; fail-closed for non-JSON/invalid/compressed/oversized responses. Residual: streaming bytes already emitted before a block cannot be retracted (documented). |
52
+ | **LLM06 Excessive Agency** | C6, C7, C9 | **Partial.** The auth gate + named policy profiles + model allowlist constrain who can drive the gateway and which models/operations are reachable; token reveal is governed. Agent-level authorization beyond the gateway is the operator's. |
53
+ | **LLM07 System Prompt Leakage** | C1, C5, C4 | **Partial.** Secrets/PII embedded in prompts/responses are detected and protected; audit never stores raw prompt text (C4). Haechi does not prevent a model from emitting its own system prompt text. |
54
+ | **LLM08 Vector & Embedding Weaknesses** | — | **Out of scope.** No RAG/vector-store component. |
55
+ | **LLM09 Misinformation** | — | **Out of scope.** Not a factuality control. |
56
+ | **LLM10 Unbounded Consumption** | C6 (rate limit), C15 (byte/depth limits), C16 (backpressure/timeouts) | **Addressed at the gateway.** Per-identity rate limit, request byte + nesting-depth caps, max-in-flight backpressure (503 + `Retry-After`), tuned timeouts. Residual: the built-in rate limiter is single-process (multi-replica needs an injected shared limiter — shared-responsibility.md). |
57
+
58
+ ## 3. NIST AI RMF (AI RMF 1.0) mapping
59
+
60
+ Mapped to the four AI RMF functions. Haechi is a *technical control surface* an operator uses inside their own AI-RMF program; it does not implement governance for them.
61
+
62
+ | AI RMF function | Haechi control(s) | What Haechi contributes (and the boundary) |
63
+ |---|---|---|
64
+ | **GOVERN** | C2, C10, C14, the documented threat-model + shared-responsibility split | Fail-closed defaults, a stronger-only policy lattice, loopback-by-default, and explicit documentation of what Haechi does vs. what the operator owns. Governance *policy* remains the operator's. |
65
+ | **MAP** | C1, the detection-type matrix (`configuration.md`), threat-model §1–2 | Surfaces *where* sensitive data flows through prompts/responses/tool calls and *which* categories are detected, helping an operator map AI-system data risks at the gateway boundary. |
66
+ | **MEASURE** | C3, C4, C11, the `bench:detection` precision/recall gate, `/__haechi/metrics` | Tamper-evident audit, a precision/recall measurement harness wired as a CI gate, and PII-free operational metrics give an operator measurable signals. Residual: detection metrics measure regex/validator rules only. |
67
+ | **MANAGE** | C2, C5, C6, C9, C13, C16, the operations-runbook | Inline enforcement, streaming containment, the auth gate, token-vault governance, TLS-hardened remote binding, readiness/backpressure, and a Day-2 runbook (rotation/retention) let an operator manage risk in production. |
68
+
69
+ ## 4. Structured self-pentest
70
+
71
+ This is a **self-assessment** — Haechi's own adversarial testing of its own controls — not an independent pentest. Each finding below was reproduced as a test, fixed, and is now regression-guarded.
72
+
73
+ ### 4.1 Methodology
74
+ - **Real-environment validation.** The proxy + protection pipeline is exercised against real OpenAI-compatible (vLLM) and Ollama native endpoints via the env-gated integration suite (`tests/local-inference.integration.test.mjs`; P1-OPS-003). CI skips these when no model server is present, so they are opt-in rather than mocked-only.
75
+ - **Adversarial regression corpus.** A labeled positive/hard-negative detection corpus drives `npm run bench:detection`, wired as a CI gate (`scan:detection`) so a precision/recall regression fails the build (WS2a).
76
+ - **Fail-closed assertion as a test discipline.** Every new path (depth guard, readiness, backpressure, env overlay, and the TLS remote-bind guard in §3) ships a test that asserts the *throw/deny* direction, not only the happy path.
77
+
78
+ ### 4.2 Findings caught and fixed (selected)
79
+
80
+ | Finding | Class | What broke | Fix + guard |
81
+ |---|---|---|---|
82
+ | **WS2d — Unicode evasion** | Detection bypass | Full-width / confusable code points slipped a secret/PII value past every regex rule (string leaves were matched pre-normalization). | NFKC-normalize string leaves before matching (C12); covered by the evasion fixtures in the detection corpus. |
83
+ | **WS2c — bearer-recall regression** | Detection precision | A context anchor added to cut "Bearer …"-in-prose false positives also suppressed *real* bearer-token secrets (a recall regression the corpus caught). | The anchor was re-scoped so the hard-block `secret` type is never suppressed by an allowlist/anchor (C11 invariant); the precision-control tests pin both the FP cut **and** the recall floor. |
84
+ | **WS5 — deep-nesting stack overflow** | Availability (DoS) | A deeply nested JSON within `maxRequestBytes` overflowed the recursion stack → uncaught crash. | Configurable `limits.maxNestingDepth` fail-closed 4xx (C15); a deep-nesting test. |
85
+ | **P1-SEC-009 — proxy SSRF / absolute target** | SSRF | An absolute/protocol-relative request target could redirect the upstream. | Origin-form-only targets; the upstream URL combines only path+search with the fixed upstream (C8). |
86
+ | **WS6 — remote bind in plaintext (this WS)** | Confidentiality | A non-loopback bind served plain HTTP, exposing bearer tokens + payloads in cleartext. | A remote bind now requires Haechi-terminated TLS or an explicit `trustForwardedProto` hop enforcing `X-Forwarded-Proto: https`; otherwise it throws at startup (C13). Guarded by `tests/proxy-tls.test.mjs` (throw-without-TLS, https-over-TLS smoke, forwarded-proto rejection, fail-closed config). |
87
+
88
+ ### 4.3 Accepted residuals (honest)
89
+ - A signed plugin's own `fs`/`fetch` is not blocked in the 1.0 `worker_threads` mode (memory/crash isolation only); enforced only in the opt-in 1.1 `process-isolated` runtime (threat-model §3, P1-SEC-027).
90
+ - Single-process rate limiter / audit chain / token vault: multi-replica safety needs an injected shared store (shared-responsibility.md; reliability-hardening-track §3).
91
+ - Detection stays regex + validators (no ML); documented base64 / URL-query exclusions stand (threat-model §4).
92
+
93
+ ## 5. Disclosure
94
+
95
+ Report suspected vulnerabilities via GitHub **private vulnerability reporting** (Security Advisories) at <https://github.com/raeseoklee/haechi/security/advisories>, per `SECURITY.md` and `/.well-known/security.txt`. Do not include real secrets, production prompts, customer data, or personal information in a report.
96
+
97
+ ## 6. Cross-references
98
+ - `docs/current/threat-model.md` — the authoritative threat model and exclusions.
99
+ - `docs/current/risk-register-release-gate.md` — the per-risk resolution status and release gates.
100
+ - `docs/current/compliance-mapping.md` — the control-to-obligation mapping + DSAR/retention workflow.
101
+ - `docs/current/shared-responsibility.md` — what Haechi owns vs. the operator.
102
+ - `docs/current/operations-runbook.md` — Day-2 operations, rotation, and retention.
@@ -1,6 +1,6 @@
1
1
  # Haechi Shared Responsibility
2
2
 
3
- - 문서 상태: Living document (core 1.1.x 추적)
3
+ - 문서 상태: Living document (core 1.3.x 추적)
4
4
  - 작성일: 2026-06-10
5
5
 
6
6
  ## 1. 책임 매트릭스
@@ -41,7 +41,7 @@
41
41
 
42
42
  Haechi의 상태 보유 통제는 설계상 단일 프로세스입니다. 로드밸런서 뒤에서 복제본을 2개 이상 실행하면, 운영자가 공유 인프라를 제공하지 않는 한 이들이 **무음으로 약화**됩니다.
43
43
 
44
- - **Rate limit**은 프로세스별·인메모리이므로 전체 처리량이 복제본 수만큼 배가됩니다. identity별 한도를 공유 front door에서 강제하거나, 공유 저장소 기반 `rateLimiter`를 주입하세요.
44
+ - **Rate limit**은 프로세스별·인메모리이므로 전체 처리량이 복제본 수만큼 배가됩니다. identity별 한도를 공유 front door에서 강제하거나, `createRuntime(config, { rateLimiter })`를 통해 공유 저장소 기반 `rateLimiter`를 주입하세요(이 시임은 `allow(key, limit)` 계약을 만족하며, `boolean` 또는 `Promise<boolean>`을 반환할 수 있습니다. [`configuration.md` → Rate limiter 주입](./configuration.ko.md#rate-limiter-주입) 참고). [`haechi-ratelimit-redis`](https://github.com/raeseoklee/haechi/tree/main/satellites/ratelimit-redis) satellite가 레퍼런스 공유 저장소(Redis 기반) 구현입니다 — 주입된 클라이언트 위의 fixed-window 카운터입니다. 기본 프로세스별 limiter는 window map도 bounding하므로 identity 기준 무한 메모리 증가가 없습니다.
45
45
  - **Audit hash chain + anchor**는 단일 작성자입니다. 각 복제본에 **고유한** `audit.path`(및 anchor 경로)를 주세요. 하나의 audit 파일을 복제본 간에 공유하면 체인이 분기되어 검증 불가 상태가 됩니다.
46
46
  - **TokenVault와 auth store**는 whole-file 로컬 저장소입니다 — 단일 호스트에서는 올바르지만 공유 다중 작성자 저장소는 아닙니다. 다중 복제 토큰화에는 공유 `tokenVault`를 주입하세요.
47
47
  - 파일 락은 `O_EXCL` + atomic rename에 의존하며 NFS/공유 파일시스템에서는 보장되지 않습니다 — 이 저장소들은 로컬 디스크에 두세요.
@@ -1,6 +1,6 @@
1
1
  # Haechi Shared Responsibility
2
2
 
3
- - Status: Living document (tracks core 1.1.x)
3
+ - Status: Living document (tracks core 1.3.x)
4
4
  - Date: 2026-06-10
5
5
 
6
6
  ## 1. Responsibility Matrix
@@ -41,7 +41,7 @@
41
41
 
42
42
  Haechi's stateful controls are single-process by design. Running 2+ replicas behind a load balancer **silently weakens** them unless the operator supplies shared infrastructure:
43
43
 
44
- - **Rate limit** is per-process and in-memory — total throughput multiplies by the replica count. Enforce a per-identity limit at a shared front door, or inject a shared-store `rateLimiter`.
44
+ - **Rate limit** is per-process and in-memory — total throughput multiplies by the replica count. Enforce a per-identity limit at a shared front door, or inject a shared-store `rateLimiter` via `createRuntime(config, { rateLimiter })` (the seam satisfies the `allow(key, limit)` contract, which may return `boolean` or `Promise<boolean>`; see [`configuration.md` → Rate limiter injection](./configuration.md#rate-limiter-injection)). The [`haechi-ratelimit-redis`](https://github.com/raeseoklee/haechi/tree/main/satellites/ratelimit-redis) satellite is the reference shared-store (Redis-backed) implementation — a fixed-window counter over an injected client. The default per-process limiter also bounds its window map (no unbounded memory growth keyed by identity).
45
45
  - **Audit hash chain + anchor** are single-writer. Give each replica its **own** `audit.path` (and anchor path); never share one audit file across replicas, or the chain forks into an unverifiable state.
46
46
  - **TokenVault and the auth store** are whole-file local stores — correct for one host, but not a shared multi-writer store. For multi-replica tokenization, inject a shared `tokenVault`.
47
47
  - File locking relies on `O_EXCL` + atomic rename, which do not hold on NFS / shared filesystems — keep these stores on local disk.
@@ -1,6 +1,6 @@
1
1
  # Haechi Threat Model
2
2
 
3
- - 문서 상태: Living document(core 1.1.x 추적)
3
+ - 문서 상태: Living document(core 1.3.x 추적)
4
4
  - 작성일: 2026-06-10
5
5
 
6
6
  ## 1. 보호 대상
@@ -46,6 +46,8 @@ Haechi가 보호하려는 주요 자산은 다음과 같습니다.
46
46
  | 행 걸린 upstream | proxy 연결 고갈 | `limits.upstreamTimeoutMs` 기본 120s, 초과 시 504 fail |
47
47
  | signing/encryption 키 혼용 | key separation 위반 | policy bundle 서명 키를 domain-separated 파생 키로 분리 |
48
48
  | JSON number/object key 은닉 | 카드번호 등 비문자열 leaf 미탐지 | number leaf와 object key도 detection/transform 대상 |
49
+ | 모든 정규식 규칙을 우회하는 유니코드 난독화 | card/RRN/phone/email/secret을 시각·의미상 동등한 비ASCII 유니코드 형태(전각 숫자 `4242…`, 전각 `@`, 수학·원문자 영숫자)로 보내 모든 탐지 규칙을 무력화 | **매칭 전 각 string leaf의 NFKC 정규화**(WS2d)입니다. 정규화가 무변환인 경우(leaf의 약 99%) 탐지는 이전과 바이트 단위로 동일합니다. 접힘이 **위치 안정적**인 경우(모든 코드포인트가 같은 UTF-16 길이로 접히고 코드포인트별 접힘이 전체 정규화를 그대로 재구성) 정규화 사본에서 탐지하고 원본의 정확한 구간을 redact/block하며, 기록되는 값은 접힌 형태가 아니라 원본 바이트입니다. 그 외 — 길이가 달라지거나(수학 숫자·합자) 총 길이는 같지만 내부 offset을 이동시키는 수축+확장 보상 — 의 경우 offset을 원본에 매핑할 수 없으므로 탐지가 **fail closed**되어 leaf 전체를 덮는 단일 탐지로 처리합니다(leaf 전체 redact/block — 우회 시도를 과도 redact하는 것이 안전한 실패입니다). `String.prototype.normalize` 빌트인을 사용하므로 새 의존성은 없습니다. **잔여는 이제 opt-in 통제입니다:** base64/percent-encoded 페이로드는 `filters.decodeAndRescan`이 활성화된 경우에만 디코딩 후 재검사합니다(다음 행 및 §4 참조) |
50
+ | 모든 정규식 규칙을 우회하는 base64/percent-encoded 페이로드 | 전송 전 base64·percent로 인코딩된 card/RRN/secret은 모든 규칙을 통과합니다(Haechi는 NFKC 텍스트에서 매칭하지만 디코딩하지 않습니다) | **opt-in `filters.decodeAndRescan`**입니다(기본 OFF → 이전과 바이트 단위로 동일). ON일 때, 일반 NFKC 스캔 이후 base64/base64url로 **보이는** string leaf(고정 알파벳, 유효한 길이, `16…8192` 바이트 범위, 같은 leaf로 round-trip, `node:buffer` `isUtf8`로 **유효한 UTF-8** 디코딩)이거나 `%XX` 이스케이프를 포함하는 leaf(try/catch 안의 `decodeURIComponent`)를 디코딩하여 같은 규칙·validator로 재검사합니다. **offset 처리는 fail closed입니다:** 디코딩된 매칭은 인코딩된 leaf에 유효한 offset이 없으므로, 원본 인코딩 leaf 전체를 덮는 **WHOLE-LEAF** 탐지(`start:0, end:leaf.length`)를 발생시킵니다 — transform이 leaf 전체를 redact/block하며, 디코딩된 offset을 원본으로 되돌려 매핑하지 않습니다. **정밀도 가드:** 디코딩된 매칭은 **validator 기반이거나 하드 블록 타입**일 때만 발생합니다(Luhn 통과 `card`, 체크섬 `kr_rrn`/`us_ssn`, IBAN mod-97, 또는 앵커된 규칙의 `secret`/`api_key`). validator 없는 디코딩된 소프트 타입 매칭(맨 전화번호 형태 등)은 발생하지 **않으므로** 무작위 base64는 오탐하지 않습니다. 새 의존성은 없습니다(`node:buffer` Buffer + `decodeURIComponent` 빌트인). **수용된 잔여:** Haechi가 디코딩하지 않는 인코딩(gzip, hex, 중첩/이중 인코딩, 커스텀 알파벳), 그리고 양성 텍스트 안에 Luhn-유효 16자리 런으로 디코딩되도록 의도적으로 조작된 평문(이에 발생하는 것은 오탐이 아니라 올바른 동작) |
49
51
  | 인증 없는 멀티 클라이언트 접근 | 로컬 프로세스가 upstream / token round-trip 경로를 무단 사용 | 선택적 bearer auth (`auth.provider: bearer`); 없거나 잘못된 경우 → 바디 읽기 전 401; identity별 rate limit 및 model allowlist |
50
52
  | Audit tail truncation | 꼬리 audit 레코드의 무음 삭제 | 추가 전용/별도 미디어의 `audit.anchor` head-hash anchoring으로 마지막 anchor까지의 절단 탐지 (0.7) |
51
53
  | Local dev key in production | 소프트웨어 키의 운영 custody 오용 | `assertCryptoProviderConformance`를 통한 외부 `cryptoProvider` 주입; reference KMS adapter (envelope 암호화) |
@@ -86,7 +88,7 @@ Haechi는 다음을 보장하지 않습니다.
86
88
  - 법적 컴플라이언스 인증
87
89
  - 모델 hallucination, prompt injection 완전 방어
88
90
  - 외부 MCP server의 OAuth/resource binding 검증
89
- - base64/URL-encoded 값, 유니코드 난독화 값의 디코딩 후 검사
91
+ - base64/percent-encoded 값의 **기본** 디코딩 후 검사 — Haechi는 NFKC 정규화 텍스트에서 매칭하며(§3의 유니코드 난독화 참조) opt-in `filters.decodeAndRescan`(기본 OFF)을 활성화하지 않는 한 base64/URL 디코딩 후 재검사는 하지 **않습니다**. OFF이면 전송 전 base64·percent로 인코딩된 값은 검사되지 않습니다. ON이면 §3에 설명된 정밀도 가드(validator 기반 / 하드 블록 매칭만, WHOLE-LEAF fail-closed)와 함께 디코딩-후-재검사 패스가 동작합니다. WS2d는 *상시* 디코딩을 보류했고(오탐이 많고 범위 내에서 precision-neutral하지 않음), opt-in 통제는 트레이드오프를 수용하는 운영자를 위해 그 잔여를 닫습니다. 다른 인코딩(gzip/hex/중첩/커스텀 알파벳)은 여전히 범위 밖입니다.
90
92
  - URL query string 내 민감값 검사 (JSON body만 검사)
91
93
  - 마지막 anchor 이후의 audit tail truncation — `audit.anchor`(0.7)는 anchor가 추가 전용/별도 미디어에 있을 때 마지막 anchor까지의 레코드 삭제를 탐지합니다. 마지막 anchor 이후 기록된 레코드와 동일 파일시스템 anchor는 대상에서 제외됩니다
92
94
  - JSON-RPC batch 메시지 처리 (MCP stdio filter는 batch를 fail-closed로 거부)
@@ -1,6 +1,6 @@
1
1
  # Haechi Threat Model
2
2
 
3
- - Status: Living document (tracks core 1.1.x)
3
+ - Status: Living document (tracks core 1.3.x)
4
4
  - Date: 2026-06-10
5
5
 
6
6
  ## 1. Assets Under Protection
@@ -46,6 +46,8 @@ The primary assets Haechi protects are:
46
46
  | Hung upstream | Proxy connection exhaustion | `limits.upstreamTimeoutMs` default 120 s; 504 fail on timeout |
47
47
  | Signing/encryption key conflation | Key separation violation | Policy bundle signing key isolated as a domain-separated derived key |
48
48
  | JSON number / object key concealment | Undetected non-string leaves such as card numbers | Number leaves and object keys included in detection/transform scope |
49
+ | Unicode-obfuscation evasion of every regex rule | A card/RRN/phone/email/secret sent in a visually/semantically equivalent non-ASCII Unicode form (full-width digits `4242…`, full-width `@`, mathematical/enclosed alphanumerics) defeats every detection rule | **NFKC normalization of each string leaf before matching** (WS2d). When the normalization is a no-op (~99% of leaves) detection is byte-identical to before. When the fold is **position-stable** (every codepoint folds to the same UTF-16 length and the per-codepoint folds reconstruct the whole normalization), detection runs on the normalized copy and the exact original span is redacted/blocked (the recorded value is the original bytes, never the fold). Otherwise — a length change (mathematical digits/ligatures) **or** a compensating contraction+expansion that keeps the total length equal while shifting interior offsets — offsets cannot map back, so detection **fails closed** to a single whole-leaf detection (the entire leaf is redacted/blocked — over-redacting an evasion attempt is the safe failure). Uses the `String.prototype.normalize` builtin (no new dependency). **Residual now an opt-in control:** base64/percent-encoded payloads are decoded-and-rescanned only when `filters.decodeAndRescan` is enabled (see the next row and §4) |
50
+ | Base64/percent-encoded payload evades every regex rule | A card/RRN/secret base64- or percent-encoded before sending passes every rule (Haechi matches the NFKC text but does not decode) | **Opt-in `filters.decodeAndRescan`** (default OFF → byte-identical to before). When ON, after the normal NFKC scan a string leaf that LOOKS base64/base64url (anchored alphabet, valid length, within `16…8192` bytes, round-trips to the same leaf, decodes to **valid UTF-8** via `node:buffer` `isUtf8`) or contains a `%XX` escape (`decodeURIComponent` in try/catch) is decoded and rescanned with the same rules + validators. **Offset handling fails closed:** a decoded hit has no offset in the encoded leaf, so it emits a **WHOLE-LEAF** detection of the original encoded leaf (`start:0, end:leaf.length`) — the transform redacts/blocks the entire leaf; a decoded offset is never mapped back. **Precision guard:** a decoded hit only fires when it is **validator-backed or a hard-block type** (a Luhn-passing `card`, a checksum `kr_rrn`/`us_ssn`, an IBAN mod-97, or a `secret`/`api_key` on its anchored rule). A decoded soft-type-without-validator match (a bare phone-shaped run) does **not** fire, so random base64 does not false-positive. Zero new dependency (`node:buffer` Buffer + the `decodeURIComponent` builtin). **Accepted residual:** an encoding Haechi does not decode (gzip, hex, nested/double-encoding, a custom alphabet), and a deliberately contrived plaintext that decodes to a Luhn-valid 16-digit run inside benign text (firing on it is correct, not a false positive) |
49
51
  | Unauthenticated multi-client access | Any local process uses the upstream / token round-trip | Optional bearer auth (`auth.provider: bearer`); missing/invalid → 401 before body read; per-identity rate limit and model allowlist |
50
52
  | Audit tail truncation | Silent deletion of trailing audit records | `audit.anchor` head-hash anchoring on append-only/separate media detects truncation back to the last anchor (0.7) |
51
53
  | Local dev key in production | Software key misused as production custody | External `cryptoProvider` injection with `assertCryptoProviderConformance`; reference KMS adapter (envelope encryption) |
@@ -86,7 +88,7 @@ Haechi does not guarantee:
86
88
  - Legal compliance certification
87
89
  - Complete defense against model hallucination or prompt injection
88
90
  - OAuth/resource binding validation for external MCP servers
89
- - Inspection of base64/URL-encoded values or unicode-obfuscated values after decoding
91
+ - Inspection of base64/percent-encoded values **after decoding** **by default** — Haechi matches on the NFKC-normalized text (see the Unicode-evasion row in §3) and does **not** base64/URL-decode-and-rescan unless the opt-in `filters.decodeAndRescan` is enabled (default OFF). With it OFF, a value that is base64- or percent-encoded before sending is not inspected. With it ON, the decode-and-rescan pass runs with the precision guard described in §3 (validator-backed / hard-block hits only, whole-leaf fail-closed). WS2d deferred an *always-on* decode (false-positive-prone, not precision-neutral within scope); the opt-in control closes that residual for operators who accept the trade-off, and other encodings (gzip/hex/nested/custom-alphabet) remain out of scope.
90
92
  - Detection of sensitive values in URL query strings (JSON body only)
91
93
  - Audit tail truncation beyond the last anchor — `audit.anchor` (0.7) detects deletion of records back to the last anchor when the anchor is on append-only/separate media; records written after the last anchor, and same-filesystem anchors, are not covered
92
94
  - JSON-RPC batch message processing (the MCP stdio filter rejects batches fail-closed)
@@ -0,0 +1,51 @@
1
+ # Local end-to-end demo
2
+
3
+ A self-contained, **reproducible** walkthrough of Haechi — no remote model required.
4
+ It stands up a tiny OpenAI-compatible *stub* upstream and the **real** Haechi proxy
5
+ in front of it (in `enforce` mode), then narrates what happens to a payload carrying
6
+ an email, a phone number, an API key, and a card number.
7
+
8
+ ```bash
9
+ node examples/local-proxy-demo/demo.mjs
10
+ # or, from the repo root:
11
+ npm run demo
12
+ ```
13
+
14
+ What it shows, in order:
15
+
16
+ 1. **The model only sees protected values** — the proxy detects and transforms the
17
+ payload *before* forwarding, so the stub (standing in for the model) receives
18
+ `[TOKEN:…]` for the email, a masked phone, and `[REDACTED:api_key]` for the key.
19
+ 2. **The token round-trip** — because the email was *tokenized* (reversible), the
20
+ caller gets `minji.kim@example.com` back, while the masked phone and redacted
21
+ secret stay protected. The model's own leaked secret in its reply is
22
+ response-protected too.
23
+ 3. **The audit log** carries detection metadata and is hash-chained — and never any
24
+ plaintext email/phone/key.
25
+ 4. **Day-2 operability** — the live `/__haechi/ready` readiness probe and the
26
+ Prometheus `/__haechi/metrics` surface.
27
+ 5. **A card number is blocked outright** (`403`, fail-closed) — it never reaches the
28
+ model.
29
+
30
+ Zero dependencies (only `node:` builtins + the in-repo `haechi` packages). The demo
31
+ is programmatic for reproducibility; for the real CLI invocation see the
32
+ [Quickstart](../../README.md#quickstart) and
33
+ [`docs/current/configuration.md`](../../docs/current/configuration.md).
34
+
35
+ ## Live demo against a real model
36
+
37
+ `live-demo.mjs` runs the same flow against a **real** upstream (vLLM / Ollama / any
38
+ OpenAI-compatible server) instead of the stub. It asks the model to repeat the phone
39
+ number it was given — and the model can only return the *masked* form, because the
40
+ real number never reached it. This is the run recorded in the README GIF
41
+ (`demo.tape` records the stub demo; `live-demo.tape` records this one).
42
+
43
+ ```bash
44
+ HAECHI_LIVE_UPSTREAM=http://127.0.0.1:8000 \
45
+ HAECHI_LIVE_MODEL="Qwen/Qwen3.6-35B-A3B-FP8" \
46
+ node examples/local-proxy-demo/live-demo.mjs
47
+ ```
48
+
49
+ `HAECHI_LIVE_TYPE` (default `vllm-openai`) and `HAECHI_LIVE_MODEL` override the target.
50
+ For Qwen3-style reasoning servers the request sets `chat_template_kwargs.enable_thinking
51
+ = false` so the reply is a terse line; non-reasoning servers ignore it.
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // Self-contained, reproducible Haechi demo — no remote model required.
3
+ //
4
+ // It stands up a tiny OpenAI-compatible *stub* upstream and the REAL Haechi proxy
5
+ // in front of it, then walks through what Haechi does to a payload that carries an
6
+ // email, a phone number, an API key, and a card:
7
+ // 1. the model only ever sees redacted/tokenized values (proven by echoing the
8
+ // exact body the stub received),
9
+ // 2. the caller gets the original email back (the token round-trip),
10
+ // 3. the audit log carries no plaintext,
11
+ // 4. the live /__haechi/metrics + /__haechi/ready operability surface,
12
+ // 5. a card is blocked outright (fail-closed).
13
+ //
14
+ // Run: node examples/local-proxy-demo/demo.mjs (or: npm run demo)
15
+ // Zero dependencies — only node: builtins and the in-repo haechi packages.
16
+
17
+ import { createServer } from "node:http";
18
+ import { mkdtemp, readFile } from "node:fs/promises";
19
+ import { tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+
22
+ import { createRuntime } from "../../packages/cli/runtime.mjs";
23
+ import { createHaechiProxy } from "../../packages/proxy/index.mjs";
24
+ import { initLocalKeyFile } from "../../packages/crypto/index.mjs";
25
+
26
+ const B = "\x1b[1m", D = "\x1b[2m", G = "\x1b[32m", Y = "\x1b[33m", C = "\x1b[36m", R = "\x1b[31m", X = "\x1b[0m";
27
+ const rule = () => console.log(D + "─".repeat(64) + X);
28
+ const scene = (n, t) => { console.log(); rule(); console.log(`${B}${C} ${n}. ${t}${X}`); rule(); };
29
+ const pause = (ms) => new Promise((r) => setTimeout(r, ms));
30
+
31
+ // A minimal OpenAI-compatible stub. It records the EXACT body it receives (which is
32
+ // whatever the proxy forwarded, i.e. the protected payload) and replies with a
33
+ // canned assistant message that itself leaks a secret, to exercise response protection.
34
+ function startStubUpstream() {
35
+ let lastReceived = null;
36
+ const server = createServer((req, res) => {
37
+ let body = "";
38
+ req.on("data", (c) => (body += c));
39
+ req.on("end", () => {
40
+ lastReceived = body;
41
+ // Echo the (already-protected) user content back so the response exercises the
42
+ // token round-trip, and append a leaked secret so response protection fires.
43
+ let echoed = "";
44
+ try { echoed = JSON.parse(body).messages.at(-1).content; } catch { /* ignore */ }
45
+ res.writeHead(200, { "content-type": "application/json" });
46
+ res.end(JSON.stringify({
47
+ id: "chatcmpl-demo",
48
+ object: "chat.completion",
49
+ choices: [{ index: 0, message: { role: "assistant",
50
+ content: `Noted — I will follow up. You wrote: "${echoed}" (our ref: token=DEMOleak9876543210notRealzyxwvu)` } }]
51
+ }));
52
+ });
53
+ });
54
+ return new Promise((resolve) => {
55
+ server.listen(0, "127.0.0.1", () => resolve({ server, url: `http://127.0.0.1:${server.address().port}`, received: () => lastReceived }));
56
+ });
57
+ }
58
+
59
+ async function main() {
60
+ console.log(`\n${B}🛡 Haechi — local end-to-end demo${X} ${D}(stub upstream, real proxy, enforce mode)${X}`);
61
+
62
+ const dir = await mkdtemp(join(tmpdir(), "haechi-demo-"));
63
+ const keyFile = join(dir, ".haechi", "dev.keys.json");
64
+ const auditPath = join(dir, ".haechi", "audit.jsonl");
65
+ await initLocalKeyFile(keyFile, { force: true });
66
+ const stub = await startStubUpstream();
67
+
68
+ const runtime = createRuntime({
69
+ mode: "enforce",
70
+ target: { type: "openai-compatible", upstream: stub.url },
71
+ policy: {
72
+ mode: "enforce",
73
+ presets: ["llm-redact"],
74
+ actions: { email: "tokenize", phone: "mask", secret: "redact", api_key: "redact", card: "block" }
75
+ },
76
+ tokenVault: { detokenizeResponses: true },
77
+ responseProtection: { enabled: true, mode: "enforce", failureMode: "fail-closed" },
78
+ keys: { keyFile },
79
+ audit: { path: auditPath }
80
+ });
81
+ const proxy = createHaechiProxy({ runtime, port: 0 });
82
+ const addr = await proxy.listen();
83
+ const base = `http://127.0.0.1:${addr.port}`;
84
+
85
+ // ── Scene 1 ───────────────────────────────────────────────────────────────
86
+ scene(1, "A prompt with an email, a phone number, and a deploy secret");
87
+ const userText = "Contact minji.kim@example.com or 010-1234-5678. Deploy api_key=DEMOkey0123456789notARealSecretabcdef.";
88
+ console.log(`${Y}you send →${X} ${userText}`);
89
+ await pause(700);
90
+ const r1 = await fetch(`${base}/v1/chat/completions`, {
91
+ method: "POST", headers: { "content-type": "application/json" },
92
+ body: JSON.stringify({ model: "demo", messages: [{ role: "user", content: userText }] })
93
+ });
94
+ const out1 = await r1.json();
95
+
96
+ scene(2, "What the MODEL actually received (the proxy protected it first)");
97
+ const forwarded = JSON.parse(stub.received());
98
+ console.log(`${G}model sees →${X} ${forwarded.messages[0].content}`);
99
+ console.log(`${D} (email → [TOKEN:…], phone → masked, secret → [REDACTED])${X}`);
100
+ await pause(700);
101
+
102
+ scene(3, "What YOU get back — the email token is restored (round-trip)");
103
+ console.log(`${G}you receive →${X} ${out1.choices[0].message.content}`);
104
+ console.log(`${D} (email restored from its token; phone stays masked; keys stay redacted both ways)${X}`);
105
+ await pause(700);
106
+
107
+ // ── Scene 4 ───────────────────────────────────────────────────────────────
108
+ scene(4, "The audit log — tamper-evident, and never any plaintext");
109
+ const audit = (await readFile(auditPath, "utf8")).trim().split("\n");
110
+ const ev = JSON.parse(audit[0]);
111
+ console.log(`${D}detections:${X} ${ev.detections.map((d) => `${d.type}→${d.action}`).join(" ")}`);
112
+ console.log(`${D}leaks the email/secret/phone?${X} ${audit.join("").match(/minji\.kim@|DEMOkey0123|010-1234-5678/) ? R + "YES" + X : G + "no — clean" + X}`);
113
+ await pause(700);
114
+
115
+ // ── Scene 5 ───────────────────────────────────────────────────────────────
116
+ scene(5, "Day-2 operability — live health + Prometheus metrics");
117
+ const ready = await (await fetch(`${base}/__haechi/ready`)).json();
118
+ console.log(`${D}/__haechi/ready →${X} ${ready.ready ? G + "ready" : R + "not ready"}${X} ${D}(audit writable: ${ready.checks?.auditWritable})${X}`);
119
+ const metrics = await (await fetch(`${base}/__haechi/metrics`)).text();
120
+ for (const line of metrics.split("\n").filter((l) => /^haechi_requests_total\{|^haechi_blocks_total /.test(l)).slice(0, 4)) {
121
+ console.log(`${D}metric:${X} ${line}`);
122
+ }
123
+ await pause(700);
124
+
125
+ // ── Scene 6 ───────────────────────────────────────────────────────────────
126
+ scene(6, "A card number is blocked outright (fail-closed)");
127
+ const r2 = await fetch(`${base}/v1/chat/completions`, {
128
+ method: "POST", headers: { "content-type": "application/json" },
129
+ body: JSON.stringify({ model: "demo", messages: [{ role: "user", content: "charge card 4242 4242 4242 4242 now" }] })
130
+ });
131
+ console.log(`${Y}you send →${X} "charge card 4242 4242 4242 4242 now"`);
132
+ console.log(`${G}proxy →${X} HTTP ${r2.status} ${r2.status === 403 ? R + B + "BLOCKED" + X : ""} ${D}(the card never reaches the model)${X}`);
133
+
134
+ console.log();
135
+ rule();
136
+ console.log(`${B}${G} ✓ done${X} ${D}— detection → redact/tokenize/block → forward → audit, all local.${X}`);
137
+ rule();
138
+ console.log(`${D} config reference: haechi.config.example.json · docs/current/configuration.md${X}\n`);
139
+
140
+ await proxy.close();
141
+ stub.server.close();
142
+ }
143
+
144
+ main().then(() => process.exit(0)).catch((e) => { console.error("demo failed:", e); process.exit(1); });
@@ -0,0 +1,19 @@
1
+ # VHS tape for the Haechi local end-to-end demo.
2
+ # Regenerate the README GIF with: vhs examples/local-proxy-demo/demo.tape
3
+ # (run from the repo root; requires vhs + ttyd + ffmpeg)
4
+
5
+ Output docs/assets/haechi-demo.gif
6
+
7
+ Set Shell "bash"
8
+ Set FontSize 15
9
+ Set Width 1180
10
+ Set Height 840
11
+ Set Padding 18
12
+ Set Theme "Catppuccin Mocha"
13
+ Set TypingSpeed 55ms
14
+
15
+ Sleep 500ms
16
+ Type "node examples/local-proxy-demo/demo.mjs"
17
+ Sleep 600ms
18
+ Enter
19
+ Sleep 9s