haechi 1.3.3 → 1.5.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 CHANGED
@@ -8,7 +8,7 @@
8
8
  [![CI](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml/badge.svg)](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml)
9
9
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
10
10
  [![node](https://img.shields.io/node/v/haechi)](https://nodejs.org)
11
- [![status](https://img.shields.io/badge/status-stable%201.2-brightgreen)](docs/current/api-stability.md)
11
+ [![status](https://img.shields.io/badge/status-stable%201.3-brightgreen)](docs/current/api-stability.md)
12
12
 
13
13
  [English](README.md) | **한국어**
14
14
 
@@ -16,6 +16,10 @@ Haechi는 LLM·MCP·vLLM·Ollama 및 에이전트 payload가 모델, 도구, 로
16
16
 
17
17
  이름은 분별과 보호를 상징하는 한국의 수호 신수 해치에서 따왔습니다.
18
18
 
19
+ **무엇인가:** 직접 운영하는 로컬 자체 호스팅 **게이트웨이이자 라이브러리**입니다. OpenAI 호환 / MCP / vLLM / Ollama / 에이전트 JSON을 검사하여, PII와 비밀이 모델, 도구, 로그에 도달하기 전에 redact·mask·tokenize·encrypt하거나 차단합니다.
20
+
21
+ **무엇이 *아닌가*:** 즉시 사용 가능한 **운영용 어플라이언스**, 관리형/호스팅 서비스, 컴플라이언스 보장이 아닙니다. 코어에는 운영용 KMS/HSM도, 내장 TLS도, 인터넷 노출 대비 강화도 들어 있지 않습니다 — 네트워크 통제, 인증, 키 custody, TLS 종단 reverse proxy는 **사용자**가 직접 갖춰야 합니다. 배포하기 전에 [Known limitations](#known-limitations)를 참고하십시오.
22
+
19
23
  이 저장소는 로컬 개발, 보안 설계 검토, 자체 호스팅 통합 실험을 위한 것입니다. 컴플라이언스를 보장하지는 않습니다.
20
24
 
21
25
  **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와 독립적으로 버저닝됩니다([위성 패키지](#위성-패키지) 참고).
@@ -29,6 +33,7 @@ Haechi는 LLM·MCP·vLLM·Ollama 및 에이전트 payload가 모델, 도구, 로
29
33
  - `haechi status`: 현재 설정에서 무엇이 보호되고 무엇이 보호되지 않는지 보여 줍니다
30
34
  - `haechi audit-verify`: audit hash chain을 검증하고 head hash를 출력합니다
31
35
  - `haechi mcp-wrap -- <command>`: MCP 서버를 양방향 stdio 보호로 감쌉니다
36
+ - `haechi plugin-keygen` / `plugin-sign` / `plugin-verify`: 서명된 `authProvider` 플러그인을 저작하고 검증합니다(Ed25519 신뢰 게이트)
32
37
 
33
38
  ## 데모
34
39
 
@@ -54,17 +59,32 @@ HAECHI_LIVE_UPSTREAM=http://127.0.0.1:8000 node examples/local-proxy-demo/live-d
54
59
 
55
60
  ## 설치
56
61
 
62
+ ### npm
63
+
57
64
  ```bash
58
- npm install -g haechi
65
+ npm install -g haechi # or: npx haechi init (run without installing)
59
66
  haechi init
60
67
  ```
61
68
 
62
- 설치 없이 실행하려면:
69
+ 배포된 패키지의 공급망을 검증하십시오(`0.3.2` 이후 모든 릴리스에 attestation이 붙습니다):
70
+
71
+ ```bash
72
+ npm audit signatures # npm SLSA provenance attestation
73
+ ```
74
+
75
+ ### Docker (GHCR)
76
+
77
+ 각 릴리스는 **cosign으로 서명된** 이미지를 `ghcr.io/raeseoklee/haechi`(태그 `1.3.3`, `1.3`, `1`, `latest`)에 발행합니다. 검증한 뒤 **TLS 종단 reverse proxy 뒤에서** 실행하십시오(이미지는 `proxy.trustForwardedProto: true`로 `0.0.0.0`에 바인딩합니다):
63
78
 
64
79
  ```bash
65
- npx haechi init
80
+ cosign verify ghcr.io/raeseoklee/haechi:1.3.3 \
81
+ --certificate-identity-regexp '^https://github.com/raeseoklee/haechi/' \
82
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com
83
+ docker run --rm -p 127.0.0.1:11016:11016 ghcr.io/raeseoklee/haechi:1.3.3
66
84
  ```
67
85
 
86
+ 강화된 compose 스택과 day-2 운영은 [`docs/current/operations-runbook.md`](docs/current/operations-runbook.md)를 참고하십시오.
87
+
68
88
  ## 빠른 시작
69
89
 
70
90
  이 저장소를 클론한 뒤:
@@ -291,6 +311,22 @@ Haechi는 로컬 정책 부트스트래핑을 위한 지역별 기본 Privacy Pr
291
311
 
292
312
  `haechi.config.json`에서 `privacy.profile`을 설정하면 집행 전에 해당 프로필의 기본 action이 적용됩니다. 이 프로필은 엔지니어링 기본값이며, 법적 자문이 아닙니다.
293
313
 
314
+ ## Known limitations
315
+
316
+ Haechi는 의도적으로 범위를 좁혔습니다. 아래는 숨기지 않고 솔직하게 밝히는 현재의 실제 한계입니다.
317
+
318
+ - **탐지는 ML이 아니라 정규식 + 검증기입니다.** 규칙은 prefix/charclass/length로 앵커링되고 checksum 검증기(Luhn, KR RRN, IBAN mod-97, national-ID 검사)를 함께 씁니다 — 알려진 형태에는 강한 정밀도를 보이지만, 새롭거나 난독화된 비밀은 놓칠 수 있습니다. `filters.minConfidence` / `filters.allowlist`로 튜닝하십시오. ML/분류기 레이어는 백로그이며 아직 출시되지 않았습니다.
319
+ - **스트리밍 매칭 윈도우에는 한계가 있습니다.** cross-frame PII는 JSON **델타 채널**에서 `streaming.maxMatchBytes`(기본값 256)까지 잡힙니다. **비JSON** SSE/NDJSON 프레임에 걸쳐 나뉜 매치는 프레임 단위로만 검사됩니다(문서화된 잔존 위험).
320
+ - **응답 검사는 보조 방어입니다.** 응답 방향은 기본적으로 bare JSON **number** leaf를 스캔하지 않습니다(inference 서버 메타데이터라 false-positive만 냅니다). 엄격한 위협 모델에서는 `responseProtection.scanNumbers: true`로 활성화하십시오.
321
+ - **MCP `--stderr filter`는 줄 단위입니다.** 완전한 stderr 한 줄씩 보호하므로, 자식이 개행을 넘어 쪼갠 비밀은 잡지 못합니다(앵커링된 정규식은 `\n`을 가로질러 매치할 수 없습니다). 고민감 로컬 도구에는 `--stderr drop`을 쓰십시오.
322
+ - **Audit 꼬리 절단에는 별도 미디어가 필요합니다.** `haechi audit-verify`는 수정, 재정렬, 중간 변조를 탐지합니다. *꼬리* 레코드 삭제는 추가 전용/별도 저장소에 기록된 `audit.anchor`로만 탐지할 수 있습니다.
323
+ - **Rate limiting은 기본적으로 프로세스별입니다.** N개 replica 뒤에서는 내장 limiter가 독립적으로 카운트합니다 — 플릿 전체 예산을 쓰려면 공유 저장소(`haechi-ratelimit-redis` 위성)를 주입하십시오.
324
+ - **플러그인 샌드박스: 기본 `worker_threads` 모드는 capability 샌드박스가 아닙니다**(Ed25519 trust gate로 게이트된 메모리/크래시 격리 + 데이터 최소화입니다). 진정한 커널 강제 봉쇄는 opt-in `process-isolated` 런타임이며, 이는 `--allow-net`을 강제하는 Node를 필요로 합니다.
325
+ - **코어에는 운영용 키 custody가 없습니다.** 로컬 AES-256-GCM 소프트웨어 키 파일은 **개발 전용**입니다. KMS/HSM/Vault 기반 custody에는 `haechi-crypto-kms` 위성을 쓰십시오.
326
+ - **CI 참고:** GHCR 이미지 발행 워크플로의 `docker/*` 액션은 아직 Node 20에서 실행됩니다(GitHub deprecation 경고이며 non-blocking입니다) — pin되어 있고 Node-24 bump이 예정되어 있습니다.
327
+
328
+ **의도적으로 범위 밖 (won't fix):** URL 쿼리 스트링 스캔; 항상 켜진 base64/인코딩 값 디코딩(`filters.decodeAndRescan`을 통한 opt-in만 지원); 대시보드 쓰기 동작(audit 뷰어는 설계상 읽기 전용); OS 수준(seccomp) 플러그인 샌드박싱; 그리고 일체의 컴플라이언스 인증. **Haechi는 컴플라이언스를 보장하지 않습니다.**
329
+
294
330
  ## 보안 노트
295
331
 
296
332
  - 이 프로젝트는 컴플라이언스를 보장하지 않습니다.
@@ -335,3 +371,9 @@ Haechi는 로컬 정책 부트스트래핑을 위한 지역별 기본 Privacy Pr
335
371
  1.0.0은 **첫 stable 릴리스**입니다. strict semver 하의 frozen API 계약을 선언합니다. `package.json`의 `exports` 표면, CLI의 기계 판독 동작, audit event schema(중첩 sub-schema와 `schemaVersion` 포함), config key shape이 모두 major 버전 계약의 일부이며, `tests/api-contract.test.mjs`가 이를 가드하고, 문서화된 deprecation 정책(`HAECHI_DEPRECATION_*` 런타임 경고, 제거는 다음 major에서만)과 공개된 취약점에 대한 in-minor 보안 예외 하나가 이를 규율합니다([`docs/current/api-stability.md`](docs/current/api-stability.md) 참고). 1.0은 또한 dynamic-loading 금지를 **좁게** 해제합니다 — `authProvider` 플러그인에 한해, Ed25519 서명(trust-anchor 전용 키 해석, entry-hash 바인딩, 버전 pin/floor, revocation, 서명 윈도우를 갖춘 비대칭 `node:crypto` 검증)에 capability-gated, `worker_threads` 격리, 완전 감사되는 플러그인 샌드박스를 허용합니다. dependency injection(`createRuntime(config, providers)`)이 기본으로 유지됩니다. **정직한 잔존 위험:** worker는 메모리/크래시 격리와 데이터 최소화일 뿐 capability 샌드박스가 아니므로, 악의적인 *서명된* 플러그인은 여전히 `fs`/`net`을 써서 받은 credential 슬라이스를 유출할 수 있습니다. 따라서 load-bearing 통제는 trust gate이며, 진정한 capability 강제(child-process + Node permission model)는 1.x 목표입니다. 네 개의 `haechi-*` 위성(`haechi-auth-jwt@0.2.1`, `haechi-crypto-kms@0.2.1`, `haechi-dashboard@0.1.2`, `haechi-auth-oidc@0.1.2`)은 pre-1.0으로 유지되고 독립적으로 버저닝하며, `haechi` peer 범위를 `>=0.8.0 <2.0.0`으로 넓혀 core 1.0.0이 그 설치를 깨뜨리지 않게 합니다. `docs/current/release-1.0-implementation-scope.md` 참고.
336
372
 
337
373
  1.1.0은 가장 많이 거론되던 1.0의 정직한 잔존 위험을 **진정한 플러그인 capability 강제**로 닫습니다. 새 opt-in `process-isolated` authProvider 런타임(`auth.plugin.isolation: "process"`)은 서명된 플러그인을 Node 권한 모델(`--permission`, **부여 0**) 하의 자식 프로세스에서 실행합니다 — `data:` URL 로드(파일시스템 권한 없음), `stdio: ['ignore','ignore','ignore','ipc']`, 정화된 env. `--allow-net`을 강제하는 Node에서 커널이 플러그인의 `fs`/`net`/`fetch`/`dns`/`child_process`/`worker`는 물론 `process.binding('tcp_wrap')` 우회까지 거부하므로, 악의적 서명 플러그인은 받은 credential을 유출할 수 없습니다. 네트워크 봉쇄는 커널의 `--allow-net` 거부이며(삭제 가능한 JS 하니스가 아닙니다), 기본값 `netEnforcement: "require-permission"`은 `--allow-net`이 없는 Node에서 **fail closed**(생성 거부)합니다. 커스텀 자격증명 플러그인의 경우, **호스트**가 운영자 선언 키 자료를 SSRF 강화 코어 가드(`haechi/ssrf`)로 가져와 IPC로 주입하므로 플러그인은 URL을 직접 지정하지 않습니다. spawn-storm 서킷 브레이커가 재spawn을 제한합니다. 변경되지 않은 1.0 `worker_threads` 모드가 기본으로 유지되며, `process-isolated`는 additive + opt-in(strict semver 하의 **마이너**)입니다. `docs/current/release-1.1-implementation-scope.md` 참고.
374
+
375
+ 1.2.0은 Reliability Hardening Track(WS1–WS6, 1.1을 보존하는 기본값 뒤에서 additive)입니다. 레이블링된 코퍼스 기반 탐지 정밀도/재현율 벤치마크 + 회귀 게이트; non-suppressible hard-block-types 불변식을 갖춘 `filters.minConfidence` / `filters.allowlist`; NFKC 유니코드 회피 폴딩; 주입 가능한 rate-limiter 시임; 운영성(`/__haechi/live`+`/ready`, 주입 가능한 `/metrics`, 구조화 로그 + 요청별 `correlationId`, graceful drain, env 오버레이, 강화된 Dockerfile/compose); 그리고 proxy TLS / remote-bind 강화에 더해 OWASP-LLM / NIST 통제 매핑 백서를 담았습니다. `docs/current/reliability-hardening-track.md` 참고.
376
+
377
+ 1.3.0은 백엔드와 탐지를 확장합니다. **Anthropic Messages API**와 **Google Gemini API**용 프로토콜 adapter; 클라우드/SaaS provider-key 탐지와 국제 PII(FR/ES/JP/IT/SG/IN/DE/NL national ID, checksum 검증, hard-block-vs-allowlist-clearable 결정은 측정된 collision rate로 결정); proxy 처리량 벤치마크; 그리고 `haechi-ratelimit-redis` 공유 저장소 rate-limiter 위성을 추가합니다. 모두 additive입니다(새 `target.type`/탐지 type/profile *값*, `configVersion`은 `1`로 유지).
378
+
379
+ 1.3.1 → 1.3.3은 보안 교정과 강화 **패치**입니다(API/config 변경 없음). 1.3.1과 1.3.2는 두 차례의 외부 코드 리뷰 라운드를 닫습니다 — proxy 헤더 경계 credential 유출, hex IPv4-mapped IPv6 SSRF, 응답 헤더/스트리밍 경계, 비JSON 스트리밍 검사(1.3.1); proxy upstream-reader 연결 끊김 시 취소, token-vault audit 로그 위생, 플러그인 IPC 응답 경계(1.3.2). 1.3.3은 응답 방향 마커 skip을 강화하고(모델이 비밀을 가짜 `[TOKEN:…]`로 감싸 스캔을 회피할 수 없음) cosign으로 서명된 GHCR 컨테이너 이미지를 추가합니다.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![CI](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml/badge.svg)](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml)
9
9
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
10
10
  [![node](https://img.shields.io/node/v/haechi)](https://nodejs.org)
11
- [![status](https://img.shields.io/badge/status-stable%201.2-brightgreen)](docs/current/api-stability.md)
11
+ [![status](https://img.shields.io/badge/status-stable%201.5-brightgreen)](docs/current/api-stability.md)
12
12
 
13
13
  **English** | [한국어](README.ko.md)
14
14
 
@@ -16,7 +16,11 @@ Haechi is a self-hosted AI context enforcement layer for protecting LLM, MCP, vL
16
16
 
17
17
  The name comes from Haechi, a Korean guardian figure associated with discernment and protection.
18
18
 
19
- This repository is intended for local development, security design review, and self-hosted integration experiments. It is not a compliance guarantee.
19
+ **What it is:** a local, self-hosted **gateway and library you run yourself**. It inspects OpenAI-compatible / MCP / vLLM / Ollama / agent JSON and redacts, masks, tokenizes, encrypts, or blocks PII and secrets before they reach models, tools, or logs.
20
+
21
+ **What it is *not*:** a turnkey **production appliance**, a managed/hosted service, or a compliance guarantee. Core ships no production KMS/HSM, no built-in TLS, and no internet-facing hardening — **you** bring the network controls, authentication, key custody, and a TLS-terminating reverse proxy. See [Known limitations](#known-limitations) before deploying.
22
+
23
+ This repository is intended for local development, security design review, and self-hosted integration. It is not a compliance guarantee.
20
24
 
21
25
  **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
26
 
@@ -29,6 +33,7 @@ The current scope focuses on local adoption:
29
33
  - `haechi status`: show what is and is not protected under the current config
30
34
  - `haechi audit-verify`: verify the audit hash chain and print its head hash
31
35
  - `haechi mcp-wrap -- <command>`: wrap an MCP server with bidirectional stdio protection
36
+ - `haechi plugin-keygen` / `plugin-sign` / `plugin-verify`: author and verify a signed `authProvider` plugin (Ed25519 trust gate)
32
37
 
33
38
  ## Demo
34
39
 
@@ -54,17 +59,32 @@ See [`examples/local-proxy-demo/`](examples/local-proxy-demo/).
54
59
 
55
60
  ## Install
56
61
 
62
+ ### npm
63
+
57
64
  ```bash
58
- npm install -g haechi
65
+ npm install -g haechi # or: npx haechi init (run without installing)
59
66
  haechi init
60
67
  ```
61
68
 
62
- Or run without installing:
69
+ Verify the published package's supply chain (every release after `0.3.2` is attested):
63
70
 
64
71
  ```bash
65
- npx haechi init
72
+ npm audit signatures # npm SLSA provenance attestation
66
73
  ```
67
74
 
75
+ ### Docker (GHCR)
76
+
77
+ Each release publishes a **cosign-signed** image to `ghcr.io/raeseoklee/haechi` (tags `1.3.3`, `1.3`, `1`, `latest`). Verify it, then run **behind a TLS-terminating reverse proxy** (the image binds `0.0.0.0` with `proxy.trustForwardedProto: true`):
78
+
79
+ ```bash
80
+ cosign verify ghcr.io/raeseoklee/haechi:1.3.3 \
81
+ --certificate-identity-regexp '^https://github.com/raeseoklee/haechi/' \
82
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com
83
+ docker run --rm -p 127.0.0.1:11016:11016 ghcr.io/raeseoklee/haechi:1.3.3
84
+ ```
85
+
86
+ See [`docs/current/operations-runbook.md`](docs/current/operations-runbook.md) for the hardened compose stack and day-2 operations.
87
+
68
88
  ## Quickstart
69
89
 
70
90
  From a clone of this repository:
@@ -291,6 +311,22 @@ Haechi includes baseline regional privacy profiles for local policy bootstrappin
291
311
 
292
312
  Set `privacy.profile` in `haechi.config.json` to apply the profile's default actions before enforcement. These profiles are engineering defaults, not legal advice.
293
313
 
314
+ ## Known limitations
315
+
316
+ Haechi is deliberately scoped. These are real, current limitations — listed openly, not hidden:
317
+
318
+ - **Detection is regex + validators, not ML.** Rules are anchored on prefix/charclass/length with checksum validators (Luhn, KR RRN, IBAN mod-97, national-ID checks) — strong precision on known shapes, but a novel or obfuscated secret can be missed. Tune with `filters.minConfidence` / `filters.allowlist`; an ML/classifier layer is backlog, not shipped.
319
+ - **Streaming match window is bounded.** Cross-frame PII is caught on the JSON **delta channel** up to `streaming.maxMatchBytes` (default 256). A match split across **non-JSON** SSE/NDJSON frames is inspected per-frame only (documented residual).
320
+ - **Response inspection is a secondary defense.** The response direction does not scan bare JSON **number** leaves by default (they are inference-server metadata and only false-positive); opt in with `responseProtection.scanNumbers: true` for a strict threat model.
321
+ - **MCP `--stderr filter` is line-oriented.** It protects each complete stderr line; a secret a child splits across a newline is not caught (an anchored regex cannot match across `\n`). Use `--stderr drop` for high-sensitivity local tools.
322
+ - **Audit tail-truncation needs separate media.** `haechi audit-verify` detects modification, reordering, and middle tampering; deletion of *trailing* records is only detectable via `audit.anchor` written to append-only / separate storage.
323
+ - **Rate limiting is per-process by default.** Behind N replicas the built-in limiter counts independently — inject a shared store (the `haechi-ratelimit-redis` satellite) for a fleet-wide budget.
324
+ - **Plugin sandbox: the default `worker_threads` mode is not a capability sandbox** (it is memory/crash isolation + data-minimization, gated by the Ed25519 trust gate). Real kernel-enforced containment is the opt-in `process-isolated` runtime, which requires a Node that enforces `--allow-net`.
325
+ - **No production key custody in core.** The local AES-256-GCM software-key file is **dev-only**; use the `haechi-crypto-kms` satellite for KMS/HSM/Vault-backed custody.
326
+ - **CI note:** the GHCR image-publish workflow's `docker/*` actions still run on Node 20 (a GitHub deprecation warning, non-blocking) — pinned and scheduled for a Node-24 bump.
327
+
328
+ **Deliberately out of scope (won't fix):** URL query-string scanning; always-on base64/encoded-value decoding (opt-in only via `filters.decodeAndRescan`); dashboard write actions (the audit viewer is read-only by design); OS-level (seccomp) plugin sandboxing; and any compliance certification. **Haechi is not a compliance guarantee.**
329
+
294
330
  ## Security Notes
295
331
 
296
332
  - This project is not a compliance guarantee.
@@ -335,3 +371,13 @@ Set `privacy.profile` in `haechi.config.json` to apply the profile's default act
335
371
  1.0.0 is the **first stable release**. It declares a frozen API contract under strict semver: the `package.json` `exports` surface, the CLI's machine-readable behavior, the audit event schema (including its nested sub-schemas and `schemaVersion`), and the config key shape are all part of the major-versioned contract, guarded by `tests/api-contract.test.mjs` and governed by a documented deprecation policy (`HAECHI_DEPRECATION_*` runtime warnings, removal only at the next major) with a single in-minor security exception for disclosed vulnerabilities (see [`docs/current/api-stability.md`](docs/current/api-stability.md)). 1.0 also lifts the dynamic-loading ban **narrowly**, for `authProvider` plugins only: an Ed25519-signed (asymmetric `node:crypto` verification with trust-anchor-only key resolution, entry-hash binding, version pin/floor, revocation, and a signing window), capability-gated, `worker_threads`-isolated, fully audited plugin sandbox. Dependency injection (`createRuntime(config, providers)`) stays the default. **Honest residual:** the worker is memory/crash isolation and data-minimization, not a capability sandbox — a malicious *signed* plugin can still use `fs`/`net` and exfiltrate the credential slice it receives, so the load-bearing control is the trust gate; true capability enforcement (child-process + Node permission model) is a 1.x target. The four `haechi-*` satellites (`haechi-auth-jwt@0.2.1`, `haechi-crypto-kms@0.2.1`, `haechi-dashboard@0.1.2`, `haechi-auth-oidc@0.1.2`) stay pre-1.0, version independently, and widen their `haechi` peer range to `>=0.8.0 <2.0.0` so core 1.0.0 does not break their installs. See `docs/current/release-1.0-implementation-scope.md`.
336
372
 
337
373
  1.1.0 closes the most-cited 1.0 honest residual with **real plugin capability enforcement**: a new opt-in `process-isolated` authProvider runtime (`auth.plugin.isolation: "process"`) runs the signed plugin in a child process under the Node permission model (`--permission`, **zero grants**), loaded from a `data:` URL (no filesystem grant), with `stdio: ['ignore','ignore','ignore','ipc']` and a scrubbed env. On a Node that enforces `--allow-net`, the kernel denies the plugin's `fs`/`net`/`fetch`/`dns`/`child_process`/`worker` *and* the `process.binding('tcp_wrap')` bypass, so a malicious signed plugin cannot exfiltrate the credential it receives. Network containment is the kernel `--allow-net` denial (not a deletable JS harness); the default `netEnforcement: "require-permission"` **fails closed** (refuses to construct) on a Node without `--allow-net`. For a custom-credential plugin, the **host** fetches operator-declared key material through an SSRF-hardened core guard (`haechi/ssrf`) and injects it over the IPC — the plugin never names a URL. A spawn-storm circuit breaker bounds respawns. The unchanged 1.0 `worker_threads` mode stays the default; `process-isolated` is additive and opt-in (a **minor** under strict semver). See `docs/current/release-1.1-implementation-scope.md`.
374
+
375
+ 1.2.0 is the Reliability Hardening Track (WS1–WS6, additive behind 1.1-preserving defaults): a labeled-corpus detection precision/recall benchmark + regression gate; `filters.minConfidence` / `filters.allowlist` with a non-suppressible hard-block-types invariant; NFKC unicode-evasion folding; an injectable rate-limiter seam; operability (`/__haechi/live`+`/ready`, injectable `/metrics`, structured logs + per-request `correlationId`, graceful drain, env overlay, hardened Dockerfile/compose); and proxy TLS / remote-bind hardening plus an OWASP-LLM / NIST control-mapping whitepaper. See `docs/current/reliability-hardening-track.md`.
376
+
377
+ 1.3.0 expands backends and detection: protocol adapters for the **Anthropic Messages API** and **Google Gemini API**; cloud/SaaS provider-key detection and international PII (FR/ES/JP/IT/SG/IN/DE/NL national IDs, checksum-validated, each hard-block-vs-allowlist-clearable decision driven by measured collision rates); a proxy throughput benchmark; and the `haechi-ratelimit-redis` shared-store rate-limiter satellite. All additive (new `target.type`/detection-type/profile *values*, `configVersion` stays `1`).
378
+
379
+ 1.3.1 → 1.3.3 are security-remediation and hardening **patches** (no API/config change). 1.3.1 and 1.3.2 close two external code-review rounds — proxy header-boundary credential leak, hex IPv4-mapped IPv6 SSRF, response-header/streaming bounds, and non-JSON streaming inspection (1.3.1); proxy upstream-reader cancel-on-disconnect, token-vault audit-log hygiene, and plugin IPC reply bounds (1.3.2). 1.3.3 tightens the response-direction marker skip (a model can't wrap a secret in a fake `[TOKEN:…]` to evade scanning) and adds the cosign-signed GHCR container image.
380
+
381
+ 1.4.0 adds the first-party authoring CLI for the signed-plugin trust gate: `haechi plugin-keygen` (Ed25519 keypair — private key `0600`, public key is the trust anchor), `plugin-sign` (signs the exact entry-file bytes; `authProvider` must declare `readsCredentials`), and `plugin-verify` (runs the runtime's verification, fail-closed, with `--allow-capability`). A new [`plugin-signing-and-trust.md`](docs/current/plugin-signing-and-trust.md) runbook covers the keygen → sign → wire `auth.plugin.trustAnchors` → verify → rotate/pin/revoke lifecycle. Additive CLI surface (a **minor** under strict semver); no config change (`configVersion` stays `1`).
382
+
383
+ 1.5.0 begins the **fleet-readiness** track: the audit sink and token vault gain an injectable **store seam** (`createAuditSink({ store })` / `createTokenVault({ store })` + the default `createFileAuditStore`/`createFileTokenStore`), so a shared store can back the sha256 audit hash chain and the token vault across **multiple replicas** — closing the per-process / single-writer limitations called out under [Known limitations](#known-limitations). The file-backed defaults are byte-identical, and `createJsonlAuditSink`/`createLocalTokenVault` stay as back-compat wrappers. A `haechi-store-redis` satellite (the Redis adapters for both seams) is the production consumer. Additive (a **minor**); `configVersion` stays `1`; core stays zero runtime dependency.
@@ -29,10 +29,10 @@
29
29
  | `haechi/runtime` — `createRuntime`, `normalizeConfig` (config shape), `defaultConfig`, `loadConfig`, `writeDefaultConfig`, `isValidPort`, `DEFAULT_CONFIG_PATH` | **FROZEN** |
30
30
  | `haechi/auth` — `authProvider` 계약, `buildIdentity`, `buildExternalIdentity`, `validateLabels`, `createBearerAuthProvider`, token store (`readAuthStore`, `addToken`, `listTokens`, `revokeToken`), `DEFAULT_ALLOWED_LABEL_KEYS` | **FROZEN** |
31
31
  | `haechi/crypto` — `cryptoProvider` 계약, `assertCryptoProviderConformance`, `canonicalize`, `createLocalCryptoProvider`, `initLocalKeyFile` | **FROZEN** |
32
- | `haechi/audit` — audit **event schema** (§2.3), `verifyAuditChain`, `sanitizeAudit`, `createJsonlAuditSink`, `readAuditSummary`, `FORBIDDEN_KEYS` | **FROZEN** |
32
+ | `haechi/audit` — audit **event schema** (§2.3), `verifyAuditChain`, `sanitizeAudit`, `createJsonlAuditSink`, `readAuditSummary`, `FORBIDDEN_KEYS`, 그리고 1.5.0 주입 가능한 store 시임 `createAuditSink`, `createFileAuditStore`, `buildIntegrityRecord` | **FROZEN** |
33
33
  | `haechi/policy` — `buildPolicy`, `createPolicyEngine`, `createPolicyProfiles`, `validatePolicy`, `ACTION_STRENGTH` (action ordering) | **FROZEN** |
34
34
  | `haechi/filter` — `createDefaultFilterEngine`, `detectEntry`, 그리고 **rule/detection shape** | **FROZEN** |
35
- | `haechi/token-vault` — `createLocalTokenVault`, `readVault`, token format, reveal-governance 계약 | **FROZEN** |
35
+ | `haechi/token-vault` — `createLocalTokenVault`, `readVault`, token format, reveal-governance 계약, 그리고 1.5.0 주입 가능한 store 시임 `createTokenVault`, `createFileTokenStore` | **FROZEN** |
36
36
  | `haechi/protocol-adapters` — `createProtocolAdapter`, `knownProtocolAdapters`, adapter classification 계약 | **FROZEN** |
37
37
  | `haechi/plugin` — `validatePluginManifest`, `validatePluginManifestFile`, manifest schema, 1.0 signed-plugin sandbox 표면 | **FROZEN** |
38
38
  | `haechi/proxy` — `createHaechiProxy`, `assertSafeProxyBind`, `DEFAULT_PROXY_PORT` | **FROZEN BEHAVIOR + wire/contract** (사람이 읽는 log/error **텍스트**는 변경 가능) |
@@ -29,10 +29,10 @@ Every `package.json` `exports` subpath and the CLI is classed. There is no silen
29
29
  | `haechi/runtime` — `createRuntime`, `normalizeConfig` (config shape), `defaultConfig`, `loadConfig`, `writeDefaultConfig`, `isValidPort`, `DEFAULT_CONFIG_PATH` | **FROZEN** |
30
30
  | `haechi/auth` — the `authProvider` contract, `buildIdentity`, `buildExternalIdentity`, `validateLabels`, `createBearerAuthProvider`, the token store (`readAuthStore`, `addToken`, `listTokens`, `revokeToken`), `DEFAULT_ALLOWED_LABEL_KEYS` | **FROZEN** |
31
31
  | `haechi/crypto` — the `cryptoProvider` contract, `assertCryptoProviderConformance`, `canonicalize`, `createLocalCryptoProvider`, `initLocalKeyFile` | **FROZEN** |
32
- | `haechi/audit` — the audit **event schema** (§2.3), `verifyAuditChain`, `sanitizeAudit`, `createJsonlAuditSink`, `readAuditSummary`, `FORBIDDEN_KEYS` | **FROZEN** |
32
+ | `haechi/audit` — the audit **event schema** (§2.3), `verifyAuditChain`, `sanitizeAudit`, `createJsonlAuditSink`, `readAuditSummary`, `FORBIDDEN_KEYS`, plus the 1.5.0 injectable store seam `createAuditSink`, `createFileAuditStore`, `buildIntegrityRecord` | **FROZEN** |
33
33
  | `haechi/policy` — `buildPolicy`, `createPolicyEngine`, `createPolicyProfiles`, `validatePolicy`, `ACTION_STRENGTH` (action ordering) | **FROZEN** |
34
34
  | `haechi/filter` — `createDefaultFilterEngine`, `detectEntry`, and the **rule/detection shape** | **FROZEN** |
35
- | `haechi/token-vault` — `createLocalTokenVault`, `readVault`, the token format, and the reveal-governance contract | **FROZEN** |
35
+ | `haechi/token-vault` — `createLocalTokenVault`, `readVault`, the token format, and the reveal-governance contract, plus the 1.5.0 injectable store seam `createTokenVault`, `createFileTokenStore` | **FROZEN** |
36
36
  | `haechi/protocol-adapters` — `createProtocolAdapter`, `knownProtocolAdapters`, and the adapter classification contract | **FROZEN** |
37
37
  | `haechi/plugin` — `validatePluginManifest`, `validatePluginManifestFile`, the manifest schema, and the 1.0 signed-plugin sandbox surface | **FROZEN** |
38
38
  | `haechi/proxy` — `createHaechiProxy`, `assertSafeProxyBind`, `DEFAULT_PROXY_PORT` | **FROZEN BEHAVIOR + wire/contract** (human-readable log/error **text** may change) |
@@ -1,6 +1,6 @@
1
1
  # Haechi `configVersion` & 업그레이드 노트
2
2
 
3
- - 상태: Living document (코어 1.3.x 추적)
3
+ - 상태: Living document (코어 1.5.x 추적)
4
4
 
5
5
  `configVersion`는 `haechi.config.json`(및 `haechi.config.example.json`) 최상위에 찍히는 단일 정수입니다. 향후 호환성을 깨는 설정 스키마 변경이 구체적으로 게이트할 수 있는 **버전 앵커**로서, 다른 Haechi 빌드가 쓴 설정을 조용히 잘못 읽는 일을 막습니다.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Haechi `configVersion` & Upgrade Notes
2
2
 
3
- - Status: Living document (tracks core 1.3.x)
3
+ - Status: Living document (tracks core 1.5.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.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. |
37
+ | `1` | 1.0 – 1.4.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. The 1.4.0 plugin-signing CLI (`plugin-keygen`/`plugin-sign`/`plugin-verify`) is **CLI surface, not config** it adds no config keys. The 1.5.0 store seams (`createAuditSink`/`createTokenVault` + the file-store defaults) are an **injected-provider** surface (wired via `createRuntime(config, providers)`), not config keys. 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.x 추적)
3
+ - 문서 상태: Living document(core 1.5.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
 
@@ -235,7 +235,7 @@ const runtime = createRuntime(config, { metrics });
235
235
 
236
236
  ### `auth.plugin` (signed authProvider sandbox)
237
237
 
238
- `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)를 참고하세요.
238
+ `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)를 참고하세요. 종단 간 서명 + 신뢰 앵커 큐레이션 흐름(`haechi plugin-keygen`/`plugin-sign`/`plugin-verify` CLI)은 [`plugin-signing-and-trust.md`](plugin-signing-and-trust.md)를 참고하세요.
239
239
 
240
240
  | Key | Type / values | Default | Notes |
241
241
  |---|---|---|---|
@@ -1,6 +1,6 @@
1
1
  # Haechi Configuration Reference
2
2
 
3
- - Status: Living document (tracks core 1.3.x)
3
+ - Status: Living document (tracks core 1.5.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
 
@@ -235,7 +235,7 @@ Applies to `mcp-stdio` and `mcp-wrap`.
235
235
 
236
236
  ### `auth.plugin` (signed authProvider sandbox)
237
237
 
238
- Required when `auth.provider: "plugin"`. The sandbox loads a **signed** `authProvider` plugin under a capability-gated, audited runtime. The top-level `plugins.enabled` (default `true`) is a kill-switch — `false` refuses to construct any plugin. Dynamic loading is opt-in; the default is dependency injection. See `docs/current/release-1.0-implementation-scope.md` (worker) and `release-1.1-implementation-scope.md` (process).
238
+ Required when `auth.provider: "plugin"`. The sandbox loads a **signed** `authProvider` plugin under a capability-gated, audited runtime. The top-level `plugins.enabled` (default `true`) is a kill-switch — `false` refuses to construct any plugin. Dynamic loading is opt-in; the default is dependency injection. See `docs/current/release-1.0-implementation-scope.md` (worker) and `release-1.1-implementation-scope.md` (process). For the end-to-end signing + trust-anchor curation flow (the `haechi plugin-keygen`/`plugin-sign`/`plugin-verify` CLI), see [`plugin-signing-and-trust.md`](plugin-signing-and-trust.md).
239
239
 
240
240
  | Key | Type / values | Default | Notes |
241
241
  |---|---|---|---|
@@ -1,6 +1,6 @@
1
1
  # Haechi 운영 런북 (Day-2)
2
2
 
3
- - 상태: Living document (코어 1.3.x 추적)
3
+ - 상태: Living document (코어 1.5.x 추적)
4
4
 
5
5
  Haechi를 프로덕션에서 운영하기 위한 실무 가이드입니다: 배포, 환경변수 오버레이를 통한 설정, health/readiness/metrics 모니터링, 우아한 종료, 백프레셔 튜닝, 그리고 해시 체인을 깨지 않는 audit 로그 회전입니다.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Haechi Operations Runbook (Day-2)
2
2
 
3
- - Status: Living document (tracks core 1.3.x)
3
+ - Status: Living document (tracks core 1.5.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,
@@ -0,0 +1,143 @@
1
+ # 플러그인 서명 & 신뢰 앵커 큐레이션
2
+
3
+ - 상태: Living document (코어 1.5.x 추적)
4
+ - 날짜: 2026-06-17
5
+
6
+ Haechi의 기본은 **dependency injection**입니다 — `createRuntime(config, providers)`에
7
+ `authProvider`를 전달하며, 어떤 코드도 동적으로 로드되지 않습니다. 서명된 플러그인
8
+ 샌드박스(`auth.provider: "plugin"`)는 그 **opt-in** 예외입니다: 운영자가 자신이 통제하는
9
+ 키로 서명하고 그 키를 **신뢰 앵커(trust anchor)**로 allowlist한 경우에만 서드파티
10
+ `authProvider`를 로드합니다. 이 핵심 통제는 그 신뢰 게이트(Ed25519 서명 + 운영자
11
+ allowlist + pin/floor/revocation)이지 샌드박스 격리가 아닙니다 —
12
+ [`api-stability.md`](api-stability.md)와 [위협 모델](threat-model.md)을 참고하십시오.
13
+
14
+ 이 런북은 `haechi plugin-*` CLI를 사용한 종단 간 저작 + 큐레이션 흐름입니다. 전체
15
+ `auth.plugin.*` 키 레퍼런스는
16
+ [`configuration.md`](configuration.md#authplugin-signed-authprovider-sandbox)를
17
+ 참고하십시오.
18
+
19
+ ## 1. 서명 키쌍 생성
20
+
21
+ ```bash
22
+ haechi plugin-keygen --key-id acme-signer --out-dir ./keys
23
+ ```
24
+
25
+ - `./keys/acme-signer.key`를 기록합니다 — **개인** 서명 키(PKCS8 PEM, 모드 `0600`).
26
+ 오프라인 / 본인의 비밀 저장소에 보관하십시오; Haechi는 런타임에 이를 절대 읽지 않으며
27
+ 게이트웨이 호스트에 둘 필요도 없습니다.
28
+ - `./keys/acme-signer.pub`를 기록합니다 — **공개** 키(SPKI PEM). 운영자에게 전달하는
29
+ **신뢰 앵커**이며, 커밋/배포해도 안전합니다.
30
+ - JSON 출력은 경로와 공개 PEM만 담습니다 — 개인 키 자료는 **절대** 담지 않습니다.
31
+
32
+ 안정적이고 의미 있는 `keyId`를 사용하십시오(설정과 audit 로그에서 앵커를 라벨링합니다).
33
+ 하나의 서명 키로 여러 플러그인에 서명할 수 있습니다.
34
+
35
+ ## 2. 플러그인 서명
36
+
37
+ **정확한** entry 파일 바이트에 서명하십시오 — 서명은 `sha256(entry bytes)`에 바인딩되므로,
38
+ 이후 플러그인 소스에 어떤 편집을 가하든 무효화됩니다.
39
+
40
+ ```bash
41
+ haechi plugin-sign ./acme-auth.mjs \
42
+ --key ./keys/acme-signer.key \
43
+ --signer-key-id acme-signer \
44
+ --plugin-id acme-auth \
45
+ --kind authProvider \
46
+ --plugin-version 1.0.0 \
47
+ --core-range ">=1.0.0 <2.0.0" \
48
+ --capabilities '{"readsCredentials":true}' \
49
+ --out acme-auth.signed.json
50
+ ```
51
+
52
+ - `authProvider` 플러그인은 **반드시** `readsCredentials: true`를 선언해야 합니다(선언하지
53
+ 않으면 코어가 거부합니다). `--capabilities`는 JSON 파일을 읽는 `@path` 형식도 받습니다.
54
+ - 개인 키는 `--key` **파일**에서 읽으며, 커맨드 라인에서는 절대 읽지 않습니다(argv의 키는
55
+ 셸 히스토리와 프로세스 테이블로 누출됩니다).
56
+ - 선택적 `--not-before` / `--not-after`(epoch ms)는 서명 윈도우를 한정합니다.
57
+ - 서명된 envelope `{ payload, signerKeyId, alg, signature }`를 `--out`(기본
58
+ `<pluginId>.signed.json`)에 기록합니다.
59
+
60
+ ## 3. 신뢰하기 전에 검증
61
+
62
+ `plugin-verify`는 런타임이 로드 시점에 수행하는 검증과 **동일한** 검증을 실행하므로,
63
+ envelope를 연결하기 전에 그것이 정상인지 확인할 수 있습니다. 이는 **fail closed**입니다: 어떤
64
+ 거부든 안정적인 `PluginLoadError` 사유(게이트 신호)와 함께 non-zero로 종료합니다; 잘못된
65
+ envelope에 대해 `valid:true`를 절대 출력하지 않습니다.
66
+
67
+ ```bash
68
+ haechi plugin-verify acme-auth.signed.json \
69
+ --entry ./acme-auth.mjs \
70
+ --anchor ./keys/acme-signer.pub \
71
+ --allow-capability readsCredentials \
72
+ --core-version 1.3.3
73
+ ```
74
+
75
+ - `--allow-capability <name>`(반복 가능)는 verifier의 capability allowlist입니다.
76
+ `authProvider`를 검증하려면 **필수**입니다(그 필수 `readsCredentials`는 기본적으로
77
+ allowlist되지 않습니다) — 없으면 fail-closed `capability-not-allowlisted`를 받습니다.
78
+ - 앵커는 명시적 `--anchor <pub.pem>`(`--anchor-key-id`와 함께, 기본은 envelope의
79
+ `signerKeyId`)에서 해석하거나, 실행 중인 설정에서
80
+ `--config haechi.config.json`(`auth.plugin.trustAnchors`를 읽음)으로 해석합니다.
81
+ - `--pin <entrySha256>`와 `--core-version <v>`는 pin / range 검사를 실행합니다.
82
+
83
+ 흔한 거부 사유: `tampered-entry`(서명 후 entry 편집), `invalid-signature`(잘못된 키 /
84
+ 변형된 서명), `unknown-signer`(앵커가 allowlist되지 않음), `alg-not-ed25519`,
85
+ `expired-window`, `below-version-floor`, `revoked`, `pin-mismatch`,
86
+ `capability-not-allowlisted`.
87
+
88
+ ## 4. 신뢰 앵커를 설정에 연결
89
+
90
+ **공개** 키를 신뢰 앵커로 붙여 넣고 플러그인이 필요로 하는 capability만(그 이상은 안 됨)
91
+ 정확히 allowlist하십시오:
92
+
93
+ ```jsonc
94
+ {
95
+ "auth": {
96
+ "provider": "plugin",
97
+ "plugin": {
98
+ "manifestPath": "acme-auth.signed.json",
99
+ "trustAnchors": [
100
+ { "keyId": "acme-signer", "publicKey": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----\n" }
101
+ ],
102
+ "allowCapabilities": ["readsCredentials"],
103
+ "isolation": "process"
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ - `trustAnchors`는 위의 `{keyId, publicKey}` 배열 형식 또는 `{ keyId: publicKey }` 맵을
110
+ 받습니다. 키 해석은 **trust-anchor 전용**입니다 — 여기에 나열되지 않은 서명 키는
111
+ `unknown-signer`이며 fail-closed입니다.
112
+ - 가능한 곳에서는 기본 `worker`보다 `isolation: "process"`(커널 강제 capability 거부;
113
+ `--allow-net`을 강제하는 Node 필요)를 선호하십시오.
114
+ - `plugins.enabled: false`는 어떤 플러그인 생성도 거부하는 전역 kill-switch입니다.
115
+
116
+ ## 5. Rotate, pin, revoke (큐레이션 라이프사이클)
117
+
118
+ 신뢰 앵커는 운영자가 소유합니다 — 의도적으로 큐레이션하십시오:
119
+
120
+ - **서명 키 rotate.** 새 키를 `plugin-keygen`하고, 그 키로 플러그인을 다시 서명하고, 기존
121
+ 앵커와 함께 새 앵커를 `trustAnchors`에 **추가**하십시오. 배포된 모든 플러그인을 다시 서명한
122
+ 뒤에 기존 앵커를 제거하십시오. 살아 있는 envelope가 여전히 의존하는 앵커를 조용히
123
+ 떨어뜨리지 마십시오(그것은 fail-closed 장애입니다).
124
+ - **정확한 빌드를 pin.** `auth.plugin.pin: { version?, entrySha256?, manifestSha256? }`은
125
+ pin된 빌드 외에는 모두 거부합니다 — 악성 업데이트나 rollback에 대한 방어입니다.
126
+ `plugin-sign`이 출력한 `entrySha256`을 사용하십시오.
127
+ - **버전 floor 설정.** `auth.plugin.versionFloor: { "<pluginId>": "<min>" }`은 floor
128
+ 미만의 어떤 버전도 거부합니다(anti-rollback). 정확한 빌드를 pin하지 않고도 동작합니다.
129
+ - **Revoke.** `auth.plugin.revoked: { signerKeyIds?: [...], entrySha256?: [...] }`은
130
+ 손상된 서명 키나 특정 불량 빌드를 denylist합니다; revocation은 로드 시점에 fail-closed입니다.
131
+ Revocation은 **다음 로드**(재시작, 또는 살아 있는 플러그인을 강제로 떨어뜨리는 kill-switch)에
132
+ 적용됩니다 — 실시간 revocation 피드는 향후 작업입니다(P1-SEC-025 residual).
133
+
134
+ ## 6. 운영자 체크리스트
135
+
136
+ - [ ] 개인 서명 키는 오프라인 / 비밀 저장소에 보관하고, 게이트웨이 호스트에는 절대 두지 않음.
137
+ - [ ] `trustAnchors`에는 공개 키만 있음; `allowCapabilities`는 최소 집합임.
138
+ - [ ] 배포 전 `plugin-verify`(일치하는 `--core-version`)로 envelope를 검증함.
139
+ - [ ] rotation 계획(add-new-then-remove-old)이 존재하고 프로덕션에는 `pin`/`versionFloor`가 설정됨.
140
+ - [ ] 가능한 곳에서는 `--allow-net`을 강제하는 Node에서 `isolation: "process"`.
141
+
142
+ 참고: [`configuration.md`](configuration.md#authplugin-signed-authprovider-sandbox),
143
+ [`threat-model.md`](threat-model.md), [`api-stability.md`](api-stability.md).
@@ -0,0 +1,148 @@
1
+ # Plugin Signing & Trust-Anchor Curation
2
+
3
+ - Status: Living document (tracks core 1.5.x)
4
+ - Date: 2026-06-17
5
+
6
+ Haechi's default is **dependency injection** — you pass an `authProvider` to
7
+ `createRuntime(config, providers)` and no code is loaded dynamically. The signed-
8
+ plugin sandbox (`auth.provider: "plugin"`) is the **opt-in** exception: it loads a
9
+ third-party `authProvider` only when the operator has signed it with a key they
10
+ control and allowlisted that key as a **trust anchor**. The load-bearing control
11
+ is that trust gate (Ed25519 signature + operator allowlist + pin/floor/revocation),
12
+ not the sandbox isolation — see [`api-stability.md`](api-stability.md) and the
13
+ [threat model](threat-model.md).
14
+
15
+ This runbook is the end-to-end authoring + curation flow using the `haechi
16
+ plugin-*` CLI. For the full `auth.plugin.*` key reference see
17
+ [`configuration.md`](configuration.md#authplugin-signed-authprovider-sandbox).
18
+
19
+ ## 1. Generate a signing keypair
20
+
21
+ ```bash
22
+ haechi plugin-keygen --key-id acme-signer --out-dir ./keys
23
+ ```
24
+
25
+ - Writes `./keys/acme-signer.key` — the **private** signing key (PKCS8 PEM, mode
26
+ `0600`). Keep it offline / in your own secret store; Haechi never reads it at
27
+ runtime and never needs it on the gateway host.
28
+ - Writes `./keys/acme-signer.pub` — the **public** key (SPKI PEM). This is the
29
+ **trust anchor** you give operators; it is safe to commit/distribute.
30
+ - The JSON output carries only the paths and the public PEM — **never** the
31
+ private key material.
32
+
33
+ Use a stable, meaningful `keyId` (it labels the anchor in config and the audit
34
+ log). One signer key can sign many plugins.
35
+
36
+ ## 2. Sign a plugin
37
+
38
+ Sign the **exact** entry-file bytes — the signature binds `sha256(entry bytes)`,
39
+ so any later edit to the plugin source invalidates it.
40
+
41
+ ```bash
42
+ haechi plugin-sign ./acme-auth.mjs \
43
+ --key ./keys/acme-signer.key \
44
+ --signer-key-id acme-signer \
45
+ --plugin-id acme-auth \
46
+ --kind authProvider \
47
+ --plugin-version 1.0.0 \
48
+ --core-range ">=1.0.0 <2.0.0" \
49
+ --capabilities '{"readsCredentials":true}' \
50
+ --out acme-auth.signed.json
51
+ ```
52
+
53
+ - An `authProvider` plugin **must** declare `readsCredentials: true` (core rejects
54
+ one that does not). `--capabilities` also accepts `@path` to read a JSON file.
55
+ - The private key is read from the `--key` **file**, never the command line (a key
56
+ in argv leaks into shell history and the process table).
57
+ - Optional `--not-before` / `--not-after` (epoch ms) bound a signing window.
58
+ - Writes the signed envelope `{ payload, signerKeyId, alg, signature }` to
59
+ `--out` (default `<pluginId>.signed.json`).
60
+
61
+ ## 3. Verify before you trust it
62
+
63
+ `plugin-verify` runs the **same** verification the runtime does at load, so you
64
+ can confirm an envelope is good before wiring it in. It **fails closed**: any
65
+ refusal exits non-zero with the stable `PluginLoadError` reason (the gate signal);
66
+ it never prints `valid:true` on a bad envelope.
67
+
68
+ ```bash
69
+ haechi plugin-verify acme-auth.signed.json \
70
+ --entry ./acme-auth.mjs \
71
+ --anchor ./keys/acme-signer.pub \
72
+ --allow-capability readsCredentials \
73
+ --core-version 1.3.3
74
+ ```
75
+
76
+ - `--allow-capability <name>` (repeatable) is the verifier's capability allowlist.
77
+ It is **required** to verify an `authProvider` (its mandatory `readsCredentials`
78
+ is not allowlisted by default) — without it you get a fail-closed
79
+ `capability-not-allowlisted`.
80
+ - Resolve anchors from an explicit `--anchor <pub.pem>` (with `--anchor-key-id`,
81
+ default the envelope's `signerKeyId`) **or** from a running config with
82
+ `--config haechi.config.json` (reads `auth.plugin.trustAnchors`).
83
+ - `--pin <entrySha256>` and `--core-version <v>` exercise the pin / range checks.
84
+
85
+ Common refusal reasons: `tampered-entry` (entry edited after signing),
86
+ `invalid-signature` (wrong key / mutated signature), `unknown-signer` (anchor not
87
+ allowlisted), `alg-not-ed25519`, `expired-window`, `below-version-floor`,
88
+ `revoked`, `pin-mismatch`, `capability-not-allowlisted`.
89
+
90
+ ## 4. Wire the trust anchor into config
91
+
92
+ Paste the **public** key as a trust anchor and allowlist exactly the capabilities
93
+ the plugin needs (no more):
94
+
95
+ ```jsonc
96
+ {
97
+ "auth": {
98
+ "provider": "plugin",
99
+ "plugin": {
100
+ "manifestPath": "acme-auth.signed.json",
101
+ "trustAnchors": [
102
+ { "keyId": "acme-signer", "publicKey": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----\n" }
103
+ ],
104
+ "allowCapabilities": ["readsCredentials"],
105
+ "isolation": "process"
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ - `trustAnchors` accepts the array-of-`{keyId, publicKey}` form above or a
112
+ `{ keyId: publicKey }` map. Key resolution is **trust-anchor-only** — a signer
113
+ key not listed here is `unknown-signer`, fail-closed.
114
+ - Prefer `isolation: "process"` (kernel-enforced capability denial; requires a
115
+ Node that enforces `--allow-net`) over the default `worker` where you can.
116
+ - `plugins.enabled: false` is a global kill-switch that refuses to construct any
117
+ plugin.
118
+
119
+ ## 5. Rotate, pin, and revoke (curation lifecycle)
120
+
121
+ The operator owns the trust anchors — curate them deliberately:
122
+
123
+ - **Rotate a signer key.** `plugin-keygen` a new key, re-sign the plugin with it,
124
+ and **add** the new anchor to `trustAnchors` alongside the old one. Once every
125
+ deployed plugin is re-signed, remove the old anchor. Never silently drop an
126
+ anchor that live envelopes still depend on (that is a fail-closed outage).
127
+ - **Pin an exact build.** `auth.plugin.pin: { version?, entrySha256?, manifestSha256? }`
128
+ refuses anything but the pinned build — defense against a malicious update or a
129
+ rollback. Use the `entrySha256` that `plugin-sign` printed.
130
+ - **Set a version floor.** `auth.plugin.versionFloor: { "<pluginId>": "<min>" }`
131
+ refuses any version below the floor (anti-rollback) without pinning an exact
132
+ build.
133
+ - **Revoke.** `auth.plugin.revoked: { signerKeyIds?: [...], entrySha256?: [...] }`
134
+ denylists a compromised signer key or a specific bad build; revocation is
135
+ fail-closed at load. Revocation takes effect at the **next load** (a restart, or
136
+ the kill-switch to force-drop a live plugin) — a live revocation feed is future
137
+ work (P1-SEC-025 residual).
138
+
139
+ ## 6. Operator checklist
140
+
141
+ - [ ] Private signing key stored offline / in a secret store, never on the gateway host.
142
+ - [ ] Only the public key is in `trustAnchors`; `allowCapabilities` is the minimal set.
143
+ - [ ] Envelope verified with `plugin-verify` (matching `--core-version`) before deploy.
144
+ - [ ] A rotation plan exists (add-new-then-remove-old) and `pin`/`versionFloor` are set for production.
145
+ - [ ] `isolation: "process"` on a `--allow-net`-enforcing Node where possible.
146
+
147
+ See also: [`configuration.md`](configuration.md#authplugin-signed-authprovider-sandbox),
148
+ [`threat-model.md`](threat-model.md), [`api-stability.md`](api-stability.md).