haechi 1.2.0 → 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.
- package/README.ko.md +46 -11
- package/README.md +46 -11
- package/docs/current/config-version.ko.md +2 -2
- package/docs/current/config-version.md +2 -2
- package/docs/current/configuration.ko.md +26 -10
- package/docs/current/configuration.md +26 -10
- package/docs/current/operations-runbook.ko.md +36 -2
- package/docs/current/operations-runbook.md +39 -2
- package/docs/current/release-process.ko.md +5 -1
- package/docs/current/release-process.md +5 -1
- package/docs/current/risk-register-release-gate.ko.md +4 -3
- package/docs/current/risk-register-release-gate.md +4 -3
- package/docs/current/shared-responsibility.ko.md +2 -2
- package/docs/current/shared-responsibility.md +2 -2
- package/docs/current/threat-model.ko.md +4 -3
- package/docs/current/threat-model.md +4 -3
- package/examples/local-proxy-demo/README.md +51 -0
- package/examples/local-proxy-demo/demo.mjs +144 -0
- package/examples/local-proxy-demo/demo.tape +19 -0
- package/examples/local-proxy-demo/live-demo.mjs +121 -0
- package/examples/local-proxy-demo/live-demo.tape +25 -0
- package/haechi.config.example.json +2 -1
- package/package.json +3 -1
- package/packages/cli/bin/haechi.mjs +3 -2
- package/packages/cli/runtime.mjs +12 -1
- package/packages/filter/index.mjs +679 -6
- package/packages/privacy-profiles/index.mjs +72 -3
- package/packages/protocol-adapters/index.mjs +99 -1
- package/packages/proxy/index.mjs +7 -1
- package/packages/stream-filter/index.mjs +69 -7
package/README.ko.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
|
-
[](docs/current/api-stability.md)
|
|
12
12
|
|
|
13
13
|
[English](README.md) | **한국어**
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ Haechi는 LLM·MCP·vLLM·Ollama 및 에이전트 payload가 모델, 도구, 로
|
|
|
18
18
|
|
|
19
19
|
이 저장소는 로컬 개발, 보안 설계 검토, 자체 호스팅 통합 실험을 위한 것입니다. 컴플라이언스를 보장하지는 않습니다.
|
|
20
20
|
|
|
21
|
-
**1.0.0이 첫 stable 릴리스입니다.** 1.0부터 public API는 strict semver 하의 frozen 계약입니다. `package.json`의 `exports` 표면, CLI의 기계 판독 동작, audit event schema, config key shape이 모두 major 버전 계약의 일부이며, 문서화된 deprecation 정책과 in-minor 보안 예외 하나가 함께 적용됩니다. [`docs/current/api-stability.md`](docs/current/api-stability.md)를 참고하세요.
|
|
21
|
+
**1.0.0이 첫 stable 릴리스입니다.** 1.0부터 public API는 strict semver 하의 frozen 계약입니다. `package.json`의 `exports` 표면, CLI의 기계 판독 동작, audit event schema, config key shape이 모두 major 버전 계약의 일부이며, 문서화된 deprecation 정책과 in-minor 보안 예외 하나가 함께 적용됩니다. [`docs/current/api-stability.md`](docs/current/api-stability.md)를 참고하세요. `haechi-*` 위성은 pre-1.0으로 유지되며 core와 독립적으로 버저닝됩니다([위성 패키지](#위성-패키지) 참고).
|
|
22
22
|
|
|
23
23
|
현재 범위는 로컬 도입에 초점을 맞춥니다.
|
|
24
24
|
|
|
@@ -30,6 +30,28 @@ Haechi는 LLM·MCP·vLLM·Ollama 및 에이전트 payload가 모델, 도구, 로
|
|
|
30
30
|
- `haechi audit-verify`: audit hash chain을 검증하고 head hash를 출력합니다
|
|
31
31
|
- `haechi mcp-wrap -- <command>`: MCP 서버를 양방향 stdio 보호로 감쌉니다
|
|
32
32
|
|
|
33
|
+
## 데모
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/raeseoklee/haechi/main/docs/assets/haechi-demo.gif" alt="Haechi 라이브 end-to-end 데모(실제 모델): 탐지 후 tokenize/mask/redact, 모델은 마스킹된 전화만 반복, 무평문 감사, 라이브 readiness + Prometheus metrics, 카드 차단" width="900">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
위 녹화는 실제 self-hosted 모델(vLLM의 Qwen3.6-35B)에 붙인 **라이브** end-to-end 실행입니다(`enforce` 모드). 모델에게 받은 전화번호를 그대로 반복하라고 시키면 — 진짜 번호는 모델에 도달조차 하지 않았으므로 **마스킹된** 형태만 돌려줄 수 있습니다. 무평문 감사, 라이브 `/__haechi/ready` + `/__haechi/metrics`, upstream 호출 전에 fail-closed로 차단되는 카드도 함께 보여줍니다.
|
|
40
|
+
|
|
41
|
+
직접 실행해 보십시오 — 백엔드 없이 재현 가능한 스텁 버전:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run demo
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
…또는 본인의 OpenAI-호환 서버 상대로:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
HAECHI_LIVE_UPSTREAM=http://127.0.0.1:8000 node examples/local-proxy-demo/live-demo.mjs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
[`examples/local-proxy-demo/`](examples/local-proxy-demo/)를 참고하십시오.
|
|
54
|
+
|
|
33
55
|
## 설치
|
|
34
56
|
|
|
35
57
|
```bash
|
|
@@ -76,7 +98,7 @@ upstream 요청은 `limits.upstreamTimeoutMs`(기본값 120000) 이후 타임아
|
|
|
76
98
|
|
|
77
99
|
## Local Inference Servers
|
|
78
100
|
|
|
79
|
-
Haechi는 OpenAI 호환 서버, vLLM, Ollama, llama.cpp용 프로토콜 adapter 프리셋을 제공합니다.
|
|
101
|
+
Haechi는 OpenAI 호환 서버, vLLM, Ollama, llama.cpp, Anthropic Messages API, 그리고 Google Gemini API용 프로토콜 adapter 프리셋을 제공합니다.
|
|
80
102
|
|
|
81
103
|
```json
|
|
82
104
|
{
|
|
@@ -96,7 +118,7 @@ Haechi는 OpenAI 호환 서버, vLLM, Ollama, llama.cpp용 프로토콜 adapter
|
|
|
96
118
|
}
|
|
97
119
|
```
|
|
98
120
|
|
|
99
|
-
그런 다음 OpenAI 호환 클라이언트를 `http://127.0.0.1:11016/v1`으로 향하게 합니다. Ollama 네이티브 API는 `target.adapter: "ollama"`를 사용하고 proxy를 통해 `/api/chat` 또는 `/api/generate`를 호출하세요.
|
|
121
|
+
그런 다음 OpenAI 호환 클라이언트를 `http://127.0.0.1:11016/v1`으로 향하게 합니다. Ollama 네이티브 API는 `target.adapter: "ollama"`를 사용하고 proxy를 통해 `/api/chat` 또는 `/api/generate`를 호출하세요. Claude는 `target.type: "anthropic"`을 설정하고 `/v1/messages`(또는 `/v1/messages/count_tokens`, `/v1/complete`)를 호출하세요. 클라이언트의 `x-api-key`/`anthropic-version` 헤더는 upstream으로 그대로 전달됩니다. Gemini는 `target.type: "gemini"`를 설정하고 모델이 경로에 포함된 엔드포인트 `/v1beta/models/{model}:generateContent`(또는 `:streamGenerateContent`, `:countTokens`, `:embedContent`, `:batchEmbedContents`)를 호출하세요. 클라이언트의 `x-goog-api-key`(또는 `?key=`)는 upstream으로 그대로 전달됩니다.
|
|
100
122
|
|
|
101
123
|
## 토큰 왕복
|
|
102
124
|
|
|
@@ -173,14 +195,27 @@ haechi auth revoke <id>
|
|
|
173
195
|
- **Rate limit**: identity별 분당 요청 수 → `429`(인메모리, 프로세스별).
|
|
174
196
|
- Audit 이벤트에는 **PII-safe** `identity`(keyed-HMAC subject/issuer이며 원시 값이 아닙니다)와 매핑된 `profile`이 들어가고, `auth_denied`/`model_not_allowed`/`rate_limited` 결정에는 credential이 포함되지 않습니다. `/__haechi/health`는 인증 없이 접근할 수 있습니다.
|
|
175
197
|
|
|
176
|
-
JWT/JWKS 인증과 KMS 기반 key custody는
|
|
198
|
+
JWT/JWKS 인증과 KMS 기반 key custody(및 기타 선택 기능)는 **`haechi-*` 위성 패키지**로 제공됩니다 — 아래 [위성 패키지](#위성-패키지)를 참고하세요.
|
|
199
|
+
|
|
200
|
+
## 위성 패키지
|
|
201
|
+
|
|
202
|
+
선택 기능은 **npm에 독립 발행되는 `haechi-*` 패키지**로 제공됩니다 — 각각 core와 별도로 버저닝되고, 기본적으로 `node:` 전용이며(KMS나 Redis 클라이언트 같은 무거운 SDK는 optional peer), `haechi` peer 범위를 `>=0.8.0 <2.0.0`으로 선언합니다(상한이 core major를 따라가므로 core minor가 위성 설치를 깨뜨리지 않습니다).
|
|
203
|
+
|
|
204
|
+
**위성과 함께 core를 반드시 설치하세요** — `haechi`는 **번들되지 않은 peer dependency**이므로, 위성만으로는 동작하지 않습니다:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm install haechi haechi-<satellite>
|
|
208
|
+
```
|
|
177
209
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
| 패키지 | 추가하는 기능 |
|
|
211
|
+
|---|---|
|
|
212
|
+
| [`haechi-auth-jwt`](satellites/auth-jwt/) | 헤드리스 JWKS bearer(JWT) `authProvider`. 재사용 가능한 JWS 검증기(`createJwtVerifier`)를 추가로 export합니다. |
|
|
213
|
+
| [`haechi-auth-oidc`](satellites/auth-oidc/) | 대화형 OIDC 세션 브로커(authorization-code + PKCE) — 대시보드의 사람 로그인. `haechi-auth-jwt`를 재사용합니다. |
|
|
214
|
+
| [`haechi-crypto-kms`](satellites/crypto-kms/) | `keys.provider: external`용 envelope 암호화 `cryptoProvider` — AWS, GCP(`./gcp`), Azure(`./azure`), HashiCorp Vault Transit(`./vault`, `node:` 전용) 백엔드. |
|
|
215
|
+
| [`haechi-dashboard`](satellites/dashboard/) | audit 로그와 hash chain 상태를 보는 zero-dependency 읽기 전용 audit 뷰어(`node:http`). |
|
|
216
|
+
| [`haechi-ratelimit-redis`](satellites/ratelimit-redis/) | `providers.rateLimiter` 주입 시임을 통한 다중 복제용 공유 저장소(Redis 기반) `rateLimiter`. |
|
|
182
217
|
|
|
183
|
-
|
|
218
|
+
각 패키지의 README가 사용법과 정확한 peer 요구사항을 다룹니다. 위성의 무거운 SDK는 해당 백엔드를 쓸 때만 설치되는 optional peer라 core는 zero-dependency로 유지됩니다.
|
|
184
219
|
|
|
185
220
|
## 설정
|
|
186
221
|
|
|
@@ -189,7 +224,7 @@ JWT/JWKS 인증과 KMS 기반 key custody는 `haechi-*` 위성 패키지로 제
|
|
|
189
224
|
| 키 | 기본값 | 설명 |
|
|
190
225
|
|---|---|---|
|
|
191
226
|
| `mode` / `policy.mode` | `dry-run` | `dry-run`과 `report-only`는 탐지와 audit만 하고, `enforce`는 변환/차단을 적용합니다. `policy.mode`가 `mode`보다 우선합니다 |
|
|
192
|
-
| `target.type` / `target.adapter` | `llm-http` / `openai-compatible` | upstream 프로토콜: `openai-compatible`, `vllm-openai`, `ollama`, `llama-cpp`. 알 수 없는 type은 fail-closed로 처리됩니다 |
|
|
227
|
+
| `target.type` / `target.adapter` | `llm-http` / `openai-compatible` | upstream 프로토콜: `openai-compatible`, `vllm-openai`, `ollama`, `llama-cpp`, `anthropic`, `gemini`. 알 수 없는 type은 fail-closed로 처리됩니다 |
|
|
193
228
|
| `target.upstream` | `http://127.0.0.1:9999` | proxy가 요청을 전달하는 유일한 upstream(절대 URL 요청 대상은 거부됩니다) |
|
|
194
229
|
| `proxy.host` / `proxy.port` | `127.0.0.1` / `11016` | proxy 바인드 주소. 아래 remote 바인딩 참고 |
|
|
195
230
|
| `responseProtection.enabled` | `false` | upstream JSON 응답을 검사합니다. `failureMode: fail-closed`는 비JSON/압축/대용량 응답을 거부합니다 |
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
|
-
[](docs/current/api-stability.md)
|
|
12
12
|
|
|
13
13
|
**English** | [한국어](README.ko.md)
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ The name comes from Haechi, a Korean guardian figure associated with discernment
|
|
|
18
18
|
|
|
19
19
|
This repository is intended for local development, security design review, and self-hosted integration experiments. It is not a compliance guarantee.
|
|
20
20
|
|
|
21
|
-
**1.0.0 is the first stable release.** From 1.0 the public API is a frozen contract under strict semver: the `package.json` `exports` surface, the CLI's machine-readable behavior, the audit event schema, and the config key shape are all part of the major-versioned contract, with a documented deprecation policy and a one in-minor security exception. See [`docs/current/api-stability.md`](docs/current/api-stability.md). The
|
|
21
|
+
**1.0.0 is the first stable release.** From 1.0 the public API is a frozen contract under strict semver: the `package.json` `exports` surface, the CLI's machine-readable behavior, the audit event schema, and the config key shape are all part of the major-versioned contract, with a documented deprecation policy and a one in-minor security exception. See [`docs/current/api-stability.md`](docs/current/api-stability.md). The `haechi-*` satellites stay pre-1.0 and version independently of core (see [Satellite packages](#satellite-packages)).
|
|
22
22
|
|
|
23
23
|
The current scope focuses on local adoption:
|
|
24
24
|
|
|
@@ -30,6 +30,28 @@ The current scope focuses on local adoption:
|
|
|
30
30
|
- `haechi audit-verify`: verify the audit hash chain and print its head hash
|
|
31
31
|
- `haechi mcp-wrap -- <command>`: wrap an MCP server with bidirectional stdio protection
|
|
32
32
|
|
|
33
|
+
## Demo
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/raeseoklee/haechi/main/docs/assets/haechi-demo.gif" alt="Haechi live end-to-end demo against a real model: detection then tokenize/mask/redact, the masked phone the model can only repeat, a no-plaintext audit, live readiness + Prometheus metrics, and a blocked card" width="900">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
The recording above is a **live** end-to-end run against a real self-hosted model (Qwen3.6-35B on vLLM) in `enforce` mode. The model is asked to repeat the phone number it was given — and it can only return the **masked** form, because the real number never reached it. It also shows the no-plaintext audit, the live `/__haechi/ready` + `/__haechi/metrics` surface, and a card blocked fail-closed before any upstream call.
|
|
40
|
+
|
|
41
|
+
Run it yourself — a no-backend, reproducible version with a stub upstream:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run demo
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
…or against your own OpenAI-compatible server:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
HAECHI_LIVE_UPSTREAM=http://127.0.0.1:8000 node examples/local-proxy-demo/live-demo.mjs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
See [`examples/local-proxy-demo/`](examples/local-proxy-demo/).
|
|
54
|
+
|
|
33
55
|
## Install
|
|
34
56
|
|
|
35
57
|
```bash
|
|
@@ -76,7 +98,7 @@ Upstream requests time out after `limits.upstreamTimeoutMs` (default 120000) and
|
|
|
76
98
|
|
|
77
99
|
## Local Inference Servers
|
|
78
100
|
|
|
79
|
-
Haechi includes protocol adapter presets for OpenAI-compatible servers, vLLM, Ollama,
|
|
101
|
+
Haechi includes protocol adapter presets for OpenAI-compatible servers, vLLM, Ollama, llama.cpp, the Anthropic Messages API, and the Google Gemini API.
|
|
80
102
|
|
|
81
103
|
```json
|
|
82
104
|
{
|
|
@@ -96,7 +118,7 @@ Haechi includes protocol adapter presets for OpenAI-compatible servers, vLLM, Ol
|
|
|
96
118
|
}
|
|
97
119
|
```
|
|
98
120
|
|
|
99
|
-
Then point an OpenAI-compatible client at `http://127.0.0.1:11016/v1`. For Ollama native APIs, use `target.adapter: "ollama"` and call `/api/chat` or `/api/generate` through the proxy.
|
|
121
|
+
Then point an OpenAI-compatible client at `http://127.0.0.1:11016/v1`. For Ollama native APIs, use `target.adapter: "ollama"` and call `/api/chat` or `/api/generate` through the proxy. For Claude, set `target.type: "anthropic"` and call `/v1/messages` (or `/v1/messages/count_tokens`, `/v1/complete`); the client's `x-api-key`/`anthropic-version` headers are forwarded to the upstream unchanged. For Gemini, set `target.type: "gemini"` and call the model-in-path endpoints `/v1beta/models/{model}:generateContent` (or `:streamGenerateContent`, `:countTokens`, `:embedContent`, `:batchEmbedContents`); the client's `x-goog-api-key` (or `?key=`) is forwarded to the upstream unchanged.
|
|
100
122
|
|
|
101
123
|
## Token Round-Trip
|
|
102
124
|
|
|
@@ -173,14 +195,27 @@ haechi auth revoke <id>
|
|
|
173
195
|
- **Rate limit**: per-identity requests-per-minute → `429` (in-memory, per-process).
|
|
174
196
|
- Audit events carry the **PII-safe** `identity` (keyed-HMAC subject/issuer, never raw values) and the resolved `profile`; `auth_denied` / `model_not_allowed` / `rate_limited` decisions never include credentials. `/__haechi/health` stays unauthenticated.
|
|
175
197
|
|
|
176
|
-
JWT/JWKS auth and KMS-backed key custody
|
|
198
|
+
JWT/JWKS auth and KMS-backed key custody (and other optional capabilities) ship as the **`haechi-*` satellite packages** — see [Satellite packages](#satellite-packages) below.
|
|
199
|
+
|
|
200
|
+
## Satellite packages
|
|
201
|
+
|
|
202
|
+
Optional capabilities ship as independently-published **`haechi-*` packages on npm** — each versioned separately from core, `node:`-only by default (heavy SDKs like a KMS or Redis client are optional peers), and each declaring a `haechi` peer range of `>=0.8.0 <2.0.0` (the upper bound tracks the core major, so a core minor never breaks a satellite install).
|
|
203
|
+
|
|
204
|
+
**Install the core alongside any satellite** — `haechi` is a **peer dependency, not bundled**, so a satellite does nothing on its own:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm install haechi haechi-<satellite>
|
|
208
|
+
```
|
|
177
209
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
| Package | What it adds |
|
|
211
|
+
|---|---|
|
|
212
|
+
| [`haechi-auth-jwt`](satellites/auth-jwt/) | Headless JWKS bearer (JWT) `authProvider`; additively exports a reusable JWS verifier (`createJwtVerifier`). |
|
|
213
|
+
| [`haechi-auth-oidc`](satellites/auth-oidc/) | Interactive OIDC session broker (authorization-code + PKCE) — the dashboard's human login. Reuses `haechi-auth-jwt`. |
|
|
214
|
+
| [`haechi-crypto-kms`](satellites/crypto-kms/) | Envelope-encryption `cryptoProvider` for `keys.provider: external` — AWS, GCP (`./gcp`), Azure (`./azure`), and HashiCorp Vault Transit (`./vault`, `node:`-only) backends. |
|
|
215
|
+
| [`haechi-dashboard`](satellites/dashboard/) | Zero-dependency, read-only audit viewer (`node:http`) over the audit log and its hash-chain status. |
|
|
216
|
+
| [`haechi-ratelimit-redis`](satellites/ratelimit-redis/) | Shared-store (Redis-backed) `rateLimiter` for multi-replica deployments, via the `providers.rateLimiter` injection seam. |
|
|
182
217
|
|
|
183
|
-
The satellites
|
|
218
|
+
Each package's README covers its usage and exact peer requirements. The satellites keep core zero-dependency: their heavy SDKs are optional peers installed only when you use that backend.
|
|
184
219
|
|
|
185
220
|
## Configuration
|
|
186
221
|
|
|
@@ -189,7 +224,7 @@ The satellites are `node:`-only by default (heavy SDKs are optional peers) and k
|
|
|
189
224
|
| Key | Default | Meaning |
|
|
190
225
|
|---|---|---|
|
|
191
226
|
| `mode` / `policy.mode` | `dry-run` | `dry-run` and `report-only` detect + audit only; `enforce` transforms/blocks. `policy.mode` wins over `mode` |
|
|
192
|
-
| `target.type` / `target.adapter` | `llm-http` / `openai-compatible` | Upstream protocol: `openai-compatible`, `vllm-openai`, `ollama`, `llama-cpp`. Unknown types fail closed |
|
|
227
|
+
| `target.type` / `target.adapter` | `llm-http` / `openai-compatible` | Upstream protocol: `openai-compatible`, `vllm-openai`, `ollama`, `llama-cpp`, `anthropic`, `gemini`. Unknown types fail closed |
|
|
193
228
|
| `target.upstream` | `http://127.0.0.1:9999` | The only upstream the proxy will forward to (absolute-URL request targets are rejected) |
|
|
194
229
|
| `proxy.host` / `proxy.port` | `127.0.0.1` / `11016` | Proxy bind address. See remote binding below |
|
|
195
230
|
| `responseProtection.enabled` | `false` | Inspect upstream JSON responses. `failureMode: fail-closed` rejects non-JSON/compressed/oversized responses |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi `configVersion` & 업그레이드 노트
|
|
2
2
|
|
|
3
|
-
- 상태: Living document (코어 1.
|
|
3
|
+
- 상태: Living document (코어 1.3.x 추적)
|
|
4
4
|
|
|
5
5
|
`configVersion`는 `haechi.config.json`(및 `haechi.config.example.json`) 최상위에 찍히는 단일 정수입니다. 향후 호환성을 깨는 설정 스키마 변경이 구체적으로 게이트할 수 있는 **버전 앵커**로서, 다른 Haechi 빌드가 쓴 설정을 조용히 잘못 읽는 일을 막습니다.
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
| `configVersion` | 코어 라인 | 노트 |
|
|
23
23
|
|---|---|---|
|
|
24
|
-
| `1` | 1.0 – 1.
|
|
24
|
+
| `1` | 1.0 – 1.3.x | 최초 스탬프. 모든 키는 1.0 frozen 설정 표면(`api-stability.md` §2.4)에 대해 additive입니다. 1.1.x의 additive 키(`logging`, `metrics`, WS4-B의 `limits.maxInFlight` / `limits.shutdownGraceMs` / `limits.requestTimeoutMs` / `limits.headersTimeoutMs`, 그리고 `configVersion` 자체)와 1.2.0 신뢰성 강화 키(`filters.minConfidence` / `filters.allowlist`, `proxy.tls` / `proxy.trustForwardedProto`)는 모두 이전 동작을 기본값으로 합니다. 1.3.0의 추가는 새 키가 아니라 새 *값*입니다 — `target.type`의 `anthropic`/`gemini`, 추가 탐지 타입, `asia-pdpa`/`jp-appi` `privacy.profile` 값 — 따라서 설정 스키마(및 `configVersion`)는 변경되지 않습니다. 마이그레이션 불필요. |
|
|
25
25
|
|
|
26
26
|
## 업그레이드
|
|
27
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi `configVersion` & Upgrade Notes
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.3.x)
|
|
4
4
|
|
|
5
5
|
`configVersion` is a single integer stamped at the top of `haechi.config.json`
|
|
6
6
|
(and `haechi.config.example.json`). It is a **versioned anchor** so a future
|
|
@@ -34,7 +34,7 @@ the "policies only get stronger / fail closed" invariant intact.
|
|
|
34
34
|
|
|
35
35
|
| `configVersion` | Core line | Notes |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `1` | 1.0 – 1.
|
|
37
|
+
| `1` | 1.0 – 1.3.x | Initial stamp. All keys are additive over the 1.0 frozen config surface (`api-stability.md` §2.4). The 1.1.x additive keys (`logging`, `metrics`, the WS4-B `limits.maxInFlight` / `limits.shutdownGraceMs` / `limits.requestTimeoutMs` / `limits.headersTimeoutMs`, `configVersion` itself) and the 1.2.0 Reliability-Hardening keys (`filters.minConfidence` / `filters.allowlist`, `proxy.tls` / `proxy.trustForwardedProto`) all default to prior behavior. The 1.3.0 additions are new *values*, not new keys — `target.type` `anthropic`/`gemini`, additional detection types, and the `asia-pdpa`/`jp-appi` `privacy.profile` values — so the config schema (and `configVersion`) is unchanged. No migration needed. |
|
|
38
38
|
|
|
39
39
|
## Upgrading
|
|
40
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi 설정 레퍼런스
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document(core 1.
|
|
3
|
+
- 문서 상태: Living document(core 1.3.x 추적)
|
|
4
4
|
|
|
5
5
|
`haechi init`은 `haechi.config.json`을 생성하며, 비밀 정보를 포함하지 않는 템플릿은 `haechi.config.example.json`에 있습니다. 모든 커맨드는 `--config <path>`로 설정 파일을 읽습니다(기본값: `haechi.config.json`). 설정은 **fail-closed 방식으로 검증**됩니다. 알 수 없는 provider, 범위를 벗어난 숫자, 잘못된 형식의 값은 자동으로 무시되지 않고 로드 시점에 오류를 발생시킵니다. `haechi config`는 이 레퍼런스를 출력하며, `haechi status`는 특정 설정 파일의 *실제 적용* 상태를 출력합니다.
|
|
6
6
|
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
| 키 | 타입 / 값 | 기본값 | 설명 |
|
|
40
40
|
|---|---|---|---|
|
|
41
|
-
| `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` | `llm-http` | 프로토콜 adapter를 선택합니다. `llm-http`는 `openai-compatible`의 별칭입니다. 알 수 없는 값은 로드 시 **fail-closed**로 처리됩니다. |
|
|
41
|
+
| `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` \| `anthropic` \| `gemini` | `llm-http` | 프로토콜 adapter를 선택합니다. `llm-http`는 `openai-compatible`의 별칭입니다. `anthropic`은 Anthropic Messages API(`/v1/messages`, `/v1/messages/count_tokens`, `/v1/complete`)를 대상으로 합니다. 클라이언트가 Anthropic의 `x-api-key`/`anthropic-version` 헤더를 제공하면 프록시가 이를 그대로 전달합니다. `gemini`는 Google Gemini API를 대상으로 합니다. 엔드포인트는 모델명이 경로에 포함되고 `:method` 접미사를 사용하는 형태입니다(`/v1beta/models/{model}:generateContent`, `:streamGenerateContent`(SSE), `:countTokens`, `:embedContent`, `:batchEmbedContents`; `/v1` 또는 `/v1beta` 접두사, 임의의 `{model}`). 클라이언트가 Gemini의 `x-goog-api-key`(또는 `?key=`)를 제공하면 프록시가 이를 그대로 전달합니다. 알 수 없는 값은 로드 시 **fail-closed**로 처리됩니다. |
|
|
42
42
|
| `target.adapter` | 동일한 값 집합 | `openai-compatible` | adapter를 명시적으로 지정합니다. 보통은 설정하지 않고 `type`이 결정하도록 두면 됩니다. |
|
|
43
43
|
| `target.upstream` | URL 문자열 | `http://127.0.0.1:9999` | proxy가 요청을 전달하는 유일한 upstream입니다. 요청 대상은 origin-form 경로여야 하며, 절대 URL 대상은 거부됩니다(SSRF 방어). |
|
|
44
44
|
|
|
@@ -103,8 +103,9 @@ upstream JSON 응답을 검사합니다(기본적으로 꺼져 있습니다 —
|
|
|
103
103
|
| 키 | 타입 / 값 | 기본값 | 설명 |
|
|
104
104
|
|---|---|---|---|
|
|
105
105
|
| `filters.customRules` | 규칙 객체 배열 | `[]` | 추가 탐지 규칙입니다: `{ id, type, pattern, flags?, confidence? }`. 패턴은 ReDoS 검사를 통과해야 하며(≤500자, 중첩 한정자 없음, 역참조 없음), 안전하지 않으면 로드 시 거부됩니다. |
|
|
106
|
-
| `filters.minConfidence` | `[0, 1]` 범위의 숫자 | `0` | 정밀도 다이얼입니다. 각 규칙은 `confidence`(0.6~0.95)를 가지며, confidence가 이 임계값 **미만**인 탐지는 policy 결정 전에 버려집니다. 기본값 `0`은 아무것도 게이트하지 않아 기존 동작을 보존합니다. **하드 블록 예외:** 하드 블록 타입(`secret`, `api_key`, `kr_rrn`, `card`)은 confidence만으로는 **절대** 버려지지 않습니다 — `minConfidence`는 정밀도 위험이 큰
|
|
107
|
-
| `filters.allowlist` | 문자열 및/또는 `{ value?, path? }` 의 배열 | `[]` | 운영자 false-positive 예외입니다. 매칭된 **value**가 문자열/`value` 항목과 같거나, PII-safe JSON **path**(audit에 표시되는 해시된 `pathText`)가 `path` 항목과 같은 탐지는 policy 결정 전에 억제됩니다(항목이 `value`와 `path`를 모두 설정하면 **둘 다** 일치해야 합니다). **하드 블록 예외:** 하드 블록 타입(`secret`/`api_key`/`kr_rrn`/`card`)을 억제하려는 항목은 **무시되며** 탐지는 그대로 발생합니다 — allowlist는 양성(benign)
|
|
106
|
+
| `filters.minConfidence` | `[0, 1]` 범위의 숫자 | `0` | 정밀도 다이얼입니다. 각 규칙은 `confidence`(0.6~0.95)를 가지며, confidence가 이 임계값 **미만**인 탐지는 policy 결정 전에 버려집니다. 기본값 `0`은 아무것도 게이트하지 않아 기존 동작을 보존합니다. **하드 블록 예외:** 하드 블록 타입(`secret`, `api_key`, `kr_rrn`, `card`, 그리고 강한 앵커 국가-ID `fr_nir`, `es_dni`, `it_codice_fiscale`, `sg_nric`)은 confidence만으로는 **절대** 버려지지 않습니다 — `minConfidence`는 정밀도 위험이 큰 소프트/다이얼 가능 타입(예: `phone`, `email`, `jp_mynumber`, `uk_nino`, `in_aadhaar`, `de_steuer_id`, `nl_bsn`, `injection`)만 다듬으므로, confidence가 낮은 자격증명/PII 누출도 여전히 조치됩니다(fail-closed). |
|
|
107
|
+
| `filters.allowlist` | 문자열 및/또는 `{ value?, path? }` 의 배열 | `[]` | 운영자 false-positive 예외입니다. 매칭된 **value**가 문자열/`value` 항목과 같거나, PII-safe JSON **path**(audit에 표시되는 해시된 `pathText`)가 `path` 항목과 같은 탐지는 policy 결정 전에 억제됩니다(항목이 `value`와 `path`를 모두 설정하면 **둘 다** 일치해야 합니다). **하드 블록 예외:** 하드 블록 타입(`secret`/`api_key`/`kr_rrn`/`card`/`fr_nir`/`es_dni`/`it_codice_fiscale`/`sg_nric`)을 억제하려는 항목은 **무시되며** 탐지는 그대로 발생합니다 — allowlist는 양성(benign) **소프트/다이얼 가능** 타입 FP(예: `jp_mynumber`/`in_aadhaar` 12자리 ID 오탐, `de_steuer_id` 11자리 ID 오탐, `nl_bsn` 9자리 ID 오탐, format-only `uk_nino`)만 정리할 수 있고, 자격증명/강한-앵커-PII 누출은 절대 침묵시킬 수 없습니다. 모든 억제와 모든 `minConfidence` 드롭은 개수와 타입으로 **감사 로그에 기록됩니다**(`summary.suppressedByType` / `summary.droppedByType` / `suppressedCount` / `droppedCount`) — 원시 값은 절대 기록하지 않습니다. 규칙 전체를 삭제하지 않고 양성 FP 하나만 정리할 때 사용하십시오. |
|
|
108
|
+
| `filters.decodeAndRescan` | boolean | `false` | opt-in base64/percent **디코딩 후 재검사**입니다(WS2d 잔여). 기본값 `false`에서는 탐지가 이전과 바이트 단위로 동일합니다 — base64·percent로 인코딩된 값을 디코딩하지 **않습니다**. `true`일 때, 일반 NFKC 스캔 이후 base64/base64url로 **보이는** string leaf(고정 알파벳, 유효한 길이, `16…8192` 바이트, 같은 leaf로 round-trip, **유효한 UTF-8** 디코딩)이거나 `%XX` 이스케이프를 포함하는 leaf(`decodeURIComponent`)를 디코딩하여 같은 규칙·validator로 재검사합니다. 디코딩된 매칭은 인코딩된 leaf에 offset이 없으므로, **WHOLE-LEAF** 탐지(`start:0,end:leaf.length`, value = 인코딩된 leaf 전체)로 fail closed됩니다 — transform이 leaf 전체를 redact/block합니다. **정밀도 가드:** 디코딩된 매칭은 validator 기반이거나 하드 블록 타입일 때만 발생합니다(Luhn 통과 `card`, 체크섬 `kr_rrn`/`us_ssn`, IBAN mod-97, 또는 앵커된 규칙의 `secret`/`api_key`). validator 없는 디코딩된 소프트 타입 매칭(맨 전화번호 형태)은 발생하지 않으므로 무작위 base64는 오탐하지 않습니다. 새 런타임 의존성은 없습니다(`node:buffer` Buffer + `decodeURIComponent` 빌트인). 다른 인코딩(gzip/hex/중첩/커스텀 알파벳)은 범위 밖입니다. |
|
|
108
109
|
|
|
109
110
|
### 탐지 벤치마크
|
|
110
111
|
|
|
@@ -147,7 +148,7 @@ npm run scan:detection # CI 회귀 게이트: 어떤 type이라도 baseline
|
|
|
147
148
|
|
|
148
149
|
| 키 | 타입 / 값 | 기본값 | 설명 |
|
|
149
150
|
|---|---|---|---|
|
|
150
|
-
| `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `us-general` | `null` | 집행 전에 지역별 기준 action 집합을 적용합니다. 프로필은 명시적 action을 **강화**할 수는 있지만 약화할 수는 없습니다. 엔지니어링 기본값이며, 법적 자문이 아닙니다. |
|
|
151
|
+
| `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `asia-pdpa` \| `us-general` \| `jp-appi` | `null` | 집행 전에 지역별 기준 action 집합을 적용합니다. 프로필은 명시적 action을 **강화**할 수는 있지만 약화할 수는 없습니다. `eu-gdpr`는 EU 국가 ID(`fr_nir`/`es_dni`/`uk_nino`/`it_codice_fiscale`/`de_steuer_id`/`nl_bsn`)를 block하고, `asia-pdpa`(싱가포르 PDPA / 인도 DPDP)는 `sg_nric`/`in_aadhaar`를 block하며(혼합 지역 페이로드를 위해 다른 체크섬 국가 ID도 함께 block), `jp-appi`는 `jp_mynumber`를 block하고, 모든 프로필이 `jp_mynumber`를 block합니다(체크섬 국가-ID 누출). 엔지니어링 기본값이며, 법적 자문이 아닙니다. |
|
|
151
152
|
|
|
152
153
|
## `logging`
|
|
153
154
|
|
|
@@ -273,13 +274,13 @@ rate limiter는 **주입 가능한 collaborator**이며, `createRuntime(config,
|
|
|
273
274
|
const runtime = createRuntime(config, { rateLimiter });
|
|
274
275
|
```
|
|
275
276
|
|
|
276
|
-
주입된 `rateLimiter`는 `allow(key, limit)
|
|
277
|
+
주입된 `rateLimiter`는 `allow(key, limit)`을 구현해야 하며, `boolean` **또는** `Promise<boolean>`을 반환합니다(`key`는 identity별 버킷, `limit`은 resolve된 `requestsPerMinute`입니다). 구현하지 않으면 `createRuntime`이 construction 시점에 fail-closed로 throw합니다. proxy는 결과를 `await`하므로 동기 boolean과 비동기 공유 저장소 limiter가 동일하게 동작합니다 — 내장 기본값은 동기로 유지되고, 비동기로 resolve되는 Redis 기반 limiter도 올바르게 gate합니다. proxy는 rate 통제 대상 요청마다 `runtime.rateLimiter`를 참조합니다.
|
|
277
278
|
|
|
278
|
-
**기본값**은 프로세스별 인메모리 fixed-window 카운터입니다. 재시작 시 초기화되며 **replica 간에 공유되지 않으므로**, load balancer 뒤에서 총 처리량은 replica 수만큼 곱해집니다. window map은 self-bounding입니다(lazy, amortized sweep로 만료된 one-shot identity를 제거합니다 — 백그라운드 timer 없음). 다중 replica 배포에서는 공유 front door에서 identity별 limit을 강제하거나, 동일한 `allow(key, limit)` 계약을 만족하는 공유 저장소 구현(예: Redis 기반)을
|
|
279
|
+
**기본값**은 프로세스별 인메모리 fixed-window 카운터입니다. 재시작 시 초기화되며 **replica 간에 공유되지 않으므로**, load balancer 뒤에서 총 처리량은 replica 수만큼 곱해집니다. window map은 self-bounding입니다(lazy, amortized sweep로 만료된 one-shot identity를 제거합니다 — 백그라운드 timer 없음). 다중 replica 배포에서는 공유 front door에서 identity별 limit을 강제하거나, 동일한 `allow(key, limit)` 계약을 만족하는 공유 저장소 구현(예: Redis 기반)을 주입하십시오 — [`haechi-ratelimit-redis`](./shared-responsibility.ko.md#4-수평-확장--다중-복제) satellite가 레퍼런스 구현입니다. [Shared responsibility §4](./shared-responsibility.ko.md#4-수평-확장--다중-복제)를 참고하십시오.
|
|
279
280
|
|
|
280
281
|
## Detection type과 action
|
|
281
282
|
|
|
282
|
-
내장 탐지 `type` 값은 다음과 같습니다: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `us_ssn`, `iban`, `injection`(응답 방향 휴리스틱, 기본 report-only). 커스텀 규칙으로 새로운 type을 추가할 수 있습니다.
|
|
283
|
+
내장 탐지 `type` 값은 다음과 같습니다: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `us_ssn`, `iban`, `jp_mynumber`, `fr_nir`, `es_dni`, `uk_nino`, `it_codice_fiscale`, `sg_nric`, `in_aadhaar`, `de_steuer_id`, `nl_bsn`, `injection`(응답 방향 휴리스틱, 기본 report-only). 커스텀 규칙으로 새로운 type을 추가할 수 있습니다.
|
|
283
284
|
|
|
284
285
|
### 지원하는 자격증명·PII 매트릭스
|
|
285
286
|
|
|
@@ -295,11 +296,26 @@ const runtime = createRuntime(config, { rateLimiter });
|
|
|
295
296
|
| `card` | 결제 카드(PAN) | Luhn validator, 13–19자리 | — |
|
|
296
297
|
| `us_ssn` | 미국 사회보장번호 | `AAA-GG-SSSS` + SSA 범위 validator(area `000`/`666`/`900-999`, group `00`, serial `0000` 거부) | 구분자 필수이며, bare 9자리 id는 SSN이 아닙니다. |
|
|
297
298
|
| `iban` | 국제 은행계좌번호 | **mod-97 checksum** validator | checksum이 정밀도 가드입니다 — IBAN 형태이지만 97 비검증 문자열은 거부됩니다. |
|
|
298
|
-
| `
|
|
299
|
+
| `jp_mynumber` | 일본 마이넘버(個人番号) | 12자리 + **mod-11 가중 check digit** | check digit이 정밀도 가드입니다; check 불일치 12자리 run은 거부됩니다. **하드 블록.** |
|
|
300
|
+
| `fr_nir` | 프랑스 NIR / INSEE 사회보장번호 | 15자 + **`97 - (앞13 mod 97)` 제어키**(코르시카 `2A`→19, `2B`→18) | 제어키 불일치는 거부됩니다. **하드 블록.** |
|
|
301
|
+
| `es_dni` | 스페인 DNI / NIE | 8자리(DNI) 또는 `X/Y/Z`+7자리(NIE) + **mod-23 check letter**(NIE `X/Y/Z`→`0/1/2`) | check letter 불일치는 거부됩니다. **하드 블록.** |
|
|
302
|
+
| `uk_nino` | 영국 국민보험번호 | `[A-CEGHJ-PR-TW-Z][A-CEGHJ-NPR-TW-Z]\d{6}[A-D]` + 문서화된 무효 prefix 제외(`BG`/`GB`/`NK`/`KN`/`TN`/`NT`/`ZZ`, 2번째 글자 `O`) | **format-only — checksum이 없으므로** 하드 블록 타입이 아닙니다(dial 가능: 운영자가 양성 FP를 allowlist 할 수 있음). |
|
|
303
|
+
| `it_codice_fiscale` | 이탈리아 codice fiscale | `[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]` + **mod-26 check character**(앞 15자에 대한 홀수/짝수 위치 테이블) | check character 불일치는 거부됩니다. **하드 블록** — 희귀한 16자 알파+숫자 혼합 형태에 비숫자 구조 앵커가 있습니다(형태에 대한 측정 충돌률 ~3.8%). |
|
|
304
|
+
| `sg_nric` | 싱가포르 NRIC / FIN | `[STFGM]\d{7}[A-Z]` + **가중합 check letter**(가중치 2,7,6,5,4,3,2; prefix별 offset; series별 letter 테이블) | check letter 불일치는 거부됩니다. **하드 블록** — 희귀한 형태에 비숫자 앵커가 둘(prefix letter + check letter)입니다(측정 충돌률 ~3.9%). |
|
|
305
|
+
| `in_aadhaar` | 인도 Aadhaar | 12자리(`0`/`1`로 시작 불가) + **Verhoeff 체크섬** | Verhoeff check digit 불일치는 거부됩니다. **하드 블록 타입이 아닙니다(dial 가능)** — 흔한 12자리 형태에 대한 Verhoeff는 무작위 run의 ~1/10이 통과하므로(측정 ~9.9%, `jp_mynumber` footgun), 운영자가 양성 12자리 ID FP를 allowlist 할 수 있습니다. |
|
|
306
|
+
| `de_steuer_id` | 독일 세금 ID(Steuer-ID) | 11자리 + **ISO 7064 MOD 11,10** check digit + 앞 10자리에 "정확히 한 숫자만 반복" 구조 테스트 | check digit 또는 반복 구조 불일치는 거부됩니다. **하드 블록 타입이 아닙니다(dial 가능)** — 흔한 길이의 비숫자 앵커 없는 11자리 run이므로(측정 충돌률 ~0.37%이지만 `jp_mynumber` 원칙상 bare-digit 형태는 allowlist로 정리 가능하게 둡니다). |
|
|
307
|
+
| `nl_bsn` | 네덜란드 BSN | 9자리 + **"11-proef"** 가중 mod-11 | 11-proef를 통과하지 못하는 run은 거부됩니다. **하드 블록 타입이 아닙니다(dial 가능)** — 9 bare-digit은 매우 흔하고 11-proef는 무작위 run의 ~1/11이 통과하므로(측정 ~9.1%), 가장 명확한 dial 가능 사례입니다. |
|
|
308
|
+
| `api_key` | OpenAI 형식 / Stripe(`sk_`/`rk_`/`pk_`) | prefix + 24자 이상 | 언더스코어 형식 — Stripe `sk_live_`/`rk_live_`/`sk_test_`/`rk_test_`를 포함합니다. |
|
|
299
309
|
| `api_key` | AWS access key id | `AKIA`/`ASIA` + 정확히 16자 대문자-alnum | — |
|
|
300
310
|
| `api_key` | Google API key | `AIza` + 35자 URL-safe 문자 | — |
|
|
311
|
+
| `api_key` | SendGrid API key | `SG.` + 22자 URL-safe + `.` + 43자 URL-safe | 고정 길이 두 개의 점-구분 세그먼트가 anchor입니다. |
|
|
312
|
+
| `api_key` | Twilio Account/API SID | `AC`/`SK` + 정확히 32자 **hex** | hex 전용 본문이 무작위 base62를 거부합니다; bare 32-hex AUTH TOKEN은 할당식(`auth_token`)으로 포착합니다. |
|
|
313
|
+
| `secret` | OpenAI API key | `sk-`(및 `sk-proj-`) + 20자 이상 base62 유사 문자 | **하이픈** 형식으로 언더스코어 Stripe `sk_`와 구분되며, 두 prefix는 절대 겹치지 않습니다. |
|
|
314
|
+
| `secret` | Anthropic API key | `sk-ant-` + 16자 이상 | OpenAI `sk-` 규칙의 더 엄격한 형제 규칙입니다(attribution을 위해 먼저 실행). |
|
|
315
|
+
| `secret` | Google OAuth client secret | `GOCSPX-` + 정확히 28자 URL-safe 문자 | `AIza` API key와 구분됩니다. |
|
|
316
|
+
| `secret` | npm token | `npm_` + 정확히 36자 base62 문자 | — |
|
|
301
317
|
| `secret` | `Bearer <token>` | `Bearer` + 16자 이상 | — |
|
|
302
|
-
| `secret` | 할당식 `<key> = <value>` | 키 어휘: `api_key`, `api_secret`, `secret`, `secret_key`, `aws_secret_access_key`, `client_secret`, `private_key`, `access_token`, `refresh_token`, `token`, `password` | bare-base64 시크릿(
|
|
318
|
+
| `secret` | 할당식 `<key> = <value>` | 키 어휘: `api_key`, `api_secret`, `secret`, `secret_key`, `aws_secret_access_key`, `client_secret`, `private_key`, `access_token`, `refresh_token`, `auth_token`, `accountkey`, `token`, `password` | bare-base64 시크릿(AWS secret access key, **Azure Storage `AccountKey=`**, **Twilio auth token**)을 할당식 형태로 포착합니다 — 앵커 없는 88자 base64 Azure 규칙은 임의의 blob에 오탐하므로 `AccountKey=` 컨텍스트가 anchor입니다. |
|
|
303
319
|
| `secret` | GitHub token | `gh[pousr]_` + 36자 이상 base64 유사 문자 | pat/oauth/user/server/refresh 변형. |
|
|
304
320
|
| `secret` | Slack token | `xox[baprs]-` + 10자 이상 본문 | bot/user/refresh/legacy 변형. |
|
|
305
321
|
| `secret` | JWT | 점으로 구분된 3개 base64url 세그먼트, 첫 세그먼트가 `eyJ`(즉 `{"`의 base64)로 시작 | `eyJ` anchor가 임의의 점-구분 토큰을 거부합니다. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Configuration Reference
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.3.x)
|
|
4
4
|
|
|
5
5
|
`haechi init` writes `haechi.config.json`; a non-secret template is at `haechi.config.example.json`. Every command reads it with `--config <path>` (default `haechi.config.json`). Configuration is **validated fail-closed**: unknown providers, out-of-range numbers, and malformed values throw at load time rather than degrading silently. `haechi config` prints this reference; `haechi status` prints the *effective* state of a given config.
|
|
6
6
|
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
| Key | Type / values | Default | Notes |
|
|
40
40
|
|---|---|---|---|
|
|
41
|
-
| `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` | `llm-http` | Selects the protocol adapter. `llm-http` aliases `openai-compatible`. Unknown values **fail closed** at load. |
|
|
41
|
+
| `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` \| `anthropic` \| `gemini` | `llm-http` | Selects the protocol adapter. `llm-http` aliases `openai-compatible`. `anthropic` targets the Anthropic Messages API (`/v1/messages`, `/v1/messages/count_tokens`, `/v1/complete`); the client supplies Anthropic's `x-api-key`/`anthropic-version` headers and the proxy forwards them. `gemini` targets the Google Gemini API, whose endpoints are model-in-path with a `:method` suffix (`/v1beta/models/{model}:generateContent`, `:streamGenerateContent` (SSE), `:countTokens`, `:embedContent`, `:batchEmbedContents`; `/v1` or `/v1beta` prefix, arbitrary `{model}`); the client supplies Gemini's `x-goog-api-key` (or `?key=`) and the proxy forwards it. Unknown values **fail closed** at load. |
|
|
42
42
|
| `target.adapter` | same set | `openai-compatible` | Explicit adapter override; usually leave unset and let `type` decide. |
|
|
43
43
|
| `target.upstream` | URL string | `http://127.0.0.1:9999` | The only upstream the proxy forwards to. Request targets must be origin-form paths; absolute-URL targets are rejected (SSRF guard). |
|
|
44
44
|
|
|
@@ -103,8 +103,9 @@ The detect→decide core. See [Detection types & actions](#detection-types--acti
|
|
|
103
103
|
| Key | Type / values | Default | Notes |
|
|
104
104
|
|---|---|---|---|
|
|
105
105
|
| `filters.customRules` | array of rule objects | `[]` | Extra detection rules: `{ id, type, pattern, flags?, confidence? }`. Patterns are ReDoS-screened (≤500 chars, no nested quantifiers, no backreferences) and rejected at load if unsafe. |
|
|
106
|
-
| `filters.minConfidence` | number in `[0, 1]` | `0` | Precision dial. Each rule carries a `confidence` (0.6–0.95); a detection whose confidence is **below** this threshold is dropped before the policy decides. `0` (the default) gates nothing, preserving prior behavior. **Hard-block exemption:** a hard-block type (`secret`, `api_key`, `kr_rrn`, `card`) is **never** dropped on confidence — `minConfidence` trims only the precision-risky soft types (e.g. `phone`, `email`, `injection`), so a low-confidence credential/PII leak is still acted on (fail-closed). |
|
|
107
|
-
| `filters.allowlist` | array of strings and/or `{ value?, path? }` | `[]` | Operator false-positive exceptions. A detection whose matched **value** equals a string/`value` entry, or whose PII-safe JSON **path** (the hashed `pathText`, as shown in the audit) equals a `path` entry, is suppressed before the policy decides (when an entry sets both `value` and `path`, **both** must match). **Hard-block exemption:** an entry that would suppress a hard-block type (`secret`/`api_key`/`kr_rrn`/`card`) is **ignored** and the detection still fires — the allowlist can only clear a benign **soft-
|
|
106
|
+
| `filters.minConfidence` | number in `[0, 1]` | `0` | Precision dial. Each rule carries a `confidence` (0.6–0.95); a detection whose confidence is **below** this threshold is dropped before the policy decides. `0` (the default) gates nothing, preserving prior behavior. **Hard-block exemption:** a hard-block type (`secret`, `api_key`, `kr_rrn`, `card`, and the strong-anchored national IDs `fr_nir`, `es_dni`, `it_codice_fiscale`, `sg_nric`) is **never** dropped on confidence — `minConfidence` trims only the precision-risky soft/dial-eligible types (e.g. `phone`, `email`, `jp_mynumber`, `uk_nino`, `in_aadhaar`, `de_steuer_id`, `nl_bsn`, `injection`), so a low-confidence credential/PII leak is still acted on (fail-closed). |
|
|
107
|
+
| `filters.allowlist` | array of strings and/or `{ value?, path? }` | `[]` | Operator false-positive exceptions. A detection whose matched **value** equals a string/`value` entry, or whose PII-safe JSON **path** (the hashed `pathText`, as shown in the audit) equals a `path` entry, is suppressed before the policy decides (when an entry sets both `value` and `path`, **both** must match). **Hard-block exemption:** an entry that would suppress a hard-block type (`secret`/`api_key`/`kr_rrn`/`card`/`fr_nir`/`es_dni`/`it_codice_fiscale`/`sg_nric`) is **ignored** and the detection still fires — the allowlist can only clear a benign **soft / dial-eligible** FP (e.g. a `jp_mynumber`/`in_aadhaar` 12-digit-id FP, a `de_steuer_id` 11-digit-id FP, a `nl_bsn` 9-digit-id FP, or a format-only `uk_nino`), never silence a credential/strong-anchored-PII leak. Every suppression and every `minConfidence` drop is **audited** by count and type (`summary.suppressedByType` / `summary.droppedByType` / `suppressedCount` / `droppedCount`) — never the raw value. Use this to clear one benign FP without deleting a whole rule. |
|
|
108
|
+
| `filters.decodeAndRescan` | boolean | `false` | Opt-in base64/percent **decode-and-rescan** (the WS2d residual). With the default `false`, detection is byte-identical to before — a base64- or percent-encoded value is **not** decoded. When `true`, after the normal NFKC scan a string leaf that **looks** base64/base64url (anchored alphabet, valid length, `16…8192` bytes, round-trips to the same leaf, decodes to **valid UTF-8**) or contains a `%XX` escape (`decodeURIComponent`) is decoded and rescanned with the same rules + validators. A decoded hit has no offset in the encoded leaf, so it fails closed to a **whole-leaf** detection (`start:0,end:leaf.length`, value = the whole encoded leaf) — the transform redacts/blocks the entire leaf. **Precision guard:** a decoded hit fires **only** 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. No new runtime dependency (`node:buffer` Buffer + the `decodeURIComponent` builtin). Other encodings (gzip/hex/nested/custom-alphabet) stay out of scope. |
|
|
108
109
|
|
|
109
110
|
### Detection benchmark
|
|
110
111
|
|
|
@@ -147,7 +148,7 @@ npm run scan:detection # CI regression gate: fail if any type regresses below
|
|
|
147
148
|
|
|
148
149
|
| Key | Type / values | Default | Notes |
|
|
149
150
|
|---|---|---|---|
|
|
150
|
-
| `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `us-general` | `null` | Applies a regional baseline action set before enforcement. Profiles may **strengthen** but never weaken your explicit actions. Engineering defaults, not legal advice. |
|
|
151
|
+
| `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `asia-pdpa` \| `us-general` \| `jp-appi` | `null` | Applies a regional baseline action set before enforcement. Profiles may **strengthen** but never weaken your explicit actions. `eu-gdpr` blocks the EU national IDs (`fr_nir`/`es_dni`/`uk_nino`/`it_codice_fiscale`/`de_steuer_id`/`nl_bsn`); `asia-pdpa` (Singapore PDPA / India DPDP) blocks `sg_nric`/`in_aadhaar` (plus the other checksummed national IDs for mixed-region payloads); `jp-appi` blocks `jp_mynumber`; every profile blocks `jp_mynumber` (a checksummed national-ID leak). Engineering defaults, not legal advice. |
|
|
151
152
|
|
|
152
153
|
## `logging`
|
|
153
154
|
|
|
@@ -273,13 +274,13 @@ The rate limiter is an **injectable collaborator**, supplied programmatically th
|
|
|
273
274
|
const runtime = createRuntime(config, { rateLimiter });
|
|
274
275
|
```
|
|
275
276
|
|
|
276
|
-
An injected `rateLimiter` must implement `allow(key, limit)
|
|
277
|
+
An injected `rateLimiter` must implement `allow(key, limit)` returning either a `boolean` **or** a `Promise<boolean>` (where `key` is the per-identity bucket and `limit` is the resolved `requestsPerMinute`); `createRuntime` fails closed at construction if it does not. The proxy `await`s the result, so a synchronous boolean and an async shared-store limiter behave identically — the built-in default stays synchronous, while a Redis-backed limiter that resolves asynchronously gates correctly. The proxy consults `runtime.rateLimiter` for every rate-governed request.
|
|
277
278
|
|
|
278
|
-
The **default** is a per-process, in-memory fixed-window counter: it resets on restart and is **not shared across replicas**, so total throughput multiplies by the replica count behind a load balancer. Its window map is self-bounding (a lazy, amortized sweep evicts aged-out one-shot identities — no background timer). For a multi-replica deployment, enforce a per-identity limit at a shared front door **or** inject a shared-store implementation (e.g. Redis-backed) that satisfies the same `allow(key, limit)` contract. See [Shared responsibility §4](./shared-responsibility.md#4-horizontal-scale--multiple-replicas).
|
|
279
|
+
The **default** is a per-process, in-memory fixed-window counter: it resets on restart and is **not shared across replicas**, so total throughput multiplies by the replica count behind a load balancer. Its window map is self-bounding (a lazy, amortized sweep evicts aged-out one-shot identities — no background timer). For a multi-replica deployment, enforce a per-identity limit at a shared front door **or** inject a shared-store implementation (e.g. Redis-backed) that satisfies the same `allow(key, limit)` contract — the [`haechi-ratelimit-redis`](./shared-responsibility.md#4-horizontal-scale--multiple-replicas) satellite is the reference implementation. See [Shared responsibility §4](./shared-responsibility.md#4-horizontal-scale--multiple-replicas).
|
|
279
280
|
|
|
280
281
|
## Detection types & actions
|
|
281
282
|
|
|
282
|
-
Built-in detection `type` values: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `us_ssn`, `iban`, and `injection` (response-direction heuristic, report-only by default). Custom rules may introduce new types.
|
|
283
|
+
Built-in detection `type` values: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `us_ssn`, `iban`, `jp_mynumber`, `fr_nir`, `es_dni`, `uk_nino`, `it_codice_fiscale`, `sg_nric`, `in_aadhaar`, `de_steuer_id`, `nl_bsn`, and `injection` (response-direction heuristic, report-only by default). Custom rules may introduce new types.
|
|
283
284
|
|
|
284
285
|
### Supported credential & PII matrix
|
|
285
286
|
|
|
@@ -295,11 +296,26 @@ Detection is regex + optional validator (no ML). Every rule is **anchored tightl
|
|
|
295
296
|
| `card` | Payment card (PAN) | Luhn validator, 13–19 digits | — |
|
|
296
297
|
| `us_ssn` | US Social Security Number | `AAA-GG-SSSS` + SSA-range validator (rejects area `000`/`666`/`900-999`, group `00`, serial `0000`) | Separators required; a bare 9-digit id is not an SSN. |
|
|
297
298
|
| `iban` | International Bank Account Number | **mod-97 checksum** validator | The checksum is the precision guard — IBAN-shaped non-97-valid strings are rejected. |
|
|
298
|
-
| `
|
|
299
|
+
| `jp_mynumber` | Japan My Number (個人番号) | 12 digits + **mod-11 weighted check digit** | The check digit is the precision guard; a wrong-check 12-digit run is rejected. **Hard-block.** |
|
|
300
|
+
| `fr_nir` | France NIR / INSEE social-security | 15 chars + **`97 - (first13 mod 97)` control key** (Corsica `2A`→19, `2B`→18) | A wrong control key is rejected. **Hard-block.** |
|
|
301
|
+
| `es_dni` | Spain DNI / NIE | 8 digits (DNI) or `X/Y/Z`+7 digits (NIE) + **mod-23 check letter** (NIE `X/Y/Z`→`0/1/2`) | A wrong check letter is rejected. **Hard-block.** |
|
|
302
|
+
| `uk_nino` | UK National Insurance Number | `[A-CEGHJ-PR-TW-Z][A-CEGHJ-NPR-TW-Z]\d{6}[A-D]` + documented invalid-prefix exclusions (`BG`/`GB`/`NK`/`KN`/`TN`/`NT`/`ZZ`, `O`-as-2nd-letter) | **Format-only — no checksum exists**, so it is NOT a hard-block type (dial-eligible: an operator can allowlist a benign FP). |
|
|
303
|
+
| `it_codice_fiscale` | Italy codice fiscale | `[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]` + **mod-26 check character** (odd/even position tables over the first 15 chars) | A wrong check character is rejected. **Hard-block** — a rare 16-char mixed alpha+digit shape with a non-numeric structural anchor (measured ~3.8% collision over the shape). |
|
|
304
|
+
| `sg_nric` | Singapore NRIC / FIN | `[STFGM]\d{7}[A-Z]` + **weighted-sum check letter** (weights 2,7,6,5,4,3,2; per-prefix offset; per-series letter table) | A wrong check letter is rejected. **Hard-block** — two non-numeric anchors (prefix letter + check letter) over a rare shape (measured ~3.9% collision). |
|
|
305
|
+
| `in_aadhaar` | India Aadhaar | 12 digits (never starting `0`/`1`) + **Verhoeff checksum** | A wrong Verhoeff check digit is rejected. **NOT a hard-block type (dial-eligible)** — Verhoeff over the common 12-digit shape passes ~1/10 of random runs (measured ~9.9%, the `jp_mynumber` footgun), so an operator can allowlist a benign 12-digit-id FP. |
|
|
306
|
+
| `de_steuer_id` | Germany tax ID (Steuer-ID) | 11 digits + **ISO 7064 MOD 11,10** check digit + an "exactly one repeated digit in the first 10" structural test | A wrong check digit or wrong repeat structure is rejected. **NOT a hard-block type (dial-eligible)** — a bare 11-digit run with no non-numeric anchor over a common length (measured ~0.37% collision, but the bare-digit shape keeps it allowlist-clearable per the `jp_mynumber` discipline). |
|
|
307
|
+
| `nl_bsn` | Netherlands BSN | 9 digits + the **"11-proef"** weighted mod-11 | A run that fails the 11-proef is rejected. **NOT a hard-block type (dial-eligible)** — 9 bare digits is very common and the 11-proef passes ~1/11 of random runs (measured ~9.1%), the clearest dial-eligible case. |
|
|
308
|
+
| `api_key` | OpenAI-style / Stripe (`sk_`/`rk_`/`pk_`) | prefix + ≥24 chars | Underscore form — covers Stripe `sk_live_`/`rk_live_`/`sk_test_`/`rk_test_`. |
|
|
299
309
|
| `api_key` | AWS access key id | `AKIA`/`ASIA` + exactly 16 uppercase-alnum | — |
|
|
300
310
|
| `api_key` | Google API key | `AIza` + 35 URL-safe chars | — |
|
|
311
|
+
| `api_key` | SendGrid API key | `SG.` + 22 URL-safe + `.` + 43 URL-safe | The two fixed-length dotted segments are the anchor. |
|
|
312
|
+
| `api_key` | Twilio Account/API SID | `AC`/`SK` + exactly 32 **hex** | Hex-only body rejects random base62; the bare 32-hex AUTH TOKEN is caught via the assignment form (`auth_token`). |
|
|
313
|
+
| `secret` | OpenAI API key | `sk-` (and `sk-proj-`) + ≥20 base62-ish chars | **Hyphen** form, distinct from the underscore Stripe `sk_`; the two prefixes never overlap. |
|
|
314
|
+
| `secret` | Anthropic API key | `sk-ant-` + ≥16 chars | Stricter sibling of the OpenAI `sk-` rule (runs first for attribution). |
|
|
315
|
+
| `secret` | Google OAuth client secret | `GOCSPX-` + exactly 28 URL-safe chars | Distinct from the `AIza` API key. |
|
|
316
|
+
| `secret` | npm token | `npm_` + exactly 36 base62 chars | — |
|
|
301
317
|
| `secret` | `Bearer <token>` | `Bearer` + ≥16 chars | — |
|
|
302
|
-
| `secret` | Assignment `<key> = <value>` | key vocabulary: `api_key`, `api_secret`, `secret`, `secret_key`, `aws_secret_access_key`, `client_secret`, `private_key`, `access_token`, `refresh_token`, `token`, `password` | Catches bare-base64 secrets (
|
|
318
|
+
| `secret` | Assignment `<key> = <value>` | key vocabulary: `api_key`, `api_secret`, `secret`, `secret_key`, `aws_secret_access_key`, `client_secret`, `private_key`, `access_token`, `refresh_token`, `auth_token`, `accountkey`, `token`, `password` | Catches bare-base64 secrets (AWS secret access key, **Azure Storage `AccountKey=`**, **Twilio auth token**) via the assignment form — an un-anchored 88-char-base64 Azure rule would false-fire on any blob, so `AccountKey=` context is the anchor. |
|
|
303
319
|
| `secret` | GitHub token | `gh[pousr]_` + ≥36 base64-ish chars | pat/oauth/user/server/refresh variants. |
|
|
304
320
|
| `secret` | Slack token | `xox[baprs]-` + ≥10-char body | bot/user/refresh/legacy variants. |
|
|
305
321
|
| `secret` | JWT | three base64url segments, first starts `eyJ` (the base64 of `{"`) | The `eyJ` anchor rejects arbitrary dotted tokens. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi 운영 런북 (Day-2)
|
|
2
2
|
|
|
3
|
-
- 상태: Living document (코어 1.
|
|
3
|
+
- 상태: Living document (코어 1.3.x 추적)
|
|
4
4
|
|
|
5
5
|
Haechi를 프로덕션에서 운영하기 위한 실무 가이드입니다: 배포, 환경변수 오버레이를 통한 설정, health/readiness/metrics 모니터링, 우아한 종료, 백프레셔 튜닝, 그리고 해시 체인을 깨지 않는 audit 로그 회전입니다.
|
|
6
6
|
|
|
@@ -107,7 +107,40 @@ audit 로그는 **SHA-256 해시 체인**입니다(`audit.path`): 각 이벤트
|
|
|
107
107
|
|
|
108
108
|
아카이브 파이프라인에 검증 단계를 유지하지 않는 한, 나중에 재검증이 불가능한 방식으로 세그먼트를 압축/암호화하지 **마십시오**. 회전된 세그먼트는 여전히 검증될 때에만 증거로서 유용합니다.
|
|
109
109
|
|
|
110
|
-
## 7.
|
|
110
|
+
## 7. 프록시 처리량 벤치마크
|
|
111
|
+
|
|
112
|
+
`npm run bench:throughput`(`scripts/bench-throughput.mjs`)는 동시성 부하에서
|
|
113
|
+
프록시가 더하는 요청당 오버헤드를 측정합니다. 결정적인 로컬 **스텁**
|
|
114
|
+
OpenAI 호환 업스트림(즉시 응답하는 정해진 답변 — 실제 모델 없음)과 그 앞단의
|
|
115
|
+
**실제** Haechi 프록시를 세우고, 고정 크기 워커 풀의 동시 `fetch`로 부하를
|
|
116
|
+
구동하여 **req/s**와 **p50/p95/p99/max** 지연(정렬된 표본에 대한 nearest-rank
|
|
117
|
+
백분위수)을 보고합니다. 세 가지 시나리오를 실행합니다:
|
|
118
|
+
|
|
119
|
+
1. 고정 동시성에서의 **처리량 + 지연**(워밍업 배치는 보고 통계에서 제외합니다 —
|
|
120
|
+
JIT/연결 워밍업이 초기 요청을 왜곡하기 때문입니다),
|
|
121
|
+
2. **enforce 대 dry-run 오버헤드** — 동일한 부하를 두 모드로 실행하여 지연/처리량
|
|
122
|
+
**델타**를 보고하므로, 보호 비용이 추측이 아닌 측정된 수치가 됩니다,
|
|
123
|
+
3. **백프레셔** — 낮은 `limits.maxInFlight`를 버스트로 포화시켜 `503 + Retry-After`와
|
|
124
|
+
`200`의 비율을 보고합니다(실제 응답을 관찰하여 천장이 부하를 흘려보냄을 증명).
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm run bench:throughput
|
|
128
|
+
HAECHI_BENCH_REQUESTS=5000 HAECHI_BENCH_CONCURRENCY=64 npm run bench:throughput
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
노브(env, 매 실행 상단에 출력됨): `HAECHI_BENCH_REQUESTS`(총 요청 수, 기본 2000),
|
|
132
|
+
`HAECHI_BENCH_CONCURRENCY`(기본 32), `HAECHI_BENCH_WARMUP`(제외할 워밍업 수, 기본
|
|
133
|
+
100), `HAECHI_BENCH_PAYLOAD_KB`(기본 1), `HAECHI_BENCH_MAXINFLIGHT`(백프레셔
|
|
134
|
+
시나리오의 천장, 기본 4).
|
|
135
|
+
|
|
136
|
+
> **수치는 머신 상대적입니다.** 이것은 **루프백, 단일 프로세스, 스텁 업스트림
|
|
137
|
+
> 마이크로 벤치마크**입니다: 스텁, 프록시, 부하 생성기가 모두 `127.0.0.1`의 한
|
|
138
|
+
> Node 프로세스에서 실행되므로 실제 네트워크도 실제 모델도 없습니다. 수치는 오직
|
|
139
|
+
> Haechi가 더하는 오버헤드만 측정하며 머신·Node 버전·부하에 따라 달라집니다.
|
|
140
|
+
> 네트워크/하드웨어 처리량 벤치마크가 **아니며** 보장 수치로 인용해서는 **안
|
|
141
|
+
> 됩니다**. 이 벤치는 `release:preflight`에서 실행되지 않습니다.
|
|
142
|
+
|
|
143
|
+
## 8. 빠른 참조
|
|
111
144
|
|
|
112
145
|
| 작업 | 커맨드 |
|
|
113
146
|
|---|---|
|
|
@@ -115,6 +148,7 @@ audit 로그는 **SHA-256 해시 체인**입니다(`audit.path`): 각 이벤트
|
|
|
115
148
|
| Liveness | `curl localhost:11016/__haechi/live` |
|
|
116
149
|
| Readiness | `curl localhost:11016/__haechi/ready` |
|
|
117
150
|
| Metrics | `curl localhost:11016/__haechi/metrics` |
|
|
151
|
+
| 처리량 벤치 | `npm run bench:throughput` |
|
|
118
152
|
| 세그먼트 검증 | `haechi audit-verify --audit <seg>.jsonl --anchor <seg>.anchor.jsonl` |
|
|
119
153
|
| 우아한 정지 | `docker compose stop` (SIGTERM → 드레인) |
|
|
120
154
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Operations Runbook (Day-2)
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.3.x)
|
|
4
4
|
|
|
5
5
|
A practical guide to running Haechi in production: deploy, configure via the
|
|
6
6
|
env-var overlay, monitor with health/readiness/metrics, shut down gracefully,
|
|
@@ -189,7 +189,43 @@ rotation does not purge tokens.
|
|
|
189
189
|
re-verification unless you keep the verification step in your archival pipeline. A
|
|
190
190
|
rotated segment is only useful as evidence if it still verifies.
|
|
191
191
|
|
|
192
|
-
## 7.
|
|
192
|
+
## 7. Benchmarking proxy throughput
|
|
193
|
+
|
|
194
|
+
`npm run bench:throughput` (`scripts/bench-throughput.mjs`) measures the proxy's
|
|
195
|
+
added per-request overhead under concurrency. It stands up a deterministic local
|
|
196
|
+
**stub** OpenAI-compatible upstream (an instant canned reply — no real model) and
|
|
197
|
+
the **real** Haechi proxy in front of it, drives a configurable load with a
|
|
198
|
+
fixed-size worker pool of in-flight `fetch`es, and reports **req/s** plus
|
|
199
|
+
**p50/p95/p99/max** latency (percentiles by nearest-rank over a sorted sample). It
|
|
200
|
+
runs three scenarios:
|
|
201
|
+
|
|
202
|
+
1. **throughput + latency** at a fixed concurrency (a warmup batch is excluded
|
|
203
|
+
from the reported stats — JIT/connection warmup skews the first requests),
|
|
204
|
+
2. **enforce vs dry-run overhead** — the same load run in both modes, reporting
|
|
205
|
+
the latency/throughput **delta** so the cost of protection is a measured number,
|
|
206
|
+
3. **backpressure** — a low `limits.maxInFlight` saturated by a burst, reporting
|
|
207
|
+
how many requests got `503 + Retry-After` vs `200` (observed live, proving the
|
|
208
|
+
ceiling sheds load).
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm run bench:throughput
|
|
212
|
+
HAECHI_BENCH_REQUESTS=5000 HAECHI_BENCH_CONCURRENCY=64 npm run bench:throughput
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Knobs (env, printed at the top of every run): `HAECHI_BENCH_REQUESTS` (total,
|
|
216
|
+
default 2000), `HAECHI_BENCH_CONCURRENCY` (default 32), `HAECHI_BENCH_WARMUP`
|
|
217
|
+
(excluded warmup count, default 100), `HAECHI_BENCH_PAYLOAD_KB` (default 1),
|
|
218
|
+
`HAECHI_BENCH_MAXINFLIGHT` (the backpressure scenario's ceiling, default 4).
|
|
219
|
+
|
|
220
|
+
> **The numbers are machine-relative.** This is a **loopback, single-process,
|
|
221
|
+
> stub-upstream micro-benchmark**: the stub, the proxy, and the load generator all
|
|
222
|
+
> run in one Node process on `127.0.0.1`, so there is no real network and no real
|
|
223
|
+
> model. The numbers measure Haechi's added overhead only, and vary by machine,
|
|
224
|
+
> Node version, and load. They are **not** a network/hardware throughput benchmark
|
|
225
|
+
> and must **not** be quoted as guarantees. The bench is not run by
|
|
226
|
+
> `release:preflight`.
|
|
227
|
+
|
|
228
|
+
## 8. Quick reference
|
|
193
229
|
|
|
194
230
|
| Task | Command |
|
|
195
231
|
|---|---|
|
|
@@ -197,6 +233,7 @@ rotated segment is only useful as evidence if it still verifies.
|
|
|
197
233
|
| Liveness | `curl localhost:11016/__haechi/live` |
|
|
198
234
|
| Readiness | `curl localhost:11016/__haechi/ready` |
|
|
199
235
|
| Metrics | `curl localhost:11016/__haechi/metrics` |
|
|
236
|
+
| Throughput bench | `npm run bench:throughput` |
|
|
200
237
|
| Verify a segment | `haechi audit-verify --audit <seg>.jsonl --anchor <seg>.anchor.jsonl` |
|
|
201
238
|
| Graceful stop | `docker compose stop` (SIGTERM → drain) |
|
|
202
239
|
|