haechi 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ # Haechi 0.6 Implementation Scope
2
+
3
+ - 문서 상태: Final
4
+ - 작성일: 2026-06-10
5
+ - 기준 버전: 0.6.0 (0.5.0 이후)
6
+ - 성격: auth and per-client controls
7
+ - 구현 완료: 2026-06-10 — PR #17 (계약 + bearer store + CLI), #18 (named profiles), #19 (proxy enforcement)
8
+
9
+ ## 1. 릴리스 목표
10
+
11
+ 0.4에서 예약해 둔 `authProvider`/`identity` 계약을 구현하고, identity를 실질적인 per-client 제어로 전환한다: 내장 bearer 인증, 명명된 per-client policy profile, model allowlist, request rate limiting. 이로써 Haechi를 단일 호스트에서 복수의 클라이언트/에이전트 앞에 안전하게 배치할 수 있게 된다.
12
+
13
+ **범위 결정 (2026-06-10):** 0.6은 auth 핵심에 집중한다. 원래 0.6으로 묶였던 무거운 운영 항목들 — Vault/AWS KMS 레퍼런스 어댑터, 외부 append-only audit 싱크, 서명된 릴리스 아티팩트, npm org(`@haechi/*`) 취득 — 은 각각 별도의 보안 설계를 받을 수 있도록 **0.7**로 이월한다.
14
+
15
+ ## 2. 범위
16
+
17
+ ### 2.1 `authProvider` 계약 (core 소유)
18
+
19
+ - `authenticate(request) → identity | null` (null = 거부). `request`는 Node `IncomingMessage`이며 헤더만 사용 가능 — body는 **아직 읽지 않는다**.
20
+ - Fail-closed: 예외를 던지는 provider는 거부로 처리한다. `createRuntime(config, { authProvider })`를 통해 주입한다.
21
+ - `auth.provider`로 선택한다:
22
+ - `none` (기본값) — 인증 없음; `identity`는 `null`로 유지 (0.5와 byte 단위로 동일한 audit 형태). Per-client policy는 default profile / base policy로 결정된다.
23
+ - `bearer` — 내장 token auth (§2.2).
24
+ - `external` — 주입된 `authProvider` 필요; 없으면 fail-closed (`keys.provider: external`과 동일한 방식). OIDC/JWT provider는 **0.7+ 위성 패키지** (`@haechi/auth-oidc`)로 남긴다; 0.6은 네트워크 IdP 코드를 포함하지 않는다.
25
+
26
+ ### 2.2 내장 bearer auth + token store
27
+
28
+ - 자격 증명은 **별도 파일** `.haechi/auth.json` (mode `0600`)에 저장한다. `haechi.config.json`에는 절대 두지 않는다:
29
+ ```json
30
+ {
31
+ "version": 1,
32
+ "tokens": [
33
+ { "id": "tok_auth_ab12cd", "tokenHash": "...", "type": "service",
34
+ "scopes": ["team:eng"], "labels": { "env": "prod" },
35
+ "createdAt": "...", "disabled": false }
36
+ ]
37
+ }
38
+ ```
39
+ - `tokenHash` = `HMAC(derive("haechi:auth:token:v1"), token)` — 키 기반, domain-separated ([[key-management]] 규범 적용), bare hash 금지. 조회 시 timing-safe 비교를 사용한다.
40
+ - 토큰은 고엔트로피 `hae_<base64url(32 bytes)>` 형식이다. **평문은 생성 시 한 번만 출력하고 절대 저장하지 않는다.**
41
+ - CLI:
42
+ - `haechi auth add --type user|service|agent [--scope k:v ...] [--label k=v ...]` → 토큰을 발급하고, 해시와 메타데이터를 저장하며, 평문을 한 번만 출력한다.
43
+ - `haechi auth list` → id, type, scopes, labels, createdAt, disabled만 출력 — 토큰이나 해시는 절대 노출하지 않는다.
44
+ - `haechi auth revoke <id>` → `disabled: true`로 설정한다.
45
+ - 라벨 키는 `auth.allowedLabelKeys` (기본값 `["team", "env", "tier", "role"]`)에 대해 검증하며; 값은 길이를 제한한다. `add` 시점에 라벨의 PII는 거부된다.
46
+
47
+ ### 2.3 Identity 구성 (PII-safe)
48
+
49
+ bearer 매칭 성공 시 예약된 `identity` 객체를 구성한다:
50
+ - `id`: token 레코드 id (불투명).
51
+ - `type`: 레코드에서 가져온다.
52
+ - `subjectHash`: `HMAC(derive("haechi:identity:hash:v1"), record.id)`; `issuerHash`: `"bearer-local"`의 HMAC. 식별자의 bare SHA-256은 금지한다.
53
+ - `provider`: `"bearer"`.
54
+ - `scopes`, `labels`: 레코드에서 가져온다 (이미 allowlist 검증 완료).
55
+
56
+ 동일한 identity가 해당 요청의 모든 audit 이벤트(protect 이벤트, decision)에 첨부된다. `identity`는 `auth.provider: none` 하에서 `null`로 유지된다.
57
+
58
+ ### 2.4 명명된 policy profile (per-client policy)
59
+
60
+ `policy`에 profile과 binding 맵이 추가된다:
61
+ ```json
62
+ "policy": {
63
+ "mode": "enforce", "presets": ["..."], "actions": { }, // base / fallback policy
64
+ "profiles": {
65
+ "strict": { "presets": ["strict-block"] },
66
+ "internal": { "presets": ["llm-redact"], "actions": { "email": "allow" },
67
+ "modelAllowlist": ["llama3"], "rate": { "requestsPerMinute": 120 } }
68
+ },
69
+ "profileBinding": {
70
+ "byScope": { "team:eng": "internal" },
71
+ "byLabel": { "tier=trusted": "internal" },
72
+ "default": "strict"
73
+ }
74
+ }
75
+ ```
76
+ - profile은 기존 `buildPolicy`를 통해 컴파일된다 (presets + actions, strengthen-only 병합 유지). `modelAllowlist`와 `rate`는 profile별 선택 사항이며, 없으면 base 레벨 값으로 fallback한다.
77
+ - 요청별 결정 순서: **scope 일치 → label 일치 → `profileBinding.default`**. 첫 번째 일치가 우선하며, scope가 label보다 앞선다. `profiles`가 설정된 경우 `profileBinding.default`는 **필수**이며, 가장 제한적인 profile이어야 한다 (미일치/익명 identity에 대한 fail-closed).
78
+ - `auth.provider: none`이거나 `profiles`가 없으면 base `policy`가 변경 없이 적용된다 (완전한 하위 호환성).
79
+ - 구현: `createRuntime`은 시작 시 `{ name → policyEngine }` 맵을 컴파일한다 (모든 profile을 미리 검증). `protectJson(payload, context)`는 `context.policyEngine`을 받아 기본 엔진 대신 사용한다. proxy는 `authenticate` 후 profile을 결정하고, 선택된 엔진을 전체 경로에 전달한다.
80
+
81
+ ### 2.5 Model allowlist
82
+
83
+ - Profile별 `modelAllowlist` (및 base 레벨 `policy.modelAllowlist`): 설정된 경우 요청 body의 `model`이 목록에 없으면 → `403 haechi_model_not_allowed` (audit 기록, 모델 이름 포함 — 모델 이름은 비밀이 아님). 비어 있거나 없으면 모두 허용.
84
+ - body 읽기 **이후** 실행된다 (`model` 필드는 JSON body 안에 있음).
85
+
86
+ ### 2.6 Rate limiting
87
+
88
+ - 인메모리, 프로세스 단위 fixed-window 카운터. 키는 `identity.id` (또는 `"anonymous"`). **문서화된 제한사항:** 재시작 시 초기화되며, 레플리카 간 공유 없음 — 단일 프로세스 self-hosted preview 환경에서 허용 가능한 수준.
89
+ - Profile별 / base `rate.requestsPerMinute`. 한도 초과 시 → `429 haechi_rate_limited` (identity + 한도 포함 audit 기록).
90
+ - body 읽기 **이전** 실행된다 (identity 기반으로 저렴함). throttle된 인증 클라이언트가 대용량 body로 DoS를 가할 수 없도록 한다.
91
+ - LLM **token budget** (tokens-per-window)은 이월 — 모델 토큰 계산이 필요하며; 0.7+ 백로그로 기재.
92
+
93
+ ### 2.7 Proxy 실행 순서 (예약된 계약, 확정)
94
+
95
+ ```
96
+ GET /__haechi/health (항상 무인증; mode만 노출)
97
+ assertRelativeProxyTarget(request.url)
98
+ route classify
99
+ authProvider.authenticate(request) → 거부 시 401 haechi_auth_denied; request stream 미소비
100
+ resolve policy profile from identity
101
+ rate limit by identity → 429 haechi_rate_limited
102
+ body read (bounded by limits.maxRequestBytes)
103
+ model allowlist check → 403 haechi_model_not_allowed
104
+ protect / enforce (selected profile's policyEngine)
105
+ forward
106
+ ```
107
+
108
+ ### 2.8 Audit 추가 사항
109
+
110
+ - 인증 성공 시 PII-safe `identity`를 모든 이벤트에 첨부; 이벤트에는 결정된 `profile` 이름도 포함 (profile 없으면 `null`). 둘 다 비민감 정보.
111
+ - 새로운 decision: `auth_denied` (reason: `no_token` | `invalid_token` | `provider_error`; raw token 없음), `model_not_allowed`, `rate_limited`. 가능한 경우 시도/결정된 identity를 포함하며, 평문 자격 증명은 절대 포함하지 않는다.
112
+
113
+ ## 3. Config 스키마 요약
114
+
115
+ ```json
116
+ "auth": {
117
+ "provider": "none", // none | bearer | external
118
+ "store": ".haechi/auth.json",
119
+ "allowedLabelKeys": ["team", "env", "tier", "role"]
120
+ }
121
+ ```
122
+ `policy.profiles`, `policy.profileBinding`, `policy.modelAllowlist`, `policy.rate` (§2.4–2.6) 추가. 모두 fail-closed로 검증한다 (알 수 없는 provider, profiles 설정 시 `profileBinding.default` 누락, binding의 알 수 없는 profile 이름, 비양수 `rate` 등).
123
+
124
+ ## 4. 명시적 비범위 (0.7+로 이월)
125
+
126
+ - OIDC/JWT provider (`@haechi/auth-oidc`, `@haechi/auth-jwt`) — 0.6은 bearer + external 주입만 포함.
127
+ - Vault/AWS KMS 레퍼런스 어댑터; 외부 append-only audit 싱크; 서명된 릴리스 아티팩트; npm org `@haechi/*`.
128
+ - LLM token-budget limiting; 분산/공유 rate 상태.
129
+ - auth provider의 동적 npm 로딩 (1.0 plugin sandbox).
130
+
131
+ ## 5. 하위 호환성
132
+
133
+ `auth.provider`는 `none`이 기본값이며, `profiles`가 없으면 0.6은 0.5와 완전히 동일하게 동작한다 (identity `null`, 단일 base policy). 유일한 audit 형태 변경은 항상 존재하는 `profile` 필드(identity 도입 방식과 동일) — 문서화되어 있으며, 미공개 소비자 preview에 대한 마이그레이션은 불필요하다.
134
+
135
+ ## 6. 테스트 기준 (구현 시)
136
+
137
+ - bearer: 유효 token → identity; 누락/유효하지 않은 token → 401 `auth_denied`, body 미소비, timing-safe 조회; 폐기된 token 거부.
138
+ - 주입된 external provider → 사용됨; 없으면 fail-closed.
139
+ - profile 결정: scope 일치, label 일치, default fallback; default 누락 시 검증 실패; 서로 다른 profile이 서로 다른 action/allowlist 적용.
140
+ - model allowlist: 허용된 model 통과, 허용되지 않은 model → 403.
141
+ - rate limit: 윈도 내 N개 통과 / N+1 → 429; 윈도 초기화; per-identity 격리; body 이전 적용.
142
+ - auth CLI: `add`는 token을 한 번만 출력; `list`는 token/hash 비노출; `revoke`는 비활성화.
143
+ - `/__haechi/health` 무인증.
144
+ - audit: identity PII-safe (raw token/subject 없음), `profile` 기록됨, chain 유효; `auth_denied`/`model_not_allowed`/`rate_limited` decision 존재.
145
+
146
+ ## 7. 권장 PR 분할 (스택)
147
+
148
+ 1. `authProvider` 계약 + bearer store + `haechi auth` CLI + identity 구성.
149
+ 2. 명명된 policy profile + 요청별 policy engine 선택.
150
+ 3. Model allowlist + rate limiting + proxy 실행 순서 배선.
151
+ 4. 0.6.0 릴리스 컷 (버전, 문서 EN/KO, threat-model/risk-register/api-stability, wiki).
@@ -0,0 +1,151 @@
1
+ # Haechi 0.6 Implementation Scope
2
+
3
+ - Status: Final
4
+ - Date: 2026-06-10
5
+ - Target version: 0.6.0 (after 0.5.0)
6
+ - Type: auth and per-client controls
7
+ - Shipped: 2026-06-10 — PRs #17 (contract + bearer store + CLI), #18 (named profiles), #19 (proxy enforcement)
8
+
9
+ ## 1. Release Goal
10
+
11
+ Implement the `authProvider`/`identity` contracts reserved in 0.4 and turn identity into real per-client controls: built-in bearer authentication, named per-client policy profiles, model allowlisting, and request rate limiting. This makes Haechi safe(r) to put in front of multiple clients/agents on one host.
12
+
13
+ **Scope decision (2026-06-10):** 0.6 is focused on the auth core. The heavier operational items originally grouped under 0.6 — Vault/AWS KMS reference adapter, external append-only audit sink, signed release artifacts, npm org (`@haechi/*`) acquisition — move to **0.7** so each gets its own security design instead of bloating one release.
14
+
15
+ ## 2. Scope
16
+
17
+ ### 2.1 `authProvider` contract (core-owned)
18
+
19
+ - `authenticate(request) → identity | null` (null = deny). `request` is the Node `IncomingMessage`; only headers are available — the body is **not** read yet.
20
+ - Fail-closed: a throwing provider is treated as deny. Injected via `createRuntime(config, { authProvider })`.
21
+ - Selected by `auth.provider`:
22
+ - `none` (default) — no authentication; `identity` stays `null` (byte-identical audit shape to 0.5). Per-client policy resolves to the default profile / base policy.
23
+ - `bearer` — built-in token auth (§2.2).
24
+ - `external` — requires an injected `authProvider`; fail-closed if absent (mirrors `keys.provider: external`). The OIDC/JWT providers remain **0.7+ satellite packages** (`@haechi/auth-oidc`); 0.6 ships no network IdP code.
25
+
26
+ ### 2.2 Built-in bearer auth + token store
27
+
28
+ - Credentials live in a **separate file** `.haechi/auth.json` (mode `0600`), never in `haechi.config.json`:
29
+ ```json
30
+ {
31
+ "version": 1,
32
+ "tokens": [
33
+ { "id": "tok_auth_ab12cd", "tokenHash": "...", "type": "service",
34
+ "scopes": ["team:eng"], "labels": { "env": "prod" },
35
+ "createdAt": "...", "disabled": false }
36
+ ]
37
+ }
38
+ ```
39
+ - `tokenHash` = `HMAC(derive("haechi:auth:token:v1"), token)` — keyed, domain-separated ([[key-management]] discipline), never a bare hash. Lookup uses a timing-safe comparison.
40
+ - Tokens are high-entropy `hae_<base64url(32 bytes)>`. The **plaintext is shown once at creation and never stored.**
41
+ - CLI:
42
+ - `haechi auth add --type user|service|agent [--scope k:v ...] [--label k=v ...]` → mints a token, stores its hash + metadata, prints the plaintext once.
43
+ - `haechi auth list` → ids, type, scopes, labels, createdAt, disabled — never the token or its hash.
44
+ - `haechi auth revoke <id>` → sets `disabled: true`.
45
+ - Label keys are validated against `auth.allowedLabelKeys` (default `["team", "env", "tier", "role"]`); values are length-limited. PII in labels is rejected at `add` time.
46
+
47
+ ### 2.3 Identity construction (PII-safe)
48
+
49
+ On a bearer match, build the reserved `identity` object:
50
+ - `id`: the token record id (opaque).
51
+ - `type`: from the record.
52
+ - `subjectHash`: `HMAC(derive("haechi:identity:hash:v1"), record.id)`; `issuerHash`: HMAC of `"bearer-local"`. Bare SHA-256 of any identifier is prohibited.
53
+ - `provider`: `"bearer"`.
54
+ - `scopes`, `labels`: from the record (already allowlist-validated).
55
+
56
+ The same identity is attached to every audit event for the request (protect events, decisions). `identity` remains `null` under `auth.provider: none`.
57
+
58
+ ### 2.4 Named policy profiles (per-client policy)
59
+
60
+ `policy` gains profiles and a binding map:
61
+ ```json
62
+ "policy": {
63
+ "mode": "enforce", "presets": ["..."], "actions": { }, // base / fallback policy
64
+ "profiles": {
65
+ "strict": { "presets": ["strict-block"] },
66
+ "internal": { "presets": ["llm-redact"], "actions": { "email": "allow" },
67
+ "modelAllowlist": ["llama3"], "rate": { "requestsPerMinute": 120 } }
68
+ },
69
+ "profileBinding": {
70
+ "byScope": { "team:eng": "internal" },
71
+ "byLabel": { "tier=trusted": "internal" },
72
+ "default": "strict"
73
+ }
74
+ }
75
+ ```
76
+ - A profile compiles through the existing `buildPolicy` (presets + actions, strengthen-only merges preserved). `modelAllowlist` and `rate` are optional per-profile, falling back to base-level values.
77
+ - Resolution order per request: **scope match → label match → `profileBinding.default`**. First match wins; scope precedes label. `profileBinding.default` is **required** when `profiles` is set, and should be the most restrictive profile (fail-closed for unmatched/anonymous identities).
78
+ - With `auth.provider: none` or no `profiles`, the base `policy` applies unchanged (full backward compatibility).
79
+ - Implementation: `createRuntime` compiles a `{ name → policyEngine }` map at startup (all profiles validated up front). `protectJson(payload, context)` accepts `context.policyEngine` and uses it over the default. The proxy resolves the profile after `authenticate` and threads the selected engine through.
80
+
81
+ ### 2.5 Model allowlist
82
+
83
+ - Per-profile `modelAllowlist` (and base-level `policy.modelAllowlist`): if set and the request body's `model` is not listed → `403 haechi_model_not_allowed` (audited, model name included — model names are not secret). Empty/absent = allow all.
84
+ - Runs **after** body read (the `model` field lives in the JSON body).
85
+
86
+ ### 2.6 Rate limiting
87
+
88
+ - In-memory, per-process fixed-window counter keyed by `identity.id` (or `"anonymous"`). **Documented limits:** resets on restart, not shared across replicas — acceptable for a single-process self-hosted preview.
89
+ - Per-profile / base `rate.requestsPerMinute`. Over the limit → `429 haechi_rate_limited` (audited with identity + limit).
90
+ - Runs **before body read** (cheap, by identity) so a throttled-but-authenticated client cannot DoS with large bodies.
91
+ - LLM **token budget** (tokens-per-window) is deferred — it requires counting model tokens; noted as 0.7+ backlog.
92
+
93
+ ### 2.7 Proxy execution order (the reserved contract, finalized)
94
+
95
+ ```
96
+ GET /__haechi/health (always unauthenticated; exposes mode only)
97
+ assertRelativeProxyTarget(request.url)
98
+ route classify
99
+ authProvider.authenticate(request) → 401 haechi_auth_denied on deny; request stream NOT consumed
100
+ resolve policy profile from identity
101
+ rate limit by identity → 429 haechi_rate_limited
102
+ body read (bounded by limits.maxRequestBytes)
103
+ model allowlist check → 403 haechi_model_not_allowed
104
+ protect / enforce (selected profile's policyEngine)
105
+ forward
106
+ ```
107
+
108
+ ### 2.8 Audit additions
109
+
110
+ - Successful auth attaches the PII-safe `identity` to every event; events also carry the resolved `profile` name (`null` when no profiles). Both are non-sensitive.
111
+ - New decisions: `auth_denied` (reason: `no_token` | `invalid_token` | `provider_error`; no raw token), `model_not_allowed`, `rate_limited`. All carry the attempted/resolved identity where known, never plaintext credentials.
112
+
113
+ ## 3. Config schema summary
114
+
115
+ ```json
116
+ "auth": {
117
+ "provider": "none", // none | bearer | external
118
+ "store": ".haechi/auth.json",
119
+ "allowedLabelKeys": ["team", "env", "tier", "role"]
120
+ }
121
+ ```
122
+ Plus `policy.profiles`, `policy.profileBinding`, `policy.modelAllowlist`, `policy.rate` (§2.4–2.6). All validated fail-closed (unknown provider, missing `profileBinding.default` when profiles set, unknown profile names in bindings, non-positive `rate`, etc.).
123
+
124
+ ## 4. Explicit non-scope (deferred to 0.7+)
125
+
126
+ - OIDC/JWT providers (`@haechi/auth-oidc`, `@haechi/auth-jwt`) — 0.6 ships bearer + external-injection only.
127
+ - Vault/AWS KMS reference adapter; external append-only audit sink; signed release artifacts; npm org `@haechi/*`.
128
+ - LLM token-budget limiting; distributed/shared rate state.
129
+ - Dynamic npm loading of auth providers (1.0 plugin sandbox).
130
+
131
+ ## 5. Backward compatibility
132
+
133
+ `auth.provider` defaults to `none`; with no `profiles`, 0.6 behaves exactly like 0.5 (identity `null`, single base policy). The only audit-shape change is the always-present `profile` field (mirrors how `identity` was introduced) — documented, no migration needed for an unpublished-consumer preview.
134
+
135
+ ## 6. Test criteria (for implementation)
136
+
137
+ - bearer: valid token → identity; missing/invalid → 401 `auth_denied`, body not consumed, timing-safe lookup; revoked token denied.
138
+ - external provider injected → used; absent → fail-closed.
139
+ - profile resolution: scope match, label match, default fallback; missing default fails validation; different profiles apply different actions/allowlists.
140
+ - model allowlist: allowed passes, disallowed → 403.
141
+ - rate limit: N pass / N+1 → 429 within a window; window reset; per-identity isolation; pre-body enforcement.
142
+ - auth CLI: `add` prints token once; `list` never reveals token/hash; `revoke` disables.
143
+ - `/__haechi/health` unauthenticated.
144
+ - audit: identity PII-safe (no raw token/subject), `profile` recorded, chain valid; `auth_denied`/`model_not_allowed`/`rate_limited` decisions present.
145
+
146
+ ## 7. Suggested PR breakdown (stacked)
147
+
148
+ 1. `authProvider` contract + bearer store + `haechi auth` CLI + identity construction.
149
+ 2. Named policy profiles + per-request policy engine selection.
150
+ 3. Model allowlist + rate limiting + proxy execution-order wiring.
151
+ 4. 0.6.0 release cut (version, docs EN/KO, threat-model/risk-register/api-stability, wiki).
@@ -24,11 +24,11 @@ npm run release:preflight:npm
24
24
 
25
25
  의도된 publish 경로는 GitHub Actions trusted publishing이다: npm이 release workflow를 OIDC로 인증하고 provenance 증명을 자동 생성한다. 공식 npm 요구사항에 따라 GitHub-hosted runner, `id-token: write`, 연결된 workflow에서의 publish가 필요하다.
26
26
 
27
- **현재 상태: trusted publishing 구성 완료, 증명 릴리스 대기.** `haechi@0.3.2`는 로컬 머신에서 패스키 인증과 `--provenance=false`로 배포되어 해당 버전의 provenance 증명이 존재하지 않는다. 활성화 runbook과 진행 상태:
27
+ **현재 상태: trusted publishing 구성 검증 완료.** `haechi@0.3.2`는 로컬 머신에서 패스키 인증과 `--provenance=false`로 배포되어 해당 버전의 provenance 증명이 존재하지 않는다. 활성화 runbook과 진행 상태:
28
28
 
29
29
  1. ✅ npmjs.com에서: package settings → Trusted Publisher → `raeseoklee/haechi` 저장소와 `npm-publish.yml` workflow 연결 (2026-06-10).
30
30
  2. ✅ `.github/workflows/npm-publish.yml` OIDC 인증 전환 (2026-06-10): `NODE_AUTH_TOKEN`과 `registry-url` 제거, runner의 npm CLI를 `>= 11.5.1`로 업그레이드.
31
- 3. 다음 릴리스 `npm view haechi --json`(`dist.attestations`)으로 증명을 확인. OIDC 경로는 아직 실제 publish수행한 적이 없으며, 잘못 구성된 경우 publish 시점에 fail-closed로 실패한다.
31
+ 3. `haechi@0.4.0`으로 검증 완료 (2026-06-10): `npm view haechi --json`에서 SLSA provenance v1 predicate가진 `dist.attestations` 확인. 로컬 패스키로 배포한 `haechi@0.3.2`만 비증명 상태로 남는다.
32
32
 
33
33
  provenance 없이 수행한 publish는 release note에 갭을 명시적으로 기록해야 한다(`CONTRIBUTING.md` 참조).
34
34
 
@@ -24,11 +24,11 @@ Before the first publish, it is normal for `npm view <package> version` to retur
24
24
 
25
25
  The intended publish path is GitHub Actions trusted publishing: npm authenticates the release workflow via OIDC and generates a provenance statement automatically. Per the official npm requirements this needs a GitHub-hosted runner, `id-token: write`, and a publish from the linked workflow.
26
26
 
27
- **Current state: trusted publishing is configured; first attested release pending.** `haechi@0.3.2` was published from a local machine using passkey authentication with `--provenance=false`, so no provenance attestation exists for that version. The enablement runbook and its status:
27
+ **Current state: trusted publishing is configured and verified.** `haechi@0.3.2` was published from a local machine using passkey authentication with `--provenance=false`, so no provenance attestation exists for that version. The enablement runbook and its status:
28
28
 
29
29
  1. ✅ On npmjs.com: package settings → Trusted Publisher → linked the `raeseoklee/haechi` repository and the `npm-publish.yml` workflow (2026-06-10).
30
30
  2. ✅ `.github/workflows/npm-publish.yml` authenticates via OIDC (2026-06-10): `NODE_AUTH_TOKEN` and `registry-url` removed, npm CLI upgraded to `>= 11.5.1` in the runner.
31
- 3. After the next release, verify the attestation with `npm view haechi --json` (`dist.attestations`). The OIDC path has not carried a real publish yet; if misconfigured it fails closed at publish time.
31
+ 3. Verified with `haechi@0.4.0` (2026-06-10): `npm view haechi --json` shows `dist.attestations` with a SLSA provenance v1 predicate. Only `haechi@0.3.2` remains unattested (published via local passkey).
32
32
 
33
33
  Any publish performed without provenance must record the gap explicitly in the release notes (see `CONTRIBUTING.md`).
34
34
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  - 문서 상태: Draft 0.3
4
4
  - 작성일: 2026-06-10
5
- - 기준 버전: 0.4.0
5
+ - 기준 버전: 0.6.0
6
6
  - 기준 브랜치: `main`
7
7
 
8
8
  ## 1. 현재 판단
@@ -127,8 +127,8 @@ base64/인코딩 값 디코딩 검사, query string 검사, audit tail truncatio
127
127
  | 버전 | 목표 | 남은 범위 |
128
128
  |---|---|---|
129
129
  | 0.4.0 ✅ | token round-trip and adoption | 2026-06-10 구현 완료: 요청 스코프 response detokenization, deterministic tokenization(파생 키), `haechi mcp-wrap`, `haechi audit-verify`/`haechi status`, injection detection type(기본 allow), `identity`/`authProvider` 계약 예약. `docs/current/release-0.4-implementation-scope.md` 참조 |
130
- | 0.5.0 | streaming hardening | SSE/NDJSON stream inspection, stream sequence AAD, replay cache, stronger remote deployment guide |
131
- | 0.6.0 | auth and 운영 통제 | built-in bearer auth, client별 policy scope, model allowlist/rate budget, Vault/AWS KMS reference adapter, external append-only audit sink, signed release artifacts, npm org(`@haechi/*`) 확보 |
130
+ | 0.5.0 | streaming hardening | 2026-06-10 출시: bounded cross-frame 버퍼를 사용한 SSE/NDJSON 스트리밍 응답 검사(`streaming.requestMode: inspect`). stream sequence AAD, replay cache, 강화된 원격 배포 가이드는 0.6+으로 이월. `docs/current/release-0.5-implementation-scope.md` 참조 |
131
+ | 0.6.0 | Shipped 2026-06-10 (PRs #17–#19): built-in bearer auth, named policy profiles, model allowlist, request rate limit, PII-safe identity in audit. `docs/current/release-0.6-implementation-scope.md` 참조 |
132
132
  | 0.7.0 | observability | npm workspaces 전환, `@haechi/dashboard` read-only audit viewer (hash chain 무결성 표시, 요약/검색/타임라인) |
133
133
  | 1.0.0 | stable API contract | migration policy, long-term audit schema, plugin sandbox/runtime conformance 및 allowlist/manifest 통과 외부 auth/classifier package 동적 로딩 |
134
134
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  - Status: Draft 0.3
4
4
  - Date: 2026-06-10
5
- - Target version: 0.4.0
5
+ - Target version: 0.6.0
6
6
  - Branch: `main`
7
7
 
8
8
  ## 1. Current Assessment
@@ -127,8 +127,9 @@ All checklist items below were completed for 0.3.2 on 2026-06-10 except the prov
127
127
  | Version | Goal | Remaining scope |
128
128
  |---|---|---|
129
129
  | 0.4.0 ✅ | Token round-trip and adoption | Shipped 2026-06-10: request-scoped response detokenization, deterministic tokenization (derived key), `haechi mcp-wrap`, `haechi audit-verify`/`haechi status`, injection detection type (default allow), `identity`/`authProvider` contracts reserved. See `docs/current/release-0.4-implementation-scope.md` |
130
- | 0.5.0 | Streaming hardening | SSE/NDJSON stream inspection, stream sequence AAD, replay cache, stronger remote deployment guide |
131
- | 0.6.0 | Auth and operational controls | Built-in bearer auth, per-client policy scope, model allowlist/rate budget, Vault/AWS KMS reference adapter, external append-only audit sink, signed release artifacts, npm org (`@haechi/*`) acquisition |
130
+ | 0.5.0 | Streaming hardening | Shipped 2026-06-10: SSE/NDJSON streaming response inspection with bounded cross-frame buffer (`streaming.requestMode: inspect`). Stream sequence AAD, replay cache, stronger remote deployment guide deferred to 0.6+. See `docs/current/release-0.5-implementation-scope.md` |
131
+ | 0.6.0 | Auth and per-client controls | Shipped 2026-06-10 (PRs #17–#19): built-in bearer auth, named policy profiles, model allowlist, request rate limit, PII-safe identity in audit. See `docs/current/release-0.6-implementation-scope.md` |
132
+ | 0.7.0 | Ops hardening and ecosystem | Vault/AWS KMS reference adapter, external append-only audit sink, signed release artifacts, npm org (`@haechi/*`), OIDC satellite, `@haechi/dashboard` |
132
133
  | 0.7.0 | Observability | npm workspaces migration, `@haechi/dashboard` read-only audit viewer (hash chain integrity display, summary/search/timeline) |
133
134
  | 1.0.0 | Stable API contract | Migration policy, long-term audit schema, plugin sandbox/runtime conformance, and dynamic loading of external auth/classifier packages that pass allowlist/manifest |
134
135
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  - 문서 상태: Draft 0.1
4
4
  - 작성일: 2026-06-10
5
- - 기준 버전: 0.4.0
5
+ - 기준 버전: 0.6.0
6
6
 
7
7
  ## 1. 보호 대상
8
8
 
@@ -24,7 +24,7 @@ Haechi가 보호하려는 주요 자산은 다음이다.
24
24
  | CLI local process | 개발자 로컬 신뢰 | dev key 경고, dry-run 기본값 |
25
25
  | HTTP proxy listener | 비신뢰 client 입력 | loopback bind 기본, remote bind 명시 플래그 |
26
26
  | Upstream model/tool server | 비신뢰 또는 부분 신뢰 | request/response protection, uninspectable response fail-closed |
27
- | Streaming response | 현재 비검사 영역 | `stream: true` 기본 차단 |
27
+ | Streaming response | 검사(bounded) 또는 차단 | `inspect` 모드는 bounded cross-frame 버퍼로 SSE/NDJSON을 stream-filter함; `block`(기본값)은 거부 |
28
28
  | MCP stdio peer | 부분 신뢰 | JSON-RPC 2.0 요구, method allowlist |
29
29
  | Local filesystem | 부분 신뢰 | local key/token vault 0600, audit hash chain |
30
30
  | External provider/plugin | 비신뢰 | provider method contract, plugin manifest-only gate |
@@ -34,7 +34,7 @@ Haechi가 보호하려는 주요 자산은 다음이다.
34
34
  | 위협 | 영향 | 현재 통제 |
35
35
  |---|---|---|
36
36
  | 인터넷 노출 proxy | 인증 없는 LLM gateway | non-loopback bind 기본 실패 |
37
- | streaming 우회 | SSE/NDJSON 평문 유출 | streaming request 기본 실패 |
37
+ | streaming 우회 | SSE/NDJSON 평문 유출 | `inspect` 모드는 SSE/NDJSON을 stream-filter함; `block`(기본값)은 거부; `pass-through`는 명시적으로 감사된 opt-out |
38
38
  | Ollama 암묵 streaming 우회 | `stream` 생략 시 NDJSON 평문 유출 | `/api/chat`·`/api/generate`는 `stream: false` 명시 없으면 streaming으로 간주해 기본 차단 |
39
39
  | 비JSON/압축/대용량 응답 | responseProtection 우회 | fail-closed response policy |
40
40
  | token reveal 남용 | tokenized PII 복원 | revealPolicy 기본 disabled, reveal/purge 결정 audit 기록 |
@@ -47,6 +47,8 @@ Haechi가 보호하려는 주요 자산은 다음이다.
47
47
  | 행 걸린 upstream | proxy 연결 고갈 | `limits.upstreamTimeoutMs` 기본 120s, 초과 시 504 fail |
48
48
  | signing/encryption 키 혼용 | key separation 위반 | policy bundle 서명 키를 domain-separated 파생 키로 분리 |
49
49
  | JSON number/object key 은닉 | 카드번호 등 비문자열 leaf 미탐지 | number leaf와 object key도 detection/transform 대상 |
50
+ | 인증 없는 멀티 클라이언트 접근 | 로컬 프로세스가 upstream / token round-trip 경로를 무단 사용 | 선택적 bearer auth (`auth.provider: bearer`); 없거나 잘못된 경우 → 바디 읽기 전 401; identity별 rate limit 및 model allowlist |
51
+ | audit에 원시 credentials/identity 노출 | audit 로그를 통한 token 또는 subject 유출 | Token은 keyed-HMAC 해시로만 저장; identity subject/issuer는 keyed HMAC 처리; `auth_denied` 레코드에 token 미포함 |
50
52
  | token round-trip의 타 토큰 복원 | 클라이언트/요청 간 평문 복구 | detokenization은 opt-in(`detokenizeResponses`)이며 요청 스코프: 같은 요청을 보호하며 발급된 토큰만 복원 |
51
53
  | tool result/응답 내 간접 prompt injection | 심어진 지시문에 의한 agent 조작 | 응답 방향 휴리스틱, 기본 report-only(`injection` action `allow`), 격상은 명시적 정책 선택. 완전 방어 아님 |
52
54
 
@@ -56,7 +58,9 @@ Haechi가 보호하려는 주요 자산은 다음이다.
56
58
 
57
59
  - 운영 KMS/HSM/Vault adapter 자체 제공
58
60
  - internet-facing gateway 인증/인가
59
- - SSE/NDJSON stream inspection
61
+ - `streaming.maxMatchBytes`보다 cross-frame 매칭(스트림 프레임에 걸쳐 분할될 수 있음)
62
+ - `block`이 발동되기 전에 이미 방출된 스트림 바이트의 회수
63
+ - 스트림에서 choice별(`n > 1`) cross-frame 버퍼링(보조 choice는 프레임 내 보호만 적용)
60
64
  - 법적 컴플라이언스 인증
61
65
  - 모델 hallucination, prompt injection 완전 방어
62
66
  - 외부 MCP server의 OAuth/resource binding 검증
@@ -2,7 +2,7 @@
2
2
 
3
3
  - Status: Draft 0.1
4
4
  - Date: 2026-06-10
5
- - Target version: 0.4.0
5
+ - Target version: 0.6.0
6
6
 
7
7
  ## 1. Assets Under Protection
8
8
 
@@ -24,7 +24,7 @@ The primary assets Haechi protects are:
24
24
  | CLI local process | Developer local trust | Dev key warning, dry-run default |
25
25
  | HTTP proxy listener | Untrusted client input | Loopback bind by default, remote bind requires explicit flag |
26
26
  | Upstream model/tool server | Untrusted or partially trusted | Request/response protection, uninspectable response fail-closed |
27
- | Streaming response | Currently uninspected | `stream: true` blocked by default |
27
+ | Streaming response | Inspected (bounded) or blocked | `inspect` stream-filters SSE/NDJSON with a bounded cross-frame buffer; `block` (default) refuses |
28
28
  | MCP stdio peer | Partially trusted | JSON-RPC 2.0 required, method allowlist |
29
29
  | Local filesystem | Partially trusted | Local key/token vault at 0600, audit hash chain |
30
30
  | External provider/plugin | Untrusted | Provider method contract, plugin manifest-only gate |
@@ -34,7 +34,7 @@ The primary assets Haechi protects are:
34
34
  | Threat | Impact | Current Control |
35
35
  |---|---|---|
36
36
  | Internet-exposed proxy | Unauthenticated LLM gateway | Non-loopback bind fails by default |
37
- | Streaming bypass | SSE/NDJSON plaintext leak | Streaming requests fail by default |
37
+ | Streaming bypass | SSE/NDJSON plaintext leak | `inspect` mode stream-filters SSE/NDJSON; `block` (default) refuses; `pass-through` is an explicit audited opt-out |
38
38
  | Ollama implicit streaming bypass | NDJSON plaintext leak when `stream` is omitted | `/api/chat` and `/api/generate` are treated as streaming unless `stream: false` is explicit; blocked by default |
39
39
  | Non-JSON / compressed / oversized response | responseProtection bypass | Fail-closed response policy |
40
40
  | Token reveal abuse | Restoration of tokenized PII | `revealPolicy` disabled by default; reveal/purge decisions recorded in audit |
@@ -47,6 +47,8 @@ The primary assets Haechi protects are:
47
47
  | Hung upstream | Proxy connection exhaustion | `limits.upstreamTimeoutMs` default 120 s; 504 fail on timeout |
48
48
  | Signing/encryption key conflation | Key separation violation | Policy bundle signing key isolated as a domain-separated derived key |
49
49
  | JSON number / object key concealment | Undetected non-string leaves such as card numbers | Number leaves and object keys included in detection/transform scope |
50
+ | Unauthenticated multi-client access | Any local process uses the upstream / token round-trip | Optional bearer auth (`auth.provider: bearer`); missing/invalid → 401 before body read; per-identity rate limit and model allowlist |
51
+ | Raw credentials/identity in audit | Token or subject leak through the audit log | Tokens stored only as keyed-HMAC hashes; identity subject/issuer are keyed HMAC; `auth_denied` records no token |
50
52
  | Token round-trip restoring foreign tokens | Cross-client/request plaintext recovery | Detokenization is opt-in (`detokenizeResponses`) and request-scoped: only tokens issued while protecting the same request are restored |
51
53
  | Indirect prompt injection in tool results/responses | Agent manipulation via planted instructions | Response-direction heuristics, report-only by default (`injection` action `allow`); escalation is an explicit policy choice. Not a complete defense |
52
54
 
@@ -56,7 +58,9 @@ The primary assets Haechi protects are:
56
58
 
57
59
  - A production KMS/HSM/Vault adapter
58
60
  - Authentication/authorization for internet-facing gateways
59
- - SSE/NDJSON stream inspection
61
+ - Cross-frame matches longer than `streaming.maxMatchBytes` (may still split across stream frames)
62
+ - Retraction of stream bytes already emitted before a `block` fires
63
+ - Per-choice (`n > 1`) cross-frame buffering in streams (secondary choices get within-frame protection only)
60
64
  - Legal compliance certification
61
65
  - Complete defense against model hallucination or prompt injection
62
66
  - OAuth/resource binding validation for external MCP servers
@@ -18,7 +18,9 @@
18
18
  "maxBytes": 1048576
19
19
  },
20
20
  "streaming": {
21
- "requestMode": "block"
21
+ "requestMode": "block",
22
+ "responseMode": "enforce",
23
+ "maxMatchBytes": 256
22
24
  },
23
25
  "limits": {
24
26
  "maxRequestBytes": 1048576,
@@ -59,6 +61,16 @@
59
61
  "privacy": {
60
62
  "profile": null
61
63
  },
64
+ "auth": {
65
+ "provider": "none",
66
+ "store": ".haechi/auth.json",
67
+ "allowedLabelKeys": [
68
+ "team",
69
+ "env",
70
+ "tier",
71
+ "role"
72
+ ]
73
+ },
62
74
  "mcp": {
63
75
  "allowedMethods": [
64
76
  "initialize",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haechi",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Experimental developer preview for self-hosted AI context enforcement across LLM, MCP, vLLM, Ollama, and agent traffic.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -44,7 +44,9 @@
44
44
  "./protocol-adapters": "./packages/protocol-adapters/index.mjs",
45
45
  "./proxy": "./packages/proxy/index.mjs",
46
46
  "./runtime": "./packages/cli/runtime.mjs",
47
- "./token-vault": "./packages/token-vault/index.mjs"
47
+ "./token-vault": "./packages/token-vault/index.mjs",
48
+ "./stream-filter": "./packages/stream-filter/index.mjs",
49
+ "./auth": "./packages/auth/index.mjs"
48
50
  },
49
51
  "files": [
50
52
  "README.md",