haechi 1.1.0 → 1.1.2

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.
@@ -1,9 +1,8 @@
1
1
  # Haechi 설정 레퍼런스
2
2
 
3
- - 문서 상태: Living document
4
- - 기준 버전: 0.6.0
3
+ - 문서 상태: Living document(core 1.1.x 추적)
5
4
 
6
- `haechi init`은 `haechi.config.json`을 생성하며, 비밀 정보를 포함하지 않는 템플릿은 `haechi.config.example.json`에 있다. 모든 커맨드는 `--config <path>`로 설정 파일을 읽는다(기본값: `haechi.config.json`). 설정은 **fail-closed 방식으로 검증**된다: 알 수 없는 provider, 범위를 벗어난 숫자, 잘못된 형식의 값은 자동으로 무시되지 않고 로드 시점에 오류를 발생시킨다. `haechi config`는 이 레퍼런스를 출력하며, `haechi status`는 특정 설정 파일의 *실제 적용* 상태를 출력한다.
5
+ `haechi init`은 `haechi.config.json`을 생성하며, 비밀 정보를 포함하지 않는 템플릿은 `haechi.config.example.json`에 있습니다. 모든 커맨드는 `--config <path>`로 설정 파일을 읽습니다(기본값: `haechi.config.json`). 설정은 **fail-closed 방식으로 검증**됩니다. 알 수 없는 provider, 범위를 벗어난 숫자, 잘못된 형식의 값은 자동으로 무시되지 않고 로드 시점에 오류를 발생시킵니다. `haechi config`는 이 레퍼런스를 출력하며, `haechi status`는 특정 설정 파일의 *실제 적용* 상태를 출력합니다.
7
6
 
8
7
  ## 전체 기본값
9
8
 
@@ -11,10 +10,10 @@
11
10
  {
12
11
  "mode": "dry-run",
13
12
  "target": { "type": "llm-http", "adapter": "openai-compatible", "upstream": "http://127.0.0.1:9999" },
14
- "proxy": { "host": "127.0.0.1", "port": 1016 },
13
+ "proxy": { "host": "127.0.0.1", "port": 11016 },
15
14
  "responseProtection": { "enabled": false, "mode": "enforce", "failureMode": "fail-closed", "allowNonJson": false, "allowCompressed": false, "maxBytes": 1048576 },
16
15
  "streaming": { "requestMode": "block" },
17
- "limits": { "maxRequestBytes": 1048576, "upstreamTimeoutMs": 120000 },
16
+ "limits": { "maxRequestBytes": 1048576, "upstreamTimeoutMs": 120000, "maxNestingDepth": 256 },
18
17
  "policy": { "mode": "dry-run", "presets": ["korean-pii", "secrets-only", "llm-redact"], "defaultAction": "redact", "actions": { "card": "block" } },
19
18
  "filters": { "customRules": [] },
20
19
  "keys": { "provider": "local", "keyFile": ".haechi/dev.keys.json" },
@@ -29,125 +28,126 @@
29
28
 
30
29
  | 키 | 타입 / 값 | 기본값 | 설명 |
31
30
  |---|---|---|---|
32
- | `mode` | `dry-run` \| `report-only` \| `enforce` | `dry-run` | 전역 집행 모드. `dry-run`/`report-only`는 탐지 audit만 수행하며, `enforce`는 변환/차단을 적용한다. `policy.mode`가 설정된 경우 해당 값이 우선한다. |
31
+ | `mode` | `dry-run` \| `report-only` \| `enforce` | `dry-run` | 전역 집행 모드입니다. `dry-run`/`report-only`는 탐지와 audit만 수행하며, `enforce`는 변환/차단을 적용합니다. `policy.mode`가 설정된 경우 해당 값이 우선합니다. |
33
32
 
34
33
  ## `target`
35
34
 
36
35
  | 키 | 타입 / 값 | 기본값 | 설명 |
37
36
  |---|---|---|---|
38
- | `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` | `llm-http` | 프로토콜 adapter를 선택한다. `llm-http`는 `openai-compatible`의 별칭이다. 알 수 없는 값은 로드 시 **fail-closed**로 처리된다. |
39
- | `target.adapter` | 동일한 값 집합 | `openai-compatible` | adapter를 명시적으로 지정한다. 보통은 설정하지 않고 `type`이 결정하도록 두면 된다. |
40
- | `target.upstream` | URL 문자열 | `http://127.0.0.1:9999` | proxy가 요청을 전달하는 유일한 upstream. 요청 대상은 origin-form 경로여야 하며, 절대 URL 대상은 거부된다(SSRF 방어). |
37
+ | `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` | `llm-http` | 프로토콜 adapter를 선택합니다. `llm-http`는 `openai-compatible`의 별칭입니다. 알 수 없는 값은 로드 시 **fail-closed**로 처리됩니다. |
38
+ | `target.adapter` | 동일한 값 집합 | `openai-compatible` | adapter를 명시적으로 지정합니다. 보통은 설정하지 않고 `type`이 결정하도록 두면 됩니다. |
39
+ | `target.upstream` | URL 문자열 | `http://127.0.0.1:9999` | proxy가 요청을 전달하는 유일한 upstream입니다. 요청 대상은 origin-form 경로여야 하며, 절대 URL 대상은 거부됩니다(SSRF 방어). |
41
40
 
42
41
  ## `proxy`
43
42
 
44
43
  | 키 | 타입 / 값 | 기본값 | 설명 |
45
44
  |---|---|---|---|
46
- | `proxy.host` | 비어 있지 않은 문자열 | `127.0.0.1` | 바인드 주소. loopback이 아닌 host를 사용하려면 `--allow-remote-bind` CLI 플래그가 필요하다 설정 파일만으로는 시작되지 않는다([loopback 밖으로 바인딩](#binding-beyond-loopback) 참고). |
47
- | `proxy.port` | 정수 0–65535 | `1016` | 리슨 포트(`0` = 임시 포트). `--port`로 실행 시마다 덮어쓸 수 있다. |
45
+ | `proxy.host` | 비어 있지 않은 문자열 | `127.0.0.1` | 바인드 주소입니다. loopback이 아닌 host를 사용하려면 `--allow-remote-bind` CLI 플래그가 필요합니다. 설정 파일만으로는 시작되지 않습니다([loopback 밖으로 바인딩](#binding-beyond-loopback) 참고). |
46
+ | `proxy.port` | 정수 0–65535 | `11016` | 리슨 포트입니다(`0` = 임시 포트). `--port`로 실행할 때마다 덮어쓸 수 있습니다. |
48
47
 
49
48
  ## `responseProtection`
50
49
 
51
- upstream JSON 응답을 검사한다(기본적으로 꺼져 있음 — 모델로부터 *돌아오는* 내용을 보호하려면 활성화한다).
50
+ upstream JSON 응답을 검사합니다(기본적으로 꺼져 있습니다 — 모델로부터 *돌아오는* 내용을 보호하려면 활성화하세요).
52
51
 
53
52
  | 키 | 타입 / 값 | 기본값 | 설명 |
54
53
  |---|---|---|---|
55
- | `responseProtection.enabled` | boolean | `false` | 마스터 스위치. `detokenizeResponses`가 작동하려면 반드시 활성화되어 있어야 한다. |
56
- | `responseProtection.mode` | `dry-run` \| `report-only` \| `enforce` | `enforce` | 응답 방향의 집행 모드. **실제 LLM upstream `report-only` 권장:** envelope 메타데이터(id, unix 타임스탬프 `created`, 긴 숫자 필드)가 PII/secret 모양으로 보일 수 있어 `enforce`면 정상 완성 응답을 502로 막는다. `report-only`도 탐지·감사·`detokenizeResponses`는 그대로 동작. (Haechi는 응답에서 자체 `[TOKEN:…]`/`[HAECHI_ENC:…]` 마커를 제외하고, phone 규칙도 맨 타임스탬프를 무시하며, 응답의 bare JSON number leaf는 검사하지 않으므로 실제 vLLM/Ollama 응답은 clean. 응답 *텍스트*까지 검사하려면 `enforce`가 더 엄격.) |
57
- | `responseProtection.failureMode` | `fail-closed` \| `allow` | `fail-closed` | *검사 불가능한* 응답(비JSON, 잘못된 JSON, 압축)에 대한 처리 방식. `fail-closed`는 502를 반환하고, `allow`는 통과시킨다(audit 기록됨). |
58
- | `responseProtection.allowNonJson` | boolean | `false` | 비JSON 응답을 검사 없이 통과시킨다. |
59
- | `responseProtection.allowCompressed` | boolean | `false` | 압축 응답을 검사 없이 통과시킨다. |
60
- | `responseProtection.maxBytes` | 양의 정수 | `1048576` | 응답 크기의 상한. `failureMode: allow` 상태에서도 적용되며, 크기를 초과한 응답은 항상 거부된다. |
61
- | `responseProtection.scanNumbers` | boolean | `false` | 응답의 **bare JSON number leaf**에 탐지를 돌릴지 여부. 기본 off — 응답 숫자는 추론서버 메타데이터(`*_duration`, count, timestamp)라 검사하면 `card`/`kr_rrn` 오탐만 발생. 모델이 숫자 필드로 유출할 수 있다고 보는 엄격 위협모델에서만 `true`; `mode: report-only`와 함께 써서 차단 없이 감사만. 요청 방향은 항상 숫자 검사. |
54
+ | `responseProtection.enabled` | boolean | `false` | 마스터 스위치입니다. `detokenizeResponses`가 작동하려면 반드시 활성화되어 있어야 합니다. |
55
+ | `responseProtection.mode` | `dry-run` \| `report-only` \| `enforce` | `enforce` | 응답 방향의 집행 모드입니다. **실제 LLM upstream에는 `report-only`를 권장합니다.** envelope 메타데이터(id, unix 타임스탬프 `created`, 긴 숫자 필드)가 PII/secret 모양으로 보일 수 있어, `enforce`이면 정상 완성 응답을 502로 막습니다. `report-only`에서도 탐지·감사·`detokenizeResponses`는 그대로 동작합니다. (Haechi는 응답에서 자체 `[TOKEN:…]`/`[HAECHI_ENC:…]` 마커를 제외하고, phone 규칙도 맨 타임스탬프를 무시하며, 응답의 bare JSON number leaf는 검사하지 않으므로 실제 vLLM/Ollama 응답은 clean합니다. 응답 *텍스트*까지 검사하려면 `enforce`가 더 엄격합니다.) |
56
+ | `responseProtection.failureMode` | `fail-closed` \| `allow` | `fail-closed` | *검사 불가능한* 응답(비JSON, 잘못된 JSON, 압축)에 대한 처리 방식입니다. `fail-closed`는 502를 반환하고, `allow`는 통과시킵니다(audit 기록됩니다). |
57
+ | `responseProtection.allowNonJson` | boolean | `false` | 비JSON 응답을 검사 없이 통과시킵니다. |
58
+ | `responseProtection.allowCompressed` | boolean | `false` | 압축 응답을 검사 없이 통과시킵니다. |
59
+ | `responseProtection.maxBytes` | 양의 정수 | `1048576` | 응답 크기의 상한입니다. `failureMode: allow` 상태에서도 적용되며, 크기를 초과한 응답은 항상 거부됩니다. |
60
+ | `responseProtection.scanNumbers` | boolean | `false` | 응답의 **bare JSON number leaf**에 탐지를 돌릴지 여부입니다. 기본은 off입니다 — 응답 숫자는 추론서버 메타데이터(`*_duration`, count, timestamp)라 검사하면 `card`/`kr_rrn` 오탐만 발생합니다. 모델이 숫자 필드로 유출할 수 있다고 보는 엄격 위협모델에서만 `true`로 두며, `mode: report-only`와 함께 써서 차단 없이 감사만 하세요. 요청 방향은 항상 숫자를 검사합니다. |
62
61
 
63
62
  ## `streaming`
64
63
 
65
64
  | 키 | 타입 / 값 | 기본값 | 설명 |
66
65
  |---|---|---|---|
67
- | `streaming.requestMode` | `block` \| `pass-through` \| `inspect` | `block` | `block`은 스트리밍 요청에 `501`을 반환한다; `inspect`는 bounded cross-frame 버퍼로 SSE/NDJSON 응답을 stream-filter한다; `pass-through`는 검사 없이 전달한다(감사됨). Ollama의 `/api/chat`과 `/api/generate`는 `stream: false`가 명시되지 않으면 streaming으로 간주된다. |
68
- | `streaming.responseMode` | `dry-run` \| `report-only` \| `enforce` | `enforce` | 검사된 스트림에 적용되는 집행 모드(요청 방향과 독립적). |
69
- | `streaming.maxMatchBytes` | 양의 정수 | `256` | inspect 시 cross-frame 매칭 윈도우. 이 크기의 tail을 보유하여 프레임에 걸친 탐지를 방출 전에 포착할 수 있다; 이 값보다 긴 단일 매칭은 프레임에 걸쳐 분할될 수 있다. |
66
+ | `streaming.requestMode` | `block` \| `pass-through` \| `inspect` | `block` | `block`은 스트리밍 요청에 `501`을 반환합니다. `inspect`는 bounded cross-frame 버퍼로 SSE/NDJSON 응답을 stream-filter합니다. `pass-through`는 검사 없이 전달합니다(감사됩니다). Ollama의 `/api/chat`과 `/api/generate`는 `stream: false`가 명시되지 않으면 streaming으로 간주됩니다. |
67
+ | `streaming.responseMode` | `dry-run` \| `report-only` \| `enforce` | `enforce` | 검사된 스트림에 적용되는 집행 모드입니다(요청 방향과 독립적입니다). |
68
+ | `streaming.maxMatchBytes` | 양의 정수 | `256` | inspect 시 cross-frame 매칭 윈도우입니다. 이 크기의 tail을 보유하여 프레임에 걸친 탐지를 방출 전에 포착할 수 있습니다. 이 값보다 긴 단일 매칭은 프레임에 걸쳐 분할될 수 있습니다. |
70
69
 
71
70
  ## `limits`
72
71
 
73
72
  | 키 | 타입 / 값 | 기본값 | 설명 |
74
73
  |---|---|---|---|
75
- | `limits.maxRequestBytes` | 양의 정수 | `1048576` | 요청 바디 크기 상한. 초과 시 `413`을 반환한다. 바디를 전부 버퍼링하지 않고 증분 방식으로 적용된다. |
76
- | `limits.upstreamTimeoutMs` | 양의 정수 | `120000` | upstream 요청 타임아웃. 만료 시 `504 haechi_upstream_timeout`을 반환한다. 연결 실패 시에는 `502 haechi_upstream_unreachable`을 반환한다. |
74
+ | `limits.maxRequestBytes` | 양의 정수 | `1048576` | 요청 바디 크기 상한입니다. 초과 시 `413`을 반환합니다. 바디를 전부 버퍼링하지 않고 증분 방식으로 적용됩니다. |
75
+ | `limits.upstreamTimeoutMs` | 양의 정수 | `120000` | upstream 요청 타임아웃입니다. 만료 시 `504 haechi_upstream_timeout`을 반환합니다. 연결 실패 시에는 `502 haechi_upstream_unreachable`을 반환합니다. |
76
+ | `limits.maxNestingDepth` | 양의 정수 | `256` | 탐지 시 walk하는 JSON 최대 중첩 깊이입니다. 이보다 깊게 중첩된 바디는 `413 haechi_request_too_deeply_nested`로 거부되어(upstream 이전, fail-closed), 재귀적 payload walk를 스택 오버플로로부터 보호합니다. 컨테이너 하강을 제한하며, 한도에 있는 leaf는 여전히 검사됩니다. (별도로, 비UTF-8 요청 바디는 fail-closed로 거부됩니다: `400 haechi_request_body_not_utf8`.) |
77
77
 
78
78
  ## `policy`
79
79
 
80
- 탐지→결정의 핵심. [Detection type과 action](#detection-types--actions) 참고.
80
+ 탐지→결정의 핵심입니다. [Detection type과 action](#detection-types--actions) 참고하세요.
81
81
 
82
82
  | 키 | 타입 / 값 | 기본값 | 설명 |
83
83
  |---|---|---|---|
84
- | `policy.mode` | `dry-run` \| `report-only` \| `enforce` | `dry-run` | 실제 적용되는 집행 모드(`policy.mode ?? mode`). |
85
- | `policy.presets` | preset 이름 배열 | `["korean-pii", "secrets-only", "llm-redact"]` | 순서대로 병합되는 내장 action 집합. [Presets](#presets) 참고. |
86
- | `policy.defaultAction` | action | `redact` | 명시적 매핑이 없는 탐지 type에 적용되는 action. |
87
- | `policy.actions` | `{ <type>: <action> }` | `{ "card": "block" }` | type별 개별 재정의. 병합 시 **강화**는 가능하지만 약화는 불가([Action strength](#action-strength) 참고). `injection`은 설정하지 않으면 기본적으로 `allow`이다. |
88
- | `policy.allowUnsafeOverrides` | boolean | `false` | 더 약한 action이 더 강한 action을 덮어쓰는 것을 허용한다. 기본적으로 꺼져 있으며, 활성화하면 안전 장치가 제거된다. |
89
- | `policy.bundlePath` | 경로 | 미설정 | 인라인 정책 대신 서명된 policy bundle을 로드한다(`keys.keyFile`에 대해 검증됨). |
84
+ | `policy.mode` | `dry-run` \| `report-only` \| `enforce` | `dry-run` | 실제 적용되는 집행 모드입니다(`policy.mode ?? mode`). |
85
+ | `policy.presets` | preset 이름 배열 | `["korean-pii", "secrets-only", "llm-redact"]` | 순서대로 병합되는 내장 action 집합입니다. [Presets](#presets) 참고하세요. |
86
+ | `policy.defaultAction` | action | `redact` | 명시적 매핑이 없는 탐지 type에 적용되는 action입니다. |
87
+ | `policy.actions` | `{ <type>: <action> }` | `{ "card": "block" }` | type별 개별 재정의입니다. 병합 시 **강화**는 가능하지만 약화는 불가합니다([Action strength](#action-strength) 참고). `injection`은 설정하지 않으면 기본적으로 `allow`입니다. |
88
+ | `policy.allowUnsafeOverrides` | boolean | `false` | 더 약한 action이 더 강한 action을 덮어쓰는 것을 허용합니다. 기본적으로 꺼져 있으며, 활성화하면 안전 장치가 제거됩니다. |
89
+ | `policy.bundlePath` | 경로 | 미설정 | 인라인 정책 대신 서명된 policy bundle을 로드합니다(`keys.keyFile`에 대해 검증됩니다). |
90
90
 
91
91
  ## `filters`
92
92
 
93
93
  | 키 | 타입 / 값 | 기본값 | 설명 |
94
94
  |---|---|---|---|
95
- | `filters.customRules` | 규칙 객체 배열 | `[]` | 추가 탐지 규칙: `{ id, type, pattern, flags?, confidence? }`. 패턴은 ReDoS 검사를 통과해야 하며(≤500자, 중첩 한정자 없음, 역참조 없음), 안전하지 않으면 로드 시 거부된다. |
95
+ | `filters.customRules` | 규칙 객체 배열 | `[]` | 추가 탐지 규칙입니다: `{ id, type, pattern, flags?, confidence? }`. 패턴은 ReDoS 검사를 통과해야 하며(≤500자, 중첩 한정자 없음, 역참조 없음), 안전하지 않으면 로드 시 거부됩니다. |
96
96
 
97
97
  ## `keys`
98
98
 
99
99
  | 키 | 타입 / 값 | 기본값 | 설명 |
100
100
  |---|---|---|---|
101
- | `keys.provider` | `local` \| `external` | `local` | `local`은 소프트웨어 AES-256-GCM 키 파일을 사용한다(개발 전용). `external`은 키 자료를 포함하지 않으며, `createRuntime(config, { cryptoProvider })`를 통해 crypto provider를 주입해야 한다. |
102
- | `keys.keyFile` | 경로 | `.haechi/dev.keys.json` | 로컬 키 파일(모드 `0600`). `haechi init --force`는 키를 교체하며, 기존 키는 `kid`로 기존 암호문/token이 복호화 가능하도록 퇴역 상태로 보관된다. |
101
+ | `keys.provider` | `local` \| `external` | `local` | `local`은 소프트웨어 AES-256-GCM 키 파일을 사용합니다(개발 전용). `external`은 키 자료를 포함하지 않으며, `createRuntime(config, { cryptoProvider })`를 통해 crypto provider를 주입해야 합니다. |
102
+ | `keys.keyFile` | 경로 | `.haechi/dev.keys.json` | 로컬 키 파일입니다(모드 `0600`). `haechi init --force`는 키를 교체하며, 기존 키는 `kid`로 기존 암호문/token이 복호화 가능하도록 퇴역 상태로 보관됩니다. |
103
103
 
104
104
  ## `audit`
105
105
 
106
106
  | 키 | 타입 / 값 | 기본값 | 설명 |
107
107
  |---|---|---|---|
108
- | `audit.sink` | `jsonl` | `jsonl` | `jsonl`만 지원된다. |
109
- | `audit.path` | 경로 | `.haechi/audit.jsonl` | SHA-256 hash chain 로그. `haechi audit-verify`로 검증한다. 평문/PII를 포함하지 않는다. |
108
+ | `audit.sink` | `jsonl` | `jsonl` | `jsonl`만 지원됩니다. |
109
+ | `audit.path` | 경로 | `.haechi/audit.jsonl` | SHA-256 hash chain 로그입니다. `haechi audit-verify`로 검증합니다. 평문/PII를 포함하지 않습니다. |
110
110
 
111
111
  ## `tokenVault`
112
112
 
113
113
  | 키 | 타입 / 값 | 기본값 | 설명 |
114
114
  |---|---|---|---|
115
- | `tokenVault.provider` | `local` | `local` | `local`만 지원된다. |
116
- | `tokenVault.path` | 경로 | `.haechi/token-vault.json` | 암호화된 token 저장소(원자적 쓰기, 파일 락). |
117
- | `tokenVault.revealPolicy` | `disabled` \| `local-dev` | `disabled` | **수동** reveal(`token-reveal`)을 허용할지 결정한다. 모든 reveal/purge 결정은 audit 기록된다. detokenization과는 독립적이다. |
118
- | `tokenVault.retentionDays` | 양의 수 | `30` | Token TTL. 만료된 token은 vault 쓰기 시 또는 `token-purge --expired`로 삭제된다. |
119
- | `tokenVault.deterministic` | boolean | `false` | 동일한 `(type, value)` → 동일한 token(도메인 분리된 파생 키로 HMAC). 멀티턴에 필요하다. **트레이드오프:** 동일한 값이 연결 가능해진다. |
120
- | `tokenVault.deterministicTypes` | `null` \| 비어 있지 않은 문자열 배열 | `null` | `null`이면 deterministic 활성화 시 모든 type에 적용. 그렇지 않으면 열거된 type에만 determinism을 제한한다(예: `["email"]`). |
121
- | `tokenVault.detokenizeResponses` | boolean | `false` | 해당 요청을 처리하며 발급한 token을 응답에서 복원한다. 동일 요청을 보호하며 발급된 token만 복원되며, `responseProtection.enabled`가 필요하다. 개수 단위로 audit 기록된다. |
115
+ | `tokenVault.provider` | `local` | `local` | `local`만 지원됩니다. |
116
+ | `tokenVault.path` | 경로 | `.haechi/token-vault.json` | 암호화된 token 저장소입니다(원자적 쓰기, 파일 락). |
117
+ | `tokenVault.revealPolicy` | `disabled` \| `local-dev` | `disabled` | **수동** reveal(`token-reveal`)을 허용할지 결정합니다. 모든 reveal/purge 결정은 audit 기록됩니다. detokenization과는 독립적입니다. |
118
+ | `tokenVault.retentionDays` | 양의 수 | `30` | Token TTL입니다. 만료된 token은 vault 쓰기 시 또는 `token-purge --expired`로 삭제됩니다. |
119
+ | `tokenVault.deterministic` | boolean | `false` | 동일한 `(type, value)` → 동일한 token입니다(도메인 분리된 파생 키로 HMAC합니다). 멀티턴에 필요합니다. **트레이드오프:** 동일한 값이 연결 가능해집니다. |
120
+ | `tokenVault.deterministicTypes` | `null` \| 비어 있지 않은 문자열 배열 | `null` | `null`이면 deterministic 활성화 시 모든 type에 적용됩니다. 그렇지 않으면 열거된 type에만 determinism을 제한합니다(예: `["email"]`). |
121
+ | `tokenVault.detokenizeResponses` | boolean | `false` | 해당 요청을 처리하며 발급한 token을 응답에서 복원합니다. 동일 요청을 보호하며 발급된 token만 복원되며, `responseProtection.enabled`가 필요합니다. 개수 단위로 audit 기록됩니다. |
122
122
 
123
123
  ## `privacy`
124
124
 
125
125
  | 키 | 타입 / 값 | 기본값 | 설명 |
126
126
  |---|---|---|---|
127
- | `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `us-general` | `null` | 집행 전에 지역별 기준 action 집합을 적용한다. 프로필은 명시적 action을 **강화**할 수는 있지만 약화할 수는 없다. 엔지니어링 기본값이며, 법적 자문이 아니다. |
127
+ | `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `us-general` | `null` | 집행 전에 지역별 기준 action 집합을 적용합니다. 프로필은 명시적 action을 **강화**할 수는 있지만 약화할 수는 없습니다. 엔지니어링 기본값이며, 법적 자문이 아닙니다. |
128
128
 
129
129
  ## `mcp`
130
130
 
131
- `mcp-stdio`와 `mcp-wrap`에 적용된다.
131
+ `mcp-stdio`와 `mcp-wrap`에 적용됩니다.
132
132
 
133
133
  | 키 | 타입 / 값 | 기본값 | 설명 |
134
134
  |---|---|---|---|
135
- | `mcp.allowedMethods` | 비어 있지 않은 문자열 배열 | `["initialize", "tools/call", "resources/read", "prompts/get"]` | 클라이언트가 호출할 수 있는 method allowlist(`"*"`는 전체 허용). 서버가 먼저 시작하는 요청은 allowlist를 우회하지만 params는 여전히 보호된다. |
136
- | `mcp.protectParams` | boolean | `true` | 요청 `params`를 보호한다. |
137
- | `mcp.protectResults` | boolean | `true` | 응답 `result`를 보호한다(injection 휴리스틱도 실행). |
138
- | `mcp.requireJsonRpc` | boolean | `true` | `jsonrpc: "2.0"`을 요구하며, 규격에 맞지 않는 메시지는 거부된다. |
135
+ | `mcp.allowedMethods` | 비어 있지 않은 문자열 배열 | `["initialize", "tools/call", "resources/read", "prompts/get"]` | 클라이언트가 호출할 수 있는 method allowlist입니다(`"*"`는 전체 허용). 서버가 먼저 시작하는 요청은 allowlist를 우회하지만 params는 여전히 보호됩니다. |
136
+ | `mcp.protectParams` | boolean | `true` | 요청 `params`를 보호합니다. |
137
+ | `mcp.protectResults` | boolean | `true` | 응답 `result`를 보호합니다(injection 휴리스틱도 실행합니다). |
138
+ | `mcp.requireJsonRpc` | boolean | `true` | `jsonrpc: "2.0"`을 요구하며, 규격에 맞지 않는 메시지는 거부됩니다. |
139
139
 
140
140
  ## `auth`
141
141
 
142
142
  | 키 | 타입 / 값 | 기본값 | 설명 |
143
143
  |---|---|---|---|
144
- | `auth.provider` | `none` \| `bearer` \| `external` \| `plugin` | `none` | `none` = 인증 없음(identity null). `bearer` = 내장 token auth. `external`은 `createRuntime(config, { authProvider })`를 통해 `authProvider`를 주입해야 한다. `plugin` = 서명된 `authProvider` 샌드박스([`auth.plugin`](#authplugin-signed-authprovider-sandbox) 참고). |
145
- | `auth.store` | 경로 | `.haechi/auth.json` | Bearer token 저장소(모드 `0600`). Token은 keyed-HMAC 해시로만 보관되며, 평문은 `haechi auth add` 실행 시 한 번만 표시된다. |
146
- | `auth.allowedLabelKeys` | 문자열 배열 | `["team", "env", "tier", "role"]` | Token이 가질 수 있는 label 키; 값은 길이가 제한되며 PII를 포함하면 안 된다. |
144
+ | `auth.provider` | `none` \| `bearer` \| `external` \| `plugin` | `none` | `none` = 인증 없음(identity null). `bearer` = 내장 token auth. `external`은 `createRuntime(config, { authProvider })`를 통해 `authProvider`를 주입해야 합니다. `plugin` = 서명된 `authProvider` 샌드박스([`auth.plugin`](#authplugin-signed-authprovider-sandbox) 참고). |
145
+ | `auth.store` | 경로 | `.haechi/auth.json` | Bearer token 저장소입니다(모드 `0600`). Token은 keyed-HMAC 해시로만 보관되며, 평문은 `haechi auth add` 실행 시 한 번만 표시됩니다. |
146
+ | `auth.allowedLabelKeys` | 문자열 배열 | `["team", "env", "tier", "role"]` | Token이 가질 수 있는 label 키입니다. 값은 길이가 제한되며 PII를 포함하면 안 됩니다. |
147
147
 
148
148
  ### `auth.plugin` (signed authProvider sandbox)
149
149
 
150
- `auth.provider: "plugin"`일 때 필요. 샌드박스는 **서명된** `authProvider` 플러그인을 capability-gated, 감사되는 런타임에서 로드한다. 최상위 `plugins.enabled`(기본 `true`)는 kill-switch — `false`면 어떤 플러그인 생성도 거부한다. 동적 로딩은 opt-in이며 기본은 dependency injection. `docs/current/release-1.0-implementation-scope.md`(worker) 및 `release-1.1-implementation-scope.md`(process) 참고.
150
+ `auth.provider: "plugin"`일 때 필요합니다. 샌드박스는 **서명된** `authProvider` 플러그인을 capability-gated, 감사되는 런타임에서 로드합니다. 최상위 `plugins.enabled`(기본 `true`)는 kill-switch입니다 — `false`이면 어떤 플러그인 생성도 거부합니다. 동적 로딩은 opt-in이며 기본은 dependency injection입니다. `docs/current/release-1.0-implementation-scope.md`(worker) 및 `release-1.1-implementation-scope.md`(process) 참고하세요.
151
151
 
152
152
  | Key | Type / values | Default | Notes |
153
153
  |---|---|---|---|
@@ -166,37 +166,37 @@ upstream JSON 응답을 검사한다(기본적으로 꺼져 있음 — 모델로
166
166
 
167
167
  ## `policy` profiles & limits
168
168
 
169
- 기본 `policy` 위에 클라이언트별 통제를 레이어로 추가한다. [Named profiles](#named-profiles) 참고.
169
+ 기본 `policy` 위에 클라이언트별 통제를 레이어로 추가합니다. [Named profiles](#named-profiles) 참고하세요.
170
170
 
171
171
  | 키 | 타입 / 값 | 기본값 | 설명 |
172
172
  |---|---|---|---|
173
- | `policy.profiles` | `{ <name>: { presets?, actions?, modelAllowlist?, rate? } }` | `{}` | Named profile; 각각 기본 policy를 재정의한다. |
174
- | `policy.profileBinding` | `{ byScope?, byLabel?, default }` | 미설정 | identity scope/label(`"k=v"` 형태)을 profile 이름으로 매핑한다. `profiles`가 설정된 경우 `default`는 **필수**이며 가장 엄격한 profile이어야 한다(fail-closed). |
175
- | `policy.modelAllowlist` | 문자열 배열 | 미설정 | 허용된 `model` (기본 레벨; profile별로도 설정 가능). 허용되지 않은 모델 → `403`. 비어 있거나 없으면 모두 허용. |
176
- | `policy.rate` | `{ requestsPerMinute }` | 미설정 | identity별 요청 rate limit(기본 레벨 또는 profile별). 초과 시 → `429`. 인메모리, 프로세스별. |
173
+ | `policy.profiles` | `{ <name>: { presets?, actions?, modelAllowlist?, rate? } }` | `{}` | Named profile입니다. 각각 기본 policy를 재정의합니다. |
174
+ | `policy.profileBinding` | `{ byScope?, byLabel?, default }` | 미설정 | identity scope/label(`"k=v"` 형태)을 profile 이름으로 매핑합니다. `profiles`가 설정된 경우 `default`는 **필수**이며 가장 엄격한 profile이어야 합니다(fail-closed). |
175
+ | `policy.modelAllowlist` | 문자열 배열 | 미설정 | 허용된 `model` 값입니다(기본 레벨; profile별로도 설정 가능). 허용되지 않은 모델 → `403`. 비어 있거나 없으면 모두 허용합니다. |
176
+ | `policy.rate` | `{ requestsPerMinute }` | 미설정 | identity별 요청 rate limit입니다(기본 레벨 또는 profile별). 초과 시 → `429`. 인메모리, 프로세스별입니다. |
177
177
 
178
178
  ### Named profiles
179
179
 
180
- identity가 인증되면 **scope → label → `default`** 순으로 profile이 resolve된다; scope가 label보다 우선하며 첫 번째 매칭이 적용된다. `profiles`가 없거나 `auth.provider: none`인 경우 기본 policy가 적용된다. Resolve된 profile의 policy 엔진, `modelAllowlist`, `rate`가 해당 요청을 처리한다.
180
+ identity가 인증되면 **scope → label → `default`** 순으로 profile이 resolve됩니다. scope가 label보다 우선하며 첫 번째 매칭이 적용됩니다. `profiles`가 없거나 `auth.provider: none`인 경우 기본 policy가 적용됩니다. Resolve된 profile의 policy 엔진, `modelAllowlist`, `rate`가 해당 요청을 처리합니다.
181
181
 
182
182
  ## Detection type과 action
183
183
 
184
- 내장 탐지 `type` 값: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `injection`(응답 방향 휴리스틱, 기본 report-only). 커스텀 규칙으로 새로운 type을 추가할 수 있다.
184
+ 내장 탐지 `type` 값은 다음과 같습니다: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `injection`(응답 방향 휴리스틱, 기본 report-only). 커스텀 규칙으로 새로운 type을 추가할 수 있습니다.
185
185
 
186
186
  Action(약한 것 → 강한 것 순):
187
187
 
188
188
  | Action | 효과 |
189
189
  |---|---|
190
- | `allow` | 변경 없음(탐지 audit은 기록됨). |
191
- | `redact` | `[REDACTED:<type>]`으로 교체한다. |
192
- | `mask` | 부분 마스킹한다(값이 8자 이하이면 전체 마스킹). |
193
- | `tokenize` | vault token으로 교체한다. token vault를 통해 복원 가능하다. |
194
- | `encrypt` | 인라인 AES-256-GCM 봉투로 교체한다. |
195
- | `block` | 전체 payload를 거부한다(`403`/`-32001`/exit 3). |
190
+ | `allow` | 변경 없음(탐지와 audit은 기록됩니다). |
191
+ | `redact` | `[REDACTED:<type>]`으로 교체합니다. |
192
+ | `mask` | 부분 마스킹합니다(값이 8자 이하이면 전체 마스킹). |
193
+ | `tokenize` | vault token으로 교체합니다. token vault를 통해 복원 가능합니다. |
194
+ | `encrypt` | 인라인 AES-256-GCM 봉투로 교체합니다. |
195
+ | `block` | 전체 payload를 거부합니다(`403`/`-32001`/exit 3). |
196
196
 
197
197
  ### Action strength
198
198
 
199
- preset과 override(또는 privacy profile)가 충돌할 경우 **강한** action이 우선하며, `policy.allowUnsafeOverrides`가 `true`가 아니면 더 강한 action을 약화하려 할 경우 오류가 발생한다. 강도 순: `allow`(0) < `redact`/`mask`(1) < `tokenize`/`encrypt`(2) < `block`(3).
199
+ preset과 override(또는 privacy profile)가 충돌할 경우 **강한** action이 우선하며, `policy.allowUnsafeOverrides`가 `true`가 아니면 더 강한 action을 약화하려 할 경우 오류가 발생합니다. 강도 순: `allow`(0) < `redact`/`mask`(1) < `tokenize`/`encrypt`(2) < `block`(3).
200
200
 
201
201
  ### Presets
202
202
 
@@ -240,96 +240,96 @@ preset과 override(또는 privacy profile)가 충돌할 경우 **강한** action
240
240
 
241
241
  ## loopback 밖으로 바인딩
242
242
 
243
- proxy는 CLI 플래그를 명시적으로 전달하지 않으면 loopback이 아닌 host를 거부한다 — 설정 파일에 `proxy.host: "0.0.0.0"`을 지정해도 의도적으로 시작되지 않는다:
243
+ proxy는 CLI 플래그를 명시적으로 전달하지 않으면 loopback이 아닌 host를 거부합니다 — 설정 파일에 `proxy.host: "0.0.0.0"`을 지정해도 의도적으로 시작되지 않습니다:
244
244
 
245
245
  ```bash
246
246
  haechi proxy --config haechi.config.json --host 0.0.0.0 --allow-remote-bind
247
247
  ```
248
248
 
249
- **proxy는 아직 클라이언트 인증을 제공하지 않는다**(0.6 계획): 포트에 접근할 수 있는 누구든 upstream과 token round-trip 경로를 사용할 수 있다. `--allow-remote-bind`는 명시적인 네트워크 통제 하에서만 사용해야 한다 — 컨테이너 내에서 `0.0.0.0`으로 바인드하고 host 포트 매핑을 제한하거나(`-p 127.0.0.1:1016:1016`), 방화벽/VPN/인증 reverse proxy 뒤에 두어야 한다.
249
+ **proxy는 bearer 클라이언트 인증을 제공합니다**(`auth.provider: bearer`, 0.6에서 출시). 해시 기반 token 저장소, identity별 policy profile, model allowlist, identity별 rate limit을 함께 제공합니다([`auth`](#auth)와 [Named profiles](#named-profiles) 참고). 기본값 `auth.provider: none`은 proxy를 인증 없이 둡니다 — `none`에서는 포트에 접근할 수 있는 누구든 upstream과 token round-trip 경로를 사용할 수 있습니다. 내장 rate limit은 단일 프로세스(인메모리, 프로세스별)이므로, 여러 replica는 공유 limiter를 앞에 두어야 합니다. `--allow-remote-bind`는 어느 경우에도 명시적인 네트워크 통제 하에서만 사용해야 합니다 — 컨테이너 안에서 `0.0.0.0`으로 바인드하고 host 포트 매핑을 제한하거나(`-p 127.0.0.1:11016:11016`), 방화벽/VPN/인증 reverse proxy 뒤에 두어야 합니다.
250
250
 
251
251
  ## 검증 요약
252
252
 
253
- 다음은 로드 시 오류(fail-closed)를 발생시킨다: 알 수 없는 `keys.provider`; 빈 `proxy.host`; 범위를 벗어난 `proxy.port`; `jsonl`이 아닌 `audit.sink`; `local`이 아닌 `tokenVault.provider`; 잘못된 `revealPolicy`; 양수가 아닌 `retentionDays`; boolean이 아닌 `deterministic`/`detokenizeResponses`; 비어 있거나 문자열이 아닌 `deterministicTypes`; 비어 있거나 문자열이 아닌 `mcp.allowedMethods`; boolean이 아닌 `mcp.*` 플래그; 알 수 없는 `privacy.profile`; 잘못된 `responseProtection.failureMode`; 양수가 아닌 `responseProtection.maxBytes`; boolean이 아닌 `responseProtection.scanNumbers`; 잘못된 `streaming.requestMode`; 잘못된 `streaming.responseMode`; 양수가 아닌 `streaming.maxMatchBytes`; 잘못된 `auth.provider`; 빈 `auth.store`; 문자열이 아닌 `auth.allowedLabelKeys`; 객체가 아닌 `policy.profiles`; 유효한 `default` 없는 `policy.profileBinding`; 문자열이 아닌 `policy.modelAllowlist`; 양수가 아닌 `policy.rate.requestsPerMinute`; 양수가 아닌 `limits.*`; 알 수 없는 `target.type`/`adapter`; 안전하지 않은 커스텀 정규식; `allowUnsafeOverrides` 없이 action을 약화하려는 시도.
253
+ 다음은 로드 시 오류(fail-closed)를 발생시킵니다: 알 수 없는 `keys.provider`; 빈 `proxy.host`; 범위를 벗어난 `proxy.port`; `jsonl`이 아닌 `audit.sink`; `local`이 아닌 `tokenVault.provider`; 잘못된 `revealPolicy`; 양수가 아닌 `retentionDays`; boolean이 아닌 `deterministic`/`detokenizeResponses`; 비어 있거나 문자열이 아닌 `deterministicTypes`; 비어 있거나 문자열이 아닌 `mcp.allowedMethods`; boolean이 아닌 `mcp.*` 플래그; 알 수 없는 `privacy.profile`; 잘못된 `responseProtection.failureMode`; 양수가 아닌 `responseProtection.maxBytes`; boolean이 아닌 `responseProtection.scanNumbers`; 잘못된 `streaming.requestMode`; 잘못된 `streaming.responseMode`; 양수가 아닌 `streaming.maxMatchBytes`; 잘못된 `auth.provider`; 빈 `auth.store`; 문자열이 아닌 `auth.allowedLabelKeys`; 객체가 아닌 `policy.profiles`; 유효한 `default` 없는 `policy.profileBinding`; 문자열이 아닌 `policy.modelAllowlist`; 양수가 아닌 `policy.rate.requestsPerMinute`; 양수가 아닌 `limits.*`; 알 수 없는 `target.type`/`adapter`; 안전하지 않은 커스텀 정규식; `allowUnsafeOverrides` 없이 action을 약화하려는 시도.
254
254
 
255
255
  # Satellite 운영자 설정 (0.9)
256
256
 
257
- 아래 두 섹션은 0.9에서 도입된 **독립적으로 배포되는 satellite 패키지** — `haechi-dashboard`와 `haechi-auth-oidc`의 설정을 다룬다. **이들은 코어 `haechi.config.json` / `normalizeConfig` 스키마의 키가 아니다.** 각 satellite는 팩토리 함수(`createDashboardServer(options)` / `createOidcSessionBroker(options)`)에 **옵션 객체**를 전달해 설정하며, 각자의 `normalizeDashboardConfig` / `normalizeOidcConfig`가 검증한다. 검증은 코어와 동일한 **strict, fail-closed** 원칙을 따른다: 알 수 없는 옵션 키는 오류를 발생시키고, 아래의 모든 필드는 fail-closed throw 조건을 명시한다. 소스: `satellites/dashboard/index.mjs`, `satellites/auth-oidc/index.mjs`. 위협 모델 커버리지: **P1-OPS-005**(dashboard audit 노출 / DNS-rebinding / remote bind), **P1-SEC-009**(broker session/login 보안), `docs/current/release-0.9-implementation-scope.md` §6 참고.
257
+ 아래 두 섹션은 0.9에서 도입된 **독립적으로 배포되는 satellite 패키지** — `haechi-dashboard`와 `haechi-auth-oidc`의 설정을 다룹니다. **이들은 코어 `haechi.config.json` / `normalizeConfig` 스키마의 키가 아닙니다.** 각 satellite는 팩토리 함수(`createDashboardServer(options)` / `createOidcSessionBroker(options)`)에 **옵션 객체**를 전달해 설정하며, 각자의 `normalizeDashboardConfig` / `normalizeOidcConfig`가 검증합니다. 검증은 코어와 동일한 **strict, fail-closed** 원칙을 따릅니다. 알 수 없는 옵션 키는 오류를 발생시키고, 아래의 모든 필드는 fail-closed throw 조건을 명시합니다. 소스: `satellites/dashboard/index.mjs`, `satellites/auth-oidc/index.mjs`. 위협 모델 커버리지: **P1-OPS-005**(dashboard audit 노출 / DNS-rebinding / remote bind), **P1-SEC-009**(broker session/login 보안), `docs/current/release-0.9-implementation-scope.md` §6 참고.
258
258
 
259
259
  ## `haechi-dashboard` (satellite)
260
260
 
261
- audit JSONL과 그 hash-chain 상태를 제공하는 zero-dependency **read-only** audit 뷰어(`node:http`)다. 런타임이 아닌 **경로**를 받는다. `createDashboardServer(options)`로 설정하며, `normalizeDashboardConfig(options)`가 검증 후 실제 적용 설정을 반환한다. 소스: `satellites/dashboard/index.mjs`.
261
+ audit JSONL과 그 hash-chain 상태를 제공하는 zero-dependency **read-only** audit 뷰어(`node:http`)입니다. 런타임이 아닌 **경로**를 받습니다. `createDashboardServer(options)`로 설정하며, `normalizeDashboardConfig(options)`가 검증 후 실제 적용 설정을 반환합니다. 소스: `satellites/dashboard/index.mjs`.
262
262
 
263
263
  | 옵션 | 타입 / 값 | 기본값 | 설명 / fail-closed throw |
264
264
  |---|---|---|---|
265
265
  | `auditPath` | 비어 있지 않은 문자열 | **필수** | audit JSONL 경로. 누락되거나 비어 있지 않은 문자열이 아니면 throw. |
266
266
  | `anchorPath` | string \| `null` | `null` | tail 절단 탐지를 위해 `verifyAuditChain`에 전달되는 anchor 스트림 경로. 존재하지만 비어 있지 않은 문자열이 아니면 throw. |
267
- | `host` | 비어 있지 않은 문자열 | `127.0.0.1` | 바인드 주소. loopback이 아니면 `allowRemoteBind`와 아래 remote-bind 전제 조건을 모두 충족해야 한다. 존재하지만 비어 있거나 문자열이 아니면 throw. |
267
+ | `host` | 비어 있지 않은 문자열 | `127.0.0.1` | 바인드 주소. loopback이 아니면 `allowRemoteBind`와 아래 remote-bind 전제 조건을 모두 충족해야 합니다. 존재하지만 비어 있거나 문자열이 아니면 throw. |
268
268
  | `port` | 정수 0–65535 | `1018` | 리슨 포트; `0` = OS 할당 임시 포트(의도된 affordance). `[0,65535]` 정수가 아니면 throw. |
269
- | `allowRemoteBind` | boolean | `false` | loopback이 아닌 `host`를 허용한다. boolean이 아니면 throw. 설정만으로는 충분하지 않다 — remote-bind 전제 조건 참고. |
270
- | `sessionGuard` | object \| `null` | `null` | `authenticate(req) -> session\|null`과 선택적 `handlers` 맵을 구현하는 guard. object가 아니거나 `authenticate`가 함수가 아니면 throw. `handlers` 키는 고정된 broker 경로 `/auth/login`, `/auth/callback`, `/auth/logout`만 허용되며, 다른 키(특히 `/api/*`, `/healthz`, `/`)는 throw — guard가 audit 경로를 게이트에서 면제시키는 auth-bypass를 차단한다. `haechi-auth-oidc` broker를 주입하면 충족된다(아래 참고). |
269
+ | `allowRemoteBind` | boolean | `false` | loopback이 아닌 `host`를 허용합니다. boolean이 아니면 throw. 설정만으로는 충분하지 않습니다 — remote-bind 전제 조건 참고. |
270
+ | `sessionGuard` | object \| `null` | `null` | `authenticate(req) -> session\|null`과 선택적 `handlers` 맵을 구현하는 guard. object가 아니거나 `authenticate`가 함수가 아니면 throw. `handlers` 키는 고정된 broker 경로 `/auth/login`, `/auth/callback`, `/auth/logout`만 허용되며, 다른 키(특히 `/api/*`, `/healthz`, `/`)는 throw — guard가 audit 경로를 게이트에서 면제시키는 auth-bypass를 차단합니다. `haechi-auth-oidc` broker를 주입하면 충족됩니다(아래 참고). |
271
271
  | `window` | 정수 4096–67108864 | `1048576` | `/api/events`와 `/api/summary`의 tail-read 윈도우(최대 바이트). `[4096, 67108864]`(4 KiB–64 MiB) 정수가 아니면 throw. |
272
- | `tlsContext` | object \| `null` | `null` | dashboard가 직접 HTTPS를 종단하기 위한 TLS 자료. object가 아니거나, non-null인데 **사용 가능한 자료**가 없으면 throw — `(key && cert)` 또는 `pfx`를 반드시 포함해야 한다(빈 `{}`는 거부되어 loopback이 아닌 plaintext 리스너를 green-light하지 못하게 한다). |
273
- | `trustProxy` | string \| `null` | `null` | 신뢰하는 fronting-proxy 주소/CIDR를 명시한다. 문자열이 아니거나, 비어 있거나, falsy 모양 문자열(`"false"`/`"0"`)이면 throw. **`trustProxy`만으로는 loopback이 아닌 바인드를 절대 인가하지 못한다** — 실제 `tlsContext`만 가능하다. |
272
+ | `tlsContext` | object \| `null` | `null` | dashboard가 직접 HTTPS를 종단하기 위한 TLS 자료. object가 아니거나, non-null인데 **사용 가능한 자료**가 없으면 throw — `(key && cert)` 또는 `pfx`를 반드시 포함해야 합니다(빈 `{}`는 거부되어 loopback이 아닌 plaintext 리스너를 green-light하지 못하게 합니다). |
273
+ | `trustProxy` | string \| `null` | `null` | 신뢰하는 fronting-proxy 주소/CIDR를 명시합니다. 문자열이 아니거나, 비어 있거나, falsy 모양 문자열(`"false"`/`"0"`)이면 throw. **`trustProxy`만으로는 loopback이 아닌 바인드를 절대 인가하지 못합니다** — 실제 `tlsContext`만 가능합니다. |
274
274
 
275
275
  ### 라우트
276
276
 
277
- 모든 라우트는 **GET/HEAD 전용**(그 외 method → `405`)이며, asset 맵은 in-code로 고정되어 있다(파일시스템 traversal 없음):
277
+ 모든 라우트는 **GET/HEAD 전용**(그 외 method → `405`)이며, asset 맵은 in-code로 고정되어 있습니다(파일시스템 traversal 없음):
278
278
 
279
- - `/api/events` — audit JSONL의 bounded tail read, 최신순. `limit`은 `[1,200]` 정수(기본 50); `cursor`는 opaque `auditIntegrity.sequence`(파일시스템 오프셋이 아님). 각 이벤트는 **recursive key-by-key allowlist projection**으로 재구성된다(blind spread 없음; identity는 scope/label/raw subject 없이 `subjectHash`/`issuerHash`만 보유). 요청된 페이지가 유지된 윈도우보다 오래되면 `windowExceeded`를 반환한다.
280
- - `/api/chain` — `verifyAuditChain`을 감싸며, 파생된 `truncationDetected` boolean을 노출한다(raw 실패 reason은 **절대** 반환하지 않음). mtime+size 캐시(동시 재-walk 없음); 32 MiB 상한 초과 시 `{valid:null}`과 함께 `413`; `HEAD`는 walk를 강제하지 않고 헤더만 반환한다.
279
+ - `/api/events` — audit JSONL의 bounded tail read, 최신순. `limit`은 `[1,200]` 정수(기본 50); `cursor`는 opaque `auditIntegrity.sequence`(파일시스템 오프셋이 아님). 각 이벤트는 **recursive key-by-key allowlist projection**으로 재구성됩니다(blind spread 없음; identity는 scope/label/raw subject 없이 `subjectHash`/`issuerHash`만 보유). 요청된 페이지가 유지된 윈도우보다 오래되면 `windowExceeded`를 반환합니다.
280
+ - `/api/chain` — `verifyAuditChain`을 감싸며, 파생된 `truncationDetected` boolean을 노출합니다(raw 실패 reason은 **절대** 반환하지 않습니다). mtime+size 캐시(동시 재-walk 없음); 32 MiB 상한 초과 시 `{valid:null}`과 함께 `413`; `HEAD`는 walk를 강제하지 않고 헤더만 반환합니다.
281
281
  - `/api/summary` — tail 윈도우에 대한 집계 탐지 카운트(`byType`/`byAction`/`detectionCount`).
282
- - `/healthz` — liveness 전용(`{status:"ok"}`); loopback 밖에서도 session 불필요.
282
+ - `/healthz` — liveness 전용(`{status:"ok"}`); loopback 밖에서도 session 필요 없습니다.
283
283
 
284
284
  ### 보안 기본값
285
285
 
286
- - **기본 loopback 바인드.** `host` 기본값은 `127.0.0.1`이며, loopback이 아닌 host 바인드는 코어의 `assertSafeProxyBind`(재-표현)를 재사용하고 `allowRemoteBind`를 요구한다.
287
- - **Remote bind는 fail-closed.** loopback이 아닌 바인드는 `allowRemoteBind: true`, `sessionGuard`, **그리고** 유효한 `tlsContext`(dashboard가 직접 TLS 종단)를 **모두** 요구한다. `trustProxy`는 이를 충족하지 못한다 — loopback이 아닌 plaintext 리스너는 audit 데이터를 평문으로 제공하면서 HSTS를 방출하므로 거부된다. HSTS는 서버가 실제로 HTTPS를 제공할 때**만** 방출된다.
288
- - **anti-DNS-rebinding Host allowlist**가 모든 요청(`/api/*`, `/healthz`, 모든 method 포함)의 무조건적 첫 게이트다; 잘못되거나 중복된 `Host` 헤더 → method 검사 이전에 `403`.
289
- - **strict CSP + Trusted Types**(`require-trusted-types-for 'script'`, `textContent` 렌더링) 및 `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy`/`-Opener-Policy: same-origin`, `X-Content-Type-Options: nosniff`, `Cache-Control: no-store`; CORS 헤더는 의도적으로 절대 설정하지 않는다.
290
- - **sessionGuard seam.** guard가 존재하면 모든 `/api/*` 라우트는 `authenticate()` 뒤에 게이트된다; 미인증 요청은 `401`(`302` 리다이렉트가 아님). auth-면제 집합은 고정된 broker-path allowlist와 guard가 선언한 handlers의 **교집합**(exact match)이다 — guard는 audit-data 라우트를 절대 면제시킬 수 없다.
291
- - **generic 오류.** 5xx는 `{error:"internal"}`만 반환한다 — stack, OS code, 파일시스템 경로는 절대 없음. satellite-local fixed-window rate limiter(소스별 120 req/60s)가 `/api/*` 앞단을 막는다.
286
+ - **기본 loopback 바인드.** `host` 기본값은 `127.0.0.1`이며, loopback이 아닌 host 바인드는 코어의 `assertSafeProxyBind`(재-표현)를 재사용하고 `allowRemoteBind`를 요구합니다.
287
+ - **Remote bind는 fail-closed.** loopback이 아닌 바인드는 `allowRemoteBind: true`, `sessionGuard`, **그리고** 유효한 `tlsContext`(dashboard가 직접 TLS 종단)를 **모두** 요구합니다. `trustProxy`는 이를 충족하지 못합니다 — loopback이 아닌 plaintext 리스너는 audit 데이터를 평문으로 제공하면서 HSTS를 방출하므로 거부됩니다. HSTS는 서버가 실제로 HTTPS를 제공할 때**만** 방출됩니다.
288
+ - **anti-DNS-rebinding Host allowlist**가 모든 요청(`/api/*`, `/healthz`, 모든 method 포함)의 무조건적 첫 게이트입니다. 잘못되거나 중복된 `Host` 헤더 → method 검사 이전에 `403`.
289
+ - **strict CSP + Trusted Types**(`require-trusted-types-for 'script'`, `textContent` 렌더링) 및 `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy`/`-Opener-Policy: same-origin`, `X-Content-Type-Options: nosniff`, `Cache-Control: no-store`; CORS 헤더는 의도적으로 절대 설정하지 않습니다.
290
+ - **sessionGuard seam.** guard가 존재하면 모든 `/api/*` 라우트는 `authenticate()` 뒤에 게이트됩니다. 미인증 요청은 `401`(`302` 리다이렉트가 아닙니다). auth-면제 집합은 고정된 broker-path allowlist와 guard가 선언한 handlers의 **교집합**(exact match)입니다 — guard는 audit-data 라우트를 절대 면제시킬 수 없습니다.
291
+ - **generic 오류.** 5xx는 `{error:"internal"}`만 반환합니다 — stack, OS code, 파일시스템 경로는 절대 없습니다. satellite-local fixed-window rate limiter(소스별 120 req/60s)가 `/api/*` 앞단을 막습니다.
292
292
 
293
- bin `haechi-dashboard`(workspace)가 서버를 구동하며, publish 워크플로는 `.github/workflows/dashboard-publish.yml`(태그 `dashboard-v<semver>`)이다. `peerDependencies: { haechi: ">=0.8.0 <1.0.0" }`.
293
+ bin `haechi-dashboard`(workspace)가 서버를 구동하며, publish 워크플로는 `.github/workflows/dashboard-publish.yml`(태그 `dashboard-v<semver>`)입니다. `peerDependencies: { haechi: ">=0.8.0 <1.0.0" }`.
294
294
 
295
295
  ## `haechi-auth-oidc` (satellite)
296
296
 
297
- zero-dependency **interactive OIDC session broker**(authorization-code + PKCE) dashboard의 사람-로그인 메커니즘이다. opaque server-side session을 생성하고, **주입을 통해 dashboard `sessionGuard` 계약을 충족한다**(`{ authenticate(req), handlers: { "/auth/login", "/auth/callback", "/auth/logout" } }`). per-request bearer validator가 **아니다**(그 역할은 `haechi-auth-jwt`에 남는다). `createOidcSessionBroker(options)`로 설정하며 `normalizeOidcConfig(options)`가 검증한다. 소스: `satellites/auth-oidc/index.mjs`. `peerDependencies: { haechi: ">=0.8.0 <1.0.0", haechi-auth-jwt: ">=0.2.0 <1.0.0" }`.
297
+ zero-dependency **interactive OIDC session broker**(authorization-code + PKCE)이며, dashboard의 사람-로그인 메커니즘입니다. opaque server-side session을 생성하고, **주입을 통해 dashboard `sessionGuard` 계약을 충족합니다**(`{ authenticate(req), handlers: { "/auth/login", "/auth/callback", "/auth/logout" } }`). per-request bearer validator가 **아닙니다**(그 역할은 `haechi-auth-jwt`에 남습니다). `createOidcSessionBroker(options)`로 설정하며 `normalizeOidcConfig(options)`가 검증합니다. 소스: `satellites/auth-oidc/index.mjs`. `peerDependencies: { haechi: ">=0.8.0 <1.0.0", haechi-auth-jwt: ">=0.2.0 <1.0.0" }`.
298
298
 
299
299
  | 옵션 | 타입 / 값 | 기본값 | 설명 / fail-closed throw |
300
300
  |---|---|---|---|
301
- | `cryptoProvider` | `hmac()`를 가진 object | **필수** | PII-safe identity 해시와 `sessionIdHash`를 위한 keyed-HMAC를 제공한다. `hmac`이 함수가 아니면 throw. |
302
- | `issuer` | HTTPS URL 문자열 | **필수** | OIDC issuer; 정확한 string-equal discovery와 single-origin endpoint 검사를 위해 pin된다. 누락되거나 `https`가 아니면 throw. |
301
+ | `cryptoProvider` | `hmac()`를 가진 object | **필수** | PII-safe identity 해시와 `sessionIdHash`를 위한 keyed-HMAC를 제공합니다. `hmac`이 함수가 아니면 throw. |
302
+ | `issuer` | HTTPS URL 문자열 | **필수** | OIDC issuer; 정확한 string-equal discovery와 single-origin endpoint 검사를 위해 pin됩니다. 누락되거나 `https`가 아니면 throw. |
303
303
  | `clientId` | 비어 있지 않은 문자열 | **필수** | OAuth client id(ID-token의 기대 `aud`이기도 함). 누락/비어 있으면 throw. |
304
304
  | `clientSecret` | string \| 생략 | 생략 | 존재 ⇒ confidential client; 생략 ⇒ public(PKCE 전용) client. 존재하지만 비어 있으면 throw. |
305
- | `redirectUri` | 절대 URL 문자열 | **필수** | `https`(또는 carve-out 하의 **loopback** `http`)여야 하고, broker와 **same-origin**이며, path가 정확히 `/auth/callback`이어야 한다. 그 외에는 throw. |
306
- | `scopes` | 문자열 배열 | `["openid"]` | `openid`는 강제 포함(dedup)되고, `offline_access`는 제거된다(refresh rotation은 0.9 범위 밖). 비어 있지 않은 문자열 배열이 아니면 throw. |
305
+ | `redirectUri` | 절대 URL 문자열 | **필수** | `https`(또는 carve-out 하의 **loopback** `http`)여야 하고, broker와 **same-origin**이며, path가 정확히 `/auth/callback`이어야 합니다. 그 외에는 throw. |
306
+ | `scopes` | 문자열 배열 | `["openid"]` | `openid`는 강제 포함(dedup)되고, `offline_access`는 제거됩니다(refresh rotation은 0.9 범위 밖). 비어 있지 않은 문자열 배열이 아니면 throw. |
307
307
  | `returnToAllowlist` | 문자열 배열 | `["/"]` | **relative same-origin** 복귀 경로의 allowlist(단일 `/`로 시작, scheme/host/`//`/백슬래시 없음). 배열이 아니거나 비적합 항목이 있으면 throw. |
308
308
  | `sessionTtlSeconds` | 정수 1–2592000 | `28800`(8h) | 절대 session 수명. `[1, 2592000]`(30d 상한)을 벗어나면 throw. |
309
309
  | `idleTtlSeconds` | 정수 1–2592000 | `1800`(30m) | idle 타임아웃(sliding `lastSeen`). 범위를 벗어나면 throw. |
310
- | `maxAgeSeconds` | 정수 1–2592000 \| `null` | `null` | 설정 시 OIDC `max_age`를 보내고 `auth_time`이 `maxAge + skew` 이내일 것을 요구한다. 존재하지만 범위를 벗어나면 throw. |
310
+ | `maxAgeSeconds` | 정수 1–2592000 \| `null` | `null` | 설정 시 OIDC `max_age`를 보내고 `auth_time`이 `maxAge + skew` 이내일 것을 요구합니다. 존재하지만 범위를 벗어나면 throw. |
311
311
  | `tokenEndpointAuthMethod` | `client_secret_basic` \| `client_secret_post` | `client_secret_basic` | token-endpoint 인증 방식. 알 수 없는 값이거나, `clientSecret` 없이 설정되면 throw(confidential client에서만 유효). |
312
- | `secureCookies` | `true` \| `false` \| `"auto"` | `"auto"` | externally-visible scheme로부터 쿠키 `Secure`/`__Host-` 하드닝을 강제하거나 자동 도출한다. 그 외 값이면 throw. |
313
- | `trustProxy` | string \| `null` | `null` | TLS를 종단하는 fronting proxy를 명시한다; browser-facing scheme를 HTTPS로 간주한다(쿠키 하드닝에 반영). 문자열이 아니거나 비어 있으면 throw. |
312
+ | `secureCookies` | `true` \| `false` \| `"auto"` | `"auto"` | externally-visible scheme로부터 쿠키 `Secure`/`__Host-` 하드닝을 강제하거나 자동 도출합니다. 그 외 값이면 throw. |
313
+ | `trustProxy` | string \| `null` | `null` | TLS를 종단하는 fronting proxy를 명시합니다; browser-facing scheme를 HTTPS로 간주합니다(쿠키 하드닝에 반영). 문자열이 아니거나 비어 있으면 throw. |
314
314
  | `algorithms` | 비어 있지 않은 문자열 배열 | `["RS256","ES256"]` | 허용된 JWS 알고리즘(verifier로 전달). 비어 있지 않은 배열이 아니면 throw. |
315
315
  | `clockSkewSeconds` | 수 0–300 | (verifier 기본값) | ID-token 시간 클레임의 여유. `[0,300]`을 벗어나면 throw. |
316
316
  | `prompt` | string \| `null` | `null` | 선택적 OIDC `prompt`. 존재하지만 비어 있거나 문자열이 아니면 throw. |
317
317
  | `pendingTtlSeconds` | 정수 1–3600 | `600`(10m) | 로그인 완료 제한 시간(pre-auth 레코드 TTL). `[1,3600]`을 벗어나면 throw. |
318
- | `pendingCap` | 정수 1–1000000 | `1024` | 동시 진행 중 로그인의 hard cap; store가 가득 차면 **새** 로그인을 거부하고 진행 중 auth는 절대 evict하지 않는다(fail-closed). 범위를 벗어나면 throw. |
318
+ | `pendingCap` | 정수 1–1000000 | `1024` | 동시 진행 중 로그인의 hard cap; store가 가득 차면 **새** 로그인을 거부하고 진행 중 auth는 절대 evict하지 않습니다(fail-closed). 범위를 벗어나면 throw. |
319
319
  | `rateLimitMax` | 정수 1–1000000 | `60` | 소스별 60s 윈도우당 `/auth/login`+`/auth/callback`. 범위를 벗어나면 throw. |
320
320
  | `fetchTimeoutMs` | 정수 1–120000 | `5000` | egress별 타임아웃(discovery / token / JWKS). 범위를 벗어나면 throw. |
321
321
  | `fetchImpl` / `lookupImpl` / `now` | 함수 | 주입/전역 | `fetch` / DNS `lookup` / clock seam 주입. 존재하지만 함수가 아니면 throw. |
322
- | `sessionStore` | object | in-memory | opaque-id → session store; `get`/`set`/`delete`를 구현해야 한다. 존재하지만 비적합하면 throw. |
323
- | `pendingStore` | object | in-memory | pre-auth 레코드 store; `set`/`take`(원자적 단일-사용 `take`)를 구현해야 한다. 존재하지만 비적합하면 throw. |
322
+ | `sessionStore` | object | in-memory | opaque-id → session store; `get`/`set`/`delete`를 구현해야 합니다. 존재하지만 비적합하면 throw. |
323
+ | `pendingStore` | object | in-memory | pre-auth 레코드 store; `set`/`take`(원자적 단일-사용 `take`)를 구현해야 합니다. 존재하지만 비적합하면 throw. |
324
324
  | `auditSink` | 함수 \| `record()`를 가진 object | 없음 | PII-safe 이벤트 sink. 존재하지만 함수도 `record()` 가진 object도 아니면 throw. |
325
325
 
326
326
  ### 쿠키 하드닝 의미
327
327
 
328
- session은 **server-side 전용**이다 — 쿠키는 클레임/토큰이 아닌 opaque id만 보유한다. 두 개의 쿠키를 사용한다(pending 레코드를 바인딩하는 pre-auth 쿠키, 그리고 session 쿠키). externally-visible scheme가 HTTPS이면(`https` `redirectUri`, `secureCookies: true`, 또는 non-null `trustProxy`) 쿠키는 **`__Host-` prefix + `Secure` + `HttpOnly` + `SameSite=Lax`**(`Path=/`, `Domain` 없음)를 사용한다; `SameSite=Lax`는 IdP의 top-level GET이 `/auth/callback`으로 쿠키를 실어 보내게 한다. 문서화된 **loopback-`http` carve-out** 하에서는 `__Host-`/`Secure` 속성이 제거되고(plaintext 리스너는 `Secure`를 설정할 수 없음) bare 쿠키 이름을 사용한다. **HTTPS가 확인되지 않은 off-loopback broker는 construction에서 fail-closed**된다 — `Secure`/`__Host-` 쿠키는 평문으로 전송되지 않으므로 로그인이 조용히 깨질 것이다. `/auth/callback`에서 **새** session id가 발급된다(fixation 없음); `/auth/logout`은 non-GET, CSRF-헤더 게이트(`x-haechi-csrf`)이며 server-side 상태를 파괴한다. access token은 폐기된다(절대 저장하지 않음). audit 이벤트(`oidc.login.start`/`success`/`failure{reasonCode}`/`logout`/`session.evict`)는 keyed-HMAC `subjectHash`/`issuerHash`/`sessionIdHash` + `provider` + 거친 `reasonCode` + timestamp만 보유한다.
328
+ session은 **server-side 전용**입니다 — 쿠키는 클레임/토큰이 아닌 opaque id만 보유합니다. 두 개의 쿠키를 사용합니다(pending 레코드를 바인딩하는 pre-auth 쿠키, 그리고 session 쿠키). externally-visible scheme가 HTTPS이면(`https` `redirectUri`, `secureCookies: true`, 또는 non-null `trustProxy`) 쿠키는 **`__Host-` prefix + `Secure` + `HttpOnly` + `SameSite=Lax`**(`Path=/`, `Domain` 없음)를 사용합니다. `SameSite=Lax`는 IdP의 top-level GET이 `/auth/callback`으로 쿠키를 실어 보내게 합니다. 문서화된 **loopback-`http` carve-out** 하에서는 `__Host-`/`Secure` 속성이 제거되고(plaintext 리스너는 `Secure`를 설정할 수 없습니다) bare 쿠키 이름을 사용합니다. **HTTPS가 확인되지 않은 off-loopback broker는 construction에서 fail-closed됩니다** — `Secure`/`__Host-` 쿠키는 평문으로 전송되지 않으므로 로그인이 조용히 깨질 것이기 때문입니다. `/auth/callback`에서 **새** session id가 발급됩니다(fixation 없음). `/auth/logout`은 non-GET, CSRF-헤더 게이트(`x-haechi-csrf`)이며 server-side 상태를 파괴합니다. access token은 폐기됩니다(절대 저장하지 않습니다). audit 이벤트(`oidc.login.start`/`success`/`failure{reasonCode}`/`logout`/`session.evict`)는 keyed-HMAC `subjectHash`/`issuerHash`/`sessionIdHash` + `provider` + 거친 `reasonCode` + timestamp만 보유합니다.
329
329
 
330
330
  ### dashboard와의 연결
331
331
 
332
- broker를 dashboard의 `sessionGuard`로 주입한다:
332
+ broker를 dashboard의 `sessionGuard`로 주입합니다:
333
333
 
334
334
  ```js
335
335
  import { createDashboardServer } from "haechi-dashboard";
@@ -353,4 +353,4 @@ const dashboard = createDashboardServer({
353
353
  });
354
354
  ```
355
355
 
356
- broker의 `handlers` 맵은 dashboard가 auth 게이트에서 면제하는 고정 broker 경로에서만 마운트되며, 모든 `/api/*` 라우트는 `broker.authenticate(req)` 뒤에 게이트된다. publish 워크플로: `.github/workflows/auth-oidc-publish.yml`(태그 `auth-oidc-v<semver>`).
356
+ broker의 `handlers` 맵은 dashboard가 auth 게이트에서 면제하는 고정 broker 경로에서만 마운트되며, 모든 `/api/*` 라우트는 `broker.authenticate(req)` 뒤에 게이트됩니다. publish 워크플로: `.github/workflows/auth-oidc-publish.yml`(태그 `auth-oidc-v<semver>`).
@@ -1,7 +1,6 @@
1
1
  # Haechi Configuration Reference
2
2
 
3
- - Status: Living document
4
- - Target version: 0.6.0
3
+ - Status: Living document (tracks core 1.1.x)
5
4
 
6
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.
7
6
 
@@ -11,10 +10,10 @@
11
10
  {
12
11
  "mode": "dry-run",
13
12
  "target": { "type": "llm-http", "adapter": "openai-compatible", "upstream": "http://127.0.0.1:9999" },
14
- "proxy": { "host": "127.0.0.1", "port": 1016 },
13
+ "proxy": { "host": "127.0.0.1", "port": 11016 },
15
14
  "responseProtection": { "enabled": false, "mode": "enforce", "failureMode": "fail-closed", "allowNonJson": false, "allowCompressed": false, "maxBytes": 1048576 },
16
15
  "streaming": { "requestMode": "block" },
17
- "limits": { "maxRequestBytes": 1048576, "upstreamTimeoutMs": 120000 },
16
+ "limits": { "maxRequestBytes": 1048576, "upstreamTimeoutMs": 120000, "maxNestingDepth": 256 },
18
17
  "policy": { "mode": "dry-run", "presets": ["korean-pii", "secrets-only", "llm-redact"], "defaultAction": "redact", "actions": { "card": "block" } },
19
18
  "filters": { "customRules": [] },
20
19
  "keys": { "provider": "local", "keyFile": ".haechi/dev.keys.json" },
@@ -44,7 +43,7 @@
44
43
  | Key | Type / values | Default | Notes |
45
44
  |---|---|---|---|
46
45
  | `proxy.host` | non-empty string | `127.0.0.1` | Bind address. Non-loopback hosts require the `--allow-remote-bind` CLI flag — config alone will not start (see [Binding beyond loopback](#binding-beyond-loopback)). |
47
- | `proxy.port` | integer 0–65535 | `1016` | Listen port (`0` = ephemeral). Override per-run with `--port`. |
46
+ | `proxy.port` | integer 0–65535 | `11016` | Listen port (`0` = ephemeral). Override per-run with `--port`. |
48
47
 
49
48
  ## `responseProtection`
50
49
 
@@ -74,6 +73,7 @@ Inspects upstream JSON responses (off by default — turn on to protect what com
74
73
  |---|---|---|---|
75
74
  | `limits.maxRequestBytes` | positive integer | `1048576` | Request body cap; over the limit returns `413`. Enforced incrementally (the body is not fully buffered first). |
76
75
  | `limits.upstreamTimeoutMs` | positive integer | `120000` | Upstream request timeout; on expiry returns `504 haechi_upstream_timeout`. Connection failure returns `502 haechi_upstream_unreachable`. |
76
+ | `limits.maxNestingDepth` | positive integer | `256` | Max JSON nesting depth walked during detection. A more deeply nested body is rejected `413 haechi_request_too_deeply_nested` (fail-closed, before upstream), guarding the recursive payload walk against a stack overflow. Bounds container descent; leaves at the limit are still inspected. (Separately, a non-UTF-8 request body is rejected fail-closed: `400 haechi_request_body_not_utf8`.) |
77
77
 
78
78
  ## `policy`
79
79
 
@@ -246,7 +246,7 @@ The proxy refuses non-loopback hosts unless the CLI flag is passed explicitly
246
246
  haechi proxy --config haechi.config.json --host 0.0.0.0 --allow-remote-bind
247
247
  ```
248
248
 
249
- **The proxy has no client authentication yet** (planned for 0.6): anyone who can reach the port can use your upstream and the token round-trip path. Use `--allow-remote-bind` only behind explicit network controls — bind `0.0.0.0` inside a container and restrict the host port mapping (`-p 127.0.0.1:1016:1016`), or front it with a firewall/VPN/authenticating reverse proxy.
249
+ **The proxy ships bearer client authentication** (`auth.provider: bearer`, shipped in 0.6): a hashed token store, per-identity policy profiles, a model allowlist, and a per-identity rate limit (see [`auth`](#auth) and [Named profiles](#named-profiles)). The default `auth.provider: none` leaves the proxy unauthenticated — with `none`, anyone who can reach the port can use your upstream and the token round-trip path. The built-in rate limit is single-process (in-memory, per-process); front multiple replicas with a shared limiter. Use `--allow-remote-bind` only behind explicit network controls regardless — bind `0.0.0.0` inside a container and restrict the host port mapping (`-p 127.0.0.1:11016:11016`), or front it with a firewall/VPN/authenticating reverse proxy.
250
250
 
251
251
  ## Validation cheatsheet
252
252
 
@@ -83,7 +83,7 @@ docs/
83
83
 
84
84
  | Mode | 변경 범위 | 적합한 상황 | 예시 |
85
85
  |---|---|---|---|
86
- | Local proxy | 코드 변경 거의 없음 | LLM HTTP, MCP Streamable HTTP를 빠르게 보호 | base URL을 `http://localhost:1016`로 변경 |
86
+ | Local proxy | 코드 변경 거의 없음 | LLM HTTP, MCP Streamable HTTP를 빠르게 보호 | base URL을 `http://localhost:11016`로 변경 |
87
87
  | SDK wrapper | 작은 코드 변경 | 앱 내부 context를 더 정확히 전달 | `haechi.protectMessage(...)` |
88
88
  | Middleware | 웹/API 서버에 삽입 | Express/Fastify/FastAPI 같은 gateway | request/response hook |
89
89
  | Sidecar | self-hosted service 옆 배치 | 컨테이너/서버 운영 환경 | app -> sidecar -> provider |
@@ -83,7 +83,7 @@ For the initial public release, `core`, `crypto`, `policy`, `filter`, `audit`, `
83
83
 
84
84
  | Mode | Scope of change | When to use | Example |
85
85
  |---|---|---|---|
86
- | Local proxy | Almost no code changes | Quickly protect LLM HTTP or MCP Streamable HTTP | Change base URL to `http://localhost:1016` |
86
+ | Local proxy | Almost no code changes | Quickly protect LLM HTTP or MCP Streamable HTTP | Change base URL to `http://localhost:11016` |
87
87
  | SDK wrapper | Small code changes | Pass more precise in-app context | `haechi.protectMessage(...)` |
88
88
  | Middleware | Insert into web/API server | Gateways like Express/Fastify/FastAPI | request/response hook |
89
89
  | Sidecar | Deploy alongside self-hosted service | Container/server runtime environments | app -> sidecar -> provider |