haechi 1.0.0 → 1.1.1

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
@@ -1,10 +1,14 @@
1
1
  # Haechi
2
2
 
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/raeseoklee/haechi/main/docs/assets/haechi.jpg" alt="해치 — 게이트를 지키며 디지털 방패로 검문하는 수호 영물" width="820">
5
+ </p>
6
+
3
7
  [![npm](https://img.shields.io/npm/v/haechi)](https://www.npmjs.com/package/haechi)
4
8
  [![CI](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml/badge.svg)](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml)
5
9
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
6
10
  [![node](https://img.shields.io/node/v/haechi)](https://nodejs.org)
7
- [![status](https://img.shields.io/badge/status-stable%201.0-brightgreen)](docs/current/api-stability.md)
11
+ [![status](https://img.shields.io/badge/status-stable%201.1-brightgreen)](docs/current/api-stability.md)
8
12
 
9
13
  [English](README.md) | **한국어**
10
14
 
@@ -52,7 +56,7 @@ npm run demo:report
52
56
 
53
57
  기본 설정은 `dry-run` 모드로 실행된다. 민감한 값을 탐지하고 audit 메타데이터를 기록하지만, 정책 모드를 변경하기 전까지는 아웃바운드 payload를 수정하지 않는다.
54
58
 
55
- `npm run demo:init`은 `haechi.config.json`과 `.haechi/dev.keys.json`을 로컬에 생성한다. 생성된 키 파일은 로컬 개발 전용이다. Haechi 0.3.x는 운영 환경용 KMS/HSM/Vault 키 provider를 포함하지 않는다. 비밀 정보를 포함하지 않는 템플릿은 `haechi.config.example.json`에서 확인할 수 있다.
59
+ `npm run demo:init`은 `haechi.config.json`과 `.haechi/dev.keys.json`을 로컬에 생성한다. 생성된 키 파일은 로컬 개발 전용이다. 코어는 운영 환경용 KMS/HSM/Vault 키 provider를 포함하지 않으며, KMS·Vault 기반 키 custody는 `haechi-crypto-kms` satellite로 제공된다(외부 `cryptoProvider` 계약을 통해 주입). 비밀 정보를 포함하지 않는 템플릿은 `haechi.config.example.json`에서 확인할 수 있다.
56
60
 
57
61
  ## Local Proxy
58
62
 
@@ -60,7 +64,7 @@ npm run demo:report
60
64
  node packages/cli/bin/haechi.mjs proxy --config haechi.config.json
61
65
  ```
62
66
 
63
- 기존 HTTP JSON 클라이언트를 `http://localhost:1016`으로 지정하고, `haechi.config.json`에서 `target.upstream`을 설정한다. 다른 로컬 포트를 사용하려면 설정에서 `proxy.port`를 변경하거나 `--port`를 전달한다.
67
+ 기존 HTTP JSON 클라이언트를 `http://localhost:11016`으로 지정하고, `haechi.config.json`에서 `target.upstream`을 설정한다. 다른 로컬 포트를 사용하려면 설정에서 `proxy.port`를 변경하거나 `--port`를 전달한다.
64
68
 
65
69
  proxy는 기본적으로 loopback에 바인드된다. `0.0.0.0`, `::`, 또는 다른 non-loopback 호스트에 바인딩하려면 `--allow-remote-bind`를 명시적으로 제공해야 한다. 이 플래그는 명시적인 네트워크 접근 통제 하에서만 사용한다.
66
70
 
@@ -72,7 +76,7 @@ upstream 요청은 `limits.upstreamTimeoutMs`(기본값 120000) 이후 타임아
72
76
 
73
77
  ## Local Inference Servers
74
78
 
75
- Haechi 0.3은 OpenAI 호환 서버, vLLM, Ollama, llama.cpp를 위한 프로토콜 adapter 프리셋을 포함한다.
79
+ Haechi OpenAI 호환 서버, vLLM, Ollama, llama.cpp를 위한 프로토콜 adapter 프리셋을 포함한다.
76
80
 
77
81
  ```json
78
82
  {
@@ -92,7 +96,7 @@ Haechi 0.3은 OpenAI 호환 서버, vLLM, Ollama, llama.cpp를 위한 프로토
92
96
  }
93
97
  ```
94
98
 
95
- 그런 다음 OpenAI 호환 클라이언트를 `http://127.0.0.1:1016/v1`으로 지정한다. Ollama 네이티브 API의 경우 `target.adapter: "ollama"`를 사용하고 proxy를 통해 `/api/chat` 또는 `/api/generate`를 호출한다.
99
+ 그런 다음 OpenAI 호환 클라이언트를 `http://127.0.0.1:11016/v1`으로 지정한다. Ollama 네이티브 API의 경우 `target.adapter: "ollama"`를 사용하고 proxy를 통해 `/api/chat` 또는 `/api/generate`를 호출한다.
96
100
 
97
101
  ## 토큰 왕복
98
102
 
@@ -187,7 +191,7 @@ JWT/JWKS 인증과 KMS 기반 key custody는 `haechi-*` 위성 패키지로 제
187
191
  | `mode` / `policy.mode` | `dry-run` | `dry-run`과 `report-only`는 탐지 및 audit만 수행하고, `enforce`는 변환/차단을 적용한다. `policy.mode`가 `mode`보다 우선한다 |
188
192
  | `target.type` / `target.adapter` | `llm-http` / `openai-compatible` | upstream 프로토콜: `openai-compatible`, `vllm-openai`, `ollama`, `llama-cpp`. 알 수 없는 type은 fail-closed로 처리된다 |
189
193
  | `target.upstream` | `http://127.0.0.1:9999` | proxy가 요청을 전달하는 유일한 upstream (절대 URL 요청 대상은 거부된다) |
190
- | `proxy.host` / `proxy.port` | `127.0.0.1` / `1016` | proxy 바인드 주소. 아래의 remote 바인딩 참고 |
194
+ | `proxy.host` / `proxy.port` | `127.0.0.1` / `11016` | proxy 바인드 주소. 아래의 remote 바인딩 참고 |
191
195
  | `responseProtection.enabled` | `false` | upstream JSON 응답을 검사한다. `failureMode: fail-closed`는 비JSON/압축/대용량 응답을 거부한다 |
192
196
  | `responseProtection.maxBytes` | `1048576` | 응답 크기의 상한 — `failureMode: allow` 상태에서도 적용된다 |
193
197
  | `streaming.requestMode` | `block` | `block`은 스트리밍을 501 차단; `inspect`는 SSE/NDJSON 응답을 stream-filter; `pass-through`는 검사 없이 전달(audit 기록). Ollama chat/generate는 `stream: false`가 없으면 스트리밍으로 간주된다 |
@@ -228,7 +232,7 @@ haechi proxy --config haechi.config.json --host 0.0.0.0 --allow-remote-bind
228
232
 
229
233
  **proxy는 아직 클라이언트 인증을 제공하지 않는다** (0.6 계획): 포트에 접근할 수 있는 누구든 upstream과 token round-trip 경로를 사용할 수 있다. `--allow-remote-bind`는 명시적인 네트워크 통제 하에서만 사용한다:
230
234
 
231
- - **컨테이너**: 컨테이너 내에서 `0.0.0.0`으로 바인딩하는 것은 일반적인 패턴이다 — 포트 매핑에서 노출을 제한한다(예: `-p 127.0.0.1:1016:1016`)
235
+ - **컨테이너**: 컨테이너 내에서 `0.0.0.0`으로 바인딩하는 것은 일반적인 패턴이다 — 포트 매핑에서 노출을 제한한다(예: `-p 127.0.0.1:11016:11016`)
232
236
  - **LAN/원격**: 방화벽, VPN(예: Tailscale), 또는 인증 reverse proxy를 앞에 둔다
233
237
 
234
238
  ## Privacy Profiles
@@ -255,7 +259,7 @@ Haechi는 로컬 정책 부트스트래핑을 위한 기본 지역별 Privacy Pr
255
259
  - Audit tail truncation: `audit.anchor.mode: file`을 설정하면(추가 전용/별도 미디어에서) `haechi audit-verify --anchor`가 마지막 anchor 이후 꼬리 레코드 삭제를 탐지한다. 동일한 쓰기 가능 파일시스템에서는 공격자가 두 파일을 함께 잘라낼 수 있다.
256
260
  - Key custody: `keys.provider: external`은 주입된 `cryptoProvider`를 허용한다; `assertCryptoProviderConformance`로 adapter를 검증한다. envelope 암호화 KMS adapter는 `haechi-crypto-kms` satellite(`satellites/crypto-kms/`)가 제공한다.
257
261
  - Release integrity: 배포된 tarball에는 npm provenance attestation이 포함되며, GitHub release asset에는 sigstore attestation과 `SHA256SUMS`가 추가된다(`gh attestation verify`와 `node scripts/release-checksums.mjs --check`로 검증한다).
258
- - 1.0 authProvider 플러그인 샌드박스는 서명된 플러그인을 `worker_threads` worker에서 실행한다. 이는 메모리/크래시 격리와 데이터 최소화(credential 슬라이스만 넘어가고, host가 keyed-HMAC identity를 만든다)이며 capability 샌드박스가 **아니다**: 악의적인 *서명된* 플러그인은 여전히 `fs`/`net`을 사용해 받은 credential을 유출할 수 있다. load-bearing 통제는 trust gate(Ed25519 서명 + 운영자 allowlist + 버전 pin/floor + revocation)다. 기본 배선은 dependency injection(`createRuntime(config, providers)`)으로 유지되며, 진정한 capability 강제(child-process + Node permission model) 1.x 목표다.
262
+ - 1.0 authProvider 플러그인 샌드박스는 서명된 플러그인을 `worker_threads` worker에서 실행한다. 이는 메모리/크래시 격리와 데이터 최소화(credential 슬라이스만 넘어가고, host가 keyed-HMAC identity를 만든다)이며 capability 샌드박스가 **아니다**: 악의적인 *서명된* 플러그인은 여전히 `fs`/`net`을 사용해 받은 credential을 유출할 수 있다. load-bearing 통제는 trust gate(Ed25519 서명 + 운영자 allowlist + 버전 pin/floor + revocation)다. **1.1이 잔존 위험을 닫는다** — opt-in `process-isolated` 런타임(`auth.plugin.isolation: "process"`): 서명된 플러그인을 Node 권한 모델(`--permission`, 부여 0) 하의 자식 프로세스에서 실행하며, fs/net/exec/worker가 커널 거부되고, 모든 stdio가 무시되며, `data:` URL로 로드(fs 권한 없음)된다 진정한 capability 강제. `--allow-net`을 강제하는 Node 필요하며 그렇지 않으면 **fail closed**한다; 변경되지 않은 `worker_threads` 모드가 기본으로 유지된다. 기본 배선은 dependency injection(`createRuntime(config, providers)`)으로 유지된다.
259
263
  - 자체 네트워크 통제와 인증을 앞에 두지 않고 Haechi를 인터넷에 노출된 운영 LLM 게이트웨이로 사용하지 않는다.
260
264
 
261
265
  ## 현재 범위
@@ -283,3 +287,5 @@ Haechi는 로컬 정책 부트스트래핑을 위한 기본 지역별 Privacy Pr
283
287
  0.9.0은 관측성(observability) + 대화형 인증 테마이다: 두 개의 새 위성 — [`haechi-dashboard`](satellites/dashboard/)(audit 로그와 hash chain 상태에 대한 zero-dependency 읽기 전용 `node:http` audit 뷰어; anti-DNS-rebinding Host allowlist, 엄격한 CSP/Trusted Types, fail-closed loopback/remote-bind 가드 포함)와 [`haechi-auth-oidc`](satellites/auth-oidc/)(대시보드의 사람 로그인을 제공하는 대화형 OIDC 세션 브로커 — authorization-code + PKCE + 서버측 세션). 기존 위성도 additive minor를 발행한다: `haechi-auth-jwt@0.2.0`은 재사용 가능한 JWS 검증기(`createJwtVerifier`)를 export하고, `haechi-crypto-kms@0.2.0`은 GCP/Azure/Vault 백엔드를 추가한다. core는 `0.9.0`으로 bump되며, 추가적인 `FORBIDDEN_KEYS` audit 새니타이즈 강화(현재 이벤트 출력은 바뀌지 않는 심층 방어)만 포함한다. `docs/current/release-0.9-implementation-scope.md` 참고.
284
288
 
285
289
  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` 참고.
290
+
291
+ 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` 참고.
package/README.md CHANGED
@@ -1,10 +1,14 @@
1
1
  # Haechi
2
2
 
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/raeseoklee/haechi/main/docs/assets/haechi.jpg" alt="Haechi — a guardian haechi warding a gateway with a digital shield" width="820">
5
+ </p>
6
+
3
7
  [![npm](https://img.shields.io/npm/v/haechi)](https://www.npmjs.com/package/haechi)
4
8
  [![CI](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml/badge.svg)](https://github.com/raeseoklee/haechi/actions/workflows/ci.yml)
5
9
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
6
10
  [![node](https://img.shields.io/node/v/haechi)](https://nodejs.org)
7
- [![status](https://img.shields.io/badge/status-stable%201.0-brightgreen)](docs/current/api-stability.md)
11
+ [![status](https://img.shields.io/badge/status-stable%201.1-brightgreen)](docs/current/api-stability.md)
8
12
 
9
13
  **English** | [한국어](README.ko.md)
10
14
 
@@ -52,7 +56,7 @@ npm run demo:report
52
56
 
53
57
  The default config runs in `dry-run` mode. It detects sensitive values and writes audit metadata, but it does not modify outbound payloads until policy mode is changed.
54
58
 
55
- `npm run demo:init` writes `haechi.config.json` and `.haechi/dev.keys.json` locally. The generated key file is for local development only. Haechi 0.3.x does not include a production KMS/HSM/Vault key provider. A non-secret template is available at `haechi.config.example.json`.
59
+ `npm run demo:init` writes `haechi.config.json` and `.haechi/dev.keys.json` locally. The generated key file is for local development only. Core ships no production KMS/HSM/Vault key provider; KMS- and Vault-backed key custody is available through the `haechi-crypto-kms` satellite, injected via the external `cryptoProvider` contract. A non-secret template is available at `haechi.config.example.json`.
56
60
 
57
61
  ## Local Proxy
58
62
 
@@ -60,7 +64,7 @@ The default config runs in `dry-run` mode. It detects sensitive values and write
60
64
  node packages/cli/bin/haechi.mjs proxy --config haechi.config.json
61
65
  ```
62
66
 
63
- Point an existing HTTP JSON client at `http://localhost:1016` and set `target.upstream` in `haechi.config.json`. Change `proxy.port` in the config or pass `--port` to use a different local port.
67
+ Point an existing HTTP JSON client at `http://localhost:11016` and set `target.upstream` in `haechi.config.json`. Change `proxy.port` in the config or pass `--port` to use a different local port.
64
68
 
65
69
  The proxy binds to loopback by default. Binding to `0.0.0.0`, `::`, or another non-loopback host fails unless `--allow-remote-bind` is provided. Use that flag only behind explicit network access controls.
66
70
 
@@ -72,7 +76,7 @@ Upstream requests time out after `limits.upstreamTimeoutMs` (default 120000) and
72
76
 
73
77
  ## Local Inference Servers
74
78
 
75
- Haechi 0.3 includes protocol adapter presets for OpenAI-compatible servers, vLLM, Ollama, and llama.cpp.
79
+ Haechi includes protocol adapter presets for OpenAI-compatible servers, vLLM, Ollama, and llama.cpp.
76
80
 
77
81
  ```json
78
82
  {
@@ -92,7 +96,7 @@ Haechi 0.3 includes protocol adapter presets for OpenAI-compatible servers, vLLM
92
96
  }
93
97
  ```
94
98
 
95
- Then point an OpenAI-compatible client at `http://127.0.0.1:1016/v1`. For Ollama native APIs, use `target.adapter: "ollama"` and call `/api/chat` or `/api/generate` through the proxy.
99
+ Then point an OpenAI-compatible client at `http://127.0.0.1:11016/v1`. For Ollama native APIs, use `target.adapter: "ollama"` and call `/api/chat` or `/api/generate` through the proxy.
96
100
 
97
101
  ## Token Round-Trip
98
102
 
@@ -187,7 +191,7 @@ The satellites are `node:`-only by default (heavy SDKs are optional peers) and k
187
191
  | `mode` / `policy.mode` | `dry-run` | `dry-run` and `report-only` detect + audit only; `enforce` transforms/blocks. `policy.mode` wins over `mode` |
188
192
  | `target.type` / `target.adapter` | `llm-http` / `openai-compatible` | Upstream protocol: `openai-compatible`, `vllm-openai`, `ollama`, `llama-cpp`. Unknown types fail closed |
189
193
  | `target.upstream` | `http://127.0.0.1:9999` | The only upstream the proxy will forward to (absolute-URL request targets are rejected) |
190
- | `proxy.host` / `proxy.port` | `127.0.0.1` / `1016` | Proxy bind address. See remote binding below |
194
+ | `proxy.host` / `proxy.port` | `127.0.0.1` / `11016` | Proxy bind address. See remote binding below |
191
195
  | `responseProtection.enabled` | `false` | Inspect upstream JSON responses. `failureMode: fail-closed` rejects non-JSON/compressed/oversized responses |
192
196
  | `responseProtection.maxBytes` | `1048576` | Hard response size cap — enforced even in `failureMode: allow` |
193
197
  | `streaming.requestMode` | `block` | `block` 501s streaming; `inspect` stream-filters SSE/NDJSON responses; `pass-through` forwards uninspected (audited). Ollama chat/generate count as streaming unless `stream: false` |
@@ -228,7 +232,7 @@ haechi proxy --config haechi.config.json --host 0.0.0.0 --allow-remote-bind
228
232
 
229
233
  **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:
230
234
 
231
- - **Containers**: binding `0.0.0.0` inside a container is the normal pattern — restrict exposure at the port mapping, e.g. `-p 127.0.0.1:1016:1016`
235
+ - **Containers**: binding `0.0.0.0` inside a container is the normal pattern — restrict exposure at the port mapping, e.g. `-p 127.0.0.1:11016:11016`
232
236
  - **LAN/remote**: put a firewall, VPN (e.g. Tailscale), or an authenticating reverse proxy in front
233
237
 
234
238
  ## Privacy Profiles
@@ -255,7 +259,7 @@ Set `privacy.profile` in `haechi.config.json` to apply the profile's default act
255
259
  - Audit tail truncation: set `audit.anchor.mode: file` (on append-only/separate media) so `haechi audit-verify --anchor` detects deletion of trailing records back to the last anchor. On the same writable filesystem an attacker can truncate both files together.
256
260
  - Key custody: `keys.provider: external` accepts an injected `cryptoProvider`; validate adapters with `assertCryptoProviderConformance`. The `haechi-crypto-kms` satellite (`satellites/crypto-kms/`) provides an envelope-encryption KMS adapter.
257
261
  - Release integrity: published tarballs carry an npm provenance attestation; GitHub release assets add a sigstore attestation and `SHA256SUMS` (verify with `gh attestation verify` and `node scripts/release-checksums.mjs --check`).
258
- - The 1.0 authProvider plugin sandbox runs a signed plugin in a `worker_threads` worker. This is memory/crash isolation and data-minimization (only the credential slice crosses; the host builds the keyed-HMAC identity), **not** a capability sandbox: a malicious *signed* plugin can still use `fs`/`net` and exfiltrate the credential it receives. The load-bearing control is the trust gate (Ed25519 signature + operator allowlist + version pin/floor + revocation). Default wiring stays dependency injection (`createRuntime(config, providers)`); true capability enforcement (child-process + Node permission model) is a 1.x target.
262
+ - The 1.0 authProvider plugin sandbox runs a signed plugin in a `worker_threads` worker. This is memory/crash isolation and data-minimization (only the credential slice crosses; the host builds the keyed-HMAC identity), **not** a capability sandbox: a malicious *signed* plugin can still use `fs`/`net` and exfiltrate the credential it receives. The load-bearing control is the trust gate (Ed25519 signature + operator allowlist + version pin/floor + revocation). **1.1 closes this residual** with an opt-in `process-isolated` runtime (`auth.plugin.isolation: "process"`): the signed plugin runs in a child process under the Node permission model (`--permission`, zero grants) with kernel-denied fs/net/exec/worker, all stdio ignored, and a `data:`-URL load (no fs grant) — real capability enforcement. It requires a Node that enforces `--allow-net` and **fails closed** otherwise; the unchanged `worker_threads` mode stays the default. Default wiring stays dependency injection (`createRuntime(config, providers)`).
259
263
  - Do not expose Haechi as an internet-facing production LLM gateway without your own network controls and authentication in front.
260
264
 
261
265
  ## Current Scope
@@ -283,3 +287,5 @@ Set `privacy.profile` in `haechi.config.json` to apply the profile's default act
283
287
  0.9.0 is the observability + interactive-auth theme: two new satellites — [`haechi-dashboard`](satellites/dashboard/) (a zero-dependency, read-only `node:http` audit viewer over the audit log and its hash-chain status, with an anti-DNS-rebinding Host allowlist, strict CSP/Trusted Types, and fail-closed loopback/remote-bind guards) and [`haechi-auth-oidc`](satellites/auth-oidc/) (an interactive OIDC session broker — authorization-code + PKCE + server-side sessions — that provides the dashboard's human login). Existing satellites also ship additive minors: `haechi-auth-jwt@0.2.0` exports a reusable JWS verifier (`createJwtVerifier`) and `haechi-crypto-kms@0.2.0` adds GCP/Azure/Vault backends. Core bumps to `0.9.0`, carrying only an additive `FORBIDDEN_KEYS` audit-sanitization hardening — defense-in-depth that changes no current event output. See `docs/current/release-0.9-implementation-scope.md`.
284
288
 
285
289
  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`.
290
+
291
+ 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`.
package/SECURITY.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Scope
4
4
 
5
- This repository is an experimental self-hosted security toolkit. It is not production-ready and is not a compliance certification, legal opinion, or assurance report.
5
+ This repository is a self-hosted security toolkit. It is not a compliance certification, legal opinion, or assurance report.
6
6
 
7
7
  Release risk tracking is maintained in `docs/current/risk-register-release-gate.md`. npm release checks must pass `npm run release:preflight`; actual npm publication additionally requires `npm run release:preflight:npm` from an authenticated npm account.
8
8
 
package/docs/README.md CHANGED
@@ -20,7 +20,7 @@ English is the primary documentation language. Korean translations are maintaine
20
20
  - `docs/current/release-0.7-implementation-scope.md`: 0.7 audit anchoring, cryptoProvider contract + reference KMS adapter, signed release artifacts
21
21
  - `docs/current/configuration.md`: full configuration reference (every key, defaults, validation, presets, common setups)
22
22
  - `docs/current/risk-register-release-gate.md`: release-blocking risks, security/operational risk status, npm release gates (0.3.2 baseline)
23
- - `docs/current/threat-model.md`: Haechi 0.3.2 trust boundaries, protected assets, key threats and controls
23
+ - `docs/current/threat-model.md`: Haechi trust boundaries, protected assets, key threats and controls
24
24
  - `docs/current/shared-responsibility.md`: responsibility split between Haechi and users/operators in self-hosted deployments
25
25
  - `docs/current/api-stability.md`: developer preview API stability and migration note criteria
26
26
  - `docs/current/release-process.md`: release preflight, SBOM, npm provenance publish procedure
@@ -11,7 +11,7 @@
11
11
  {
12
12
  "mode": "dry-run",
13
13
  "target": { "type": "llm-http", "adapter": "openai-compatible", "upstream": "http://127.0.0.1:9999" },
14
- "proxy": { "host": "127.0.0.1", "port": 1016 },
14
+ "proxy": { "host": "127.0.0.1", "port": 11016 },
15
15
  "responseProtection": { "enabled": false, "mode": "enforce", "failureMode": "fail-closed", "allowNonJson": false, "allowCompressed": false, "maxBytes": 1048576 },
16
16
  "streaming": { "requestMode": "block" },
17
17
  "limits": { "maxRequestBytes": 1048576, "upstreamTimeoutMs": 120000 },
@@ -44,7 +44,7 @@
44
44
  | 키 | 타입 / 값 | 기본값 | 설명 |
45
45
  |---|---|---|---|
46
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`로 실행 시마다 덮어쓸 수 있다. |
47
+ | `proxy.port` | 정수 0–65535 | `11016` | 리슨 포트(`0` = 임시 포트). `--port`로 실행 시마다 덮어쓸 수 있다. |
48
48
 
49
49
  ## `responseProtection`
50
50
 
@@ -141,10 +141,29 @@ upstream JSON 응답을 검사한다(기본적으로 꺼져 있음 — 모델로
141
141
 
142
142
  | 키 | 타입 / 값 | 기본값 | 설명 |
143
143
  |---|---|---|---|
144
- | `auth.provider` | `none` \| `bearer` \| `external` | `none` | `none` = 인증 없음(identity null). `bearer` = 내장 token auth. `external`은 `createRuntime(config, { authProvider })`를 통해 `authProvider`를 주입해야 한다. |
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
145
  | `auth.store` | 경로 | `.haechi/auth.json` | Bearer token 저장소(모드 `0600`). Token은 keyed-HMAC 해시로만 보관되며, 평문은 `haechi auth add` 실행 시 한 번만 표시된다. |
146
146
  | `auth.allowedLabelKeys` | 문자열 배열 | `["team", "env", "tier", "role"]` | Token이 가질 수 있는 label 키; 값은 길이가 제한되며 PII를 포함하면 안 된다. |
147
147
 
148
+ ### `auth.plugin` (signed authProvider sandbox)
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) 참고.
151
+
152
+ | Key | Type / values | Default | Notes |
153
+ |---|---|---|---|
154
+ | `auth.plugin.manifestPath` | 경로 | — | 서명된 플러그인 매니페스트(`haechi.plugin.json`). |
155
+ | `auth.plugin.trustAnchors` | `[{keyId, publicKey}]` 또는 `{ keyId: publicKey }` | — | 운영자 allowlist된 Ed25519 **공개** 키. 키 해석은 trust-anchor 전용. |
156
+ | `auth.plugin.allowCapabilities` | 문자열 배열 | — | capability 허용 목록; `readsCredentials` 포함 필수. 목록에 없는 요청 capability → 로드 거부. |
157
+ | `auth.plugin.isolation` | `worker` \| `process` | `worker` | `worker` = `worker_threads`(memory/crash 격리, **1.0**). `process` = Node 권한 모델 하 자식으로 **커널 강제** capability 거부(**1.1**); `--allow-net`을 강제하는 Node 필요. |
158
+ | `auth.plugin.timeoutMs` | 양의 정수 | — | call별 timeout; timeout 시 런타임이 자식/worker를 terminate하고 deny. |
159
+ | `auth.plugin.resourceLimits` | `{ maxOldGenerationSizeMb }` | — | **`worker` 전용** — `worker_threads` heap bound. `process`에는 N/A. |
160
+ | `auth.plugin.netEnforcement` | `require-permission` | `require-permission` | **`process` 전용** — 네트워크 봉쇄 정책. `require-permission`은 `--allow-net` 없는 Node에서 **fail closed**(생성 거부). |
161
+ | `auth.plugin.keyMaterial` | `{ url (https), ttlMs?, cooldownMs? }` | unset | **`process` 전용** — **호스트**가 가져와(SSRF 가드 + TTL+cooldown) 커스텀 자격증명 플러그인에 주입하는 선택적 운영자 선언 키 문서. 플러그인은 URL을 명명하지 않음. |
162
+ | `auth.plugin.pin` | `{ version?, entrySha256?, manifestSha256? }` | unset | 정확 일치 pin(악성 업데이트/rollback 방지). |
163
+ | `auth.plugin.revoked` | `{ signerKeyIds?, entrySha256? }` | unset | revocation denylist(로드 시 fail-closed). |
164
+ | `auth.plugin.versionFloor` | `{ <pluginId>: version }` | unset | 플러그인별 최소 버전(rollback 방지). |
165
+ | `auth.plugin.maxPendingCalls` / `maxMessageBytes` | 양의 정수 | `8` / `16384` | 동시성 + wire 한계(초과/oversized → deny). |
166
+
148
167
  ## `policy` profiles & limits
149
168
 
150
169
  기본 `policy` 위에 클라이언트별 통제를 레이어로 추가한다. [Named profiles](#named-profiles) 참고.
@@ -227,7 +246,7 @@ proxy는 CLI 플래그를 명시적으로 전달하지 않으면 loopback이 아
227
246
  haechi proxy --config haechi.config.json --host 0.0.0.0 --allow-remote-bind
228
247
  ```
229
248
 
230
- **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는 아직 클라이언트 인증을 제공하지 않는다**(0.6 계획): 포트에 접근할 수 있는 누구든 upstream과 token round-trip 경로를 사용할 수 있다. `--allow-remote-bind`는 명시적인 네트워크 통제 하에서만 사용해야 한다 — 컨테이너 내에서 `0.0.0.0`으로 바인드하고 host 포트 매핑을 제한하거나(`-p 127.0.0.1:11016:11016`), 방화벽/VPN/인증 reverse proxy 뒤에 두어야 한다.
231
250
 
232
251
  ## 검증 요약
233
252
 
@@ -11,7 +11,7 @@
11
11
  {
12
12
  "mode": "dry-run",
13
13
  "target": { "type": "llm-http", "adapter": "openai-compatible", "upstream": "http://127.0.0.1:9999" },
14
- "proxy": { "host": "127.0.0.1", "port": 1016 },
14
+ "proxy": { "host": "127.0.0.1", "port": 11016 },
15
15
  "responseProtection": { "enabled": false, "mode": "enforce", "failureMode": "fail-closed", "allowNonJson": false, "allowCompressed": false, "maxBytes": 1048576 },
16
16
  "streaming": { "requestMode": "block" },
17
17
  "limits": { "maxRequestBytes": 1048576, "upstreamTimeoutMs": 120000 },
@@ -44,7 +44,7 @@
44
44
  | Key | Type / values | Default | Notes |
45
45
  |---|---|---|---|
46
46
  | `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`. |
47
+ | `proxy.port` | integer 0–65535 | `11016` | Listen port (`0` = ephemeral). Override per-run with `--port`. |
48
48
 
49
49
  ## `responseProtection`
50
50
 
@@ -141,10 +141,29 @@ Applies to `mcp-stdio` and `mcp-wrap`.
141
141
 
142
142
  | Key | Type / values | Default | Notes |
143
143
  |---|---|---|---|
144
- | `auth.provider` | `none` \| `bearer` \| `external` | `none` | `none` = no auth (identity null). `bearer` = built-in token auth. `external` requires injecting an `authProvider` via `createRuntime(config, { authProvider })`. |
144
+ | `auth.provider` | `none` \| `bearer` \| `external` \| `plugin` | `none` | `none` = no auth (identity null). `bearer` = built-in token auth. `external` requires injecting an `authProvider` via `createRuntime(config, { authProvider })`. `plugin` = a signed `authProvider` sandbox (see [`auth.plugin`](#authplugin-signed-authprovider-sandbox)). |
145
145
  | `auth.store` | path | `.haechi/auth.json` | Bearer token store (mode `0600`). Tokens are kept only as keyed-HMAC hashes; the plaintext is shown once by `haechi auth add`. |
146
146
  | `auth.allowedLabelKeys` | string array | `["team", "env", "tier", "role"]` | Label keys a token may carry; values are length-limited and must not contain PII. |
147
147
 
148
+ ### `auth.plugin` (signed authProvider sandbox)
149
+
150
+ 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).
151
+
152
+ | Key | Type / values | Default | Notes |
153
+ |---|---|---|---|
154
+ | `auth.plugin.manifestPath` | path | — | The signed plugin manifest (`haechi.plugin.json`). |
155
+ | `auth.plugin.trustAnchors` | `[{keyId, publicKey}]` or `{ keyId: publicKey }` | — | Operator-allowlisted Ed25519 **public** keys. Key resolution is trust-anchor-only. |
156
+ | `auth.plugin.allowCapabilities` | string array | — | Capability allowlist; must include `readsCredentials`. A requested capability not listed → load refused. |
157
+ | `auth.plugin.isolation` | `worker` \| `process` | `worker` | `worker` = `worker_threads` (memory/crash isolation, **1.0**). `process` = a child under the Node permission model with **kernel-enforced** capability denial (**1.1**); requires a Node that enforces `--allow-net`. |
158
+ | `auth.plugin.timeoutMs` | positive int | — | Per-call timeout; on timeout the runtime terminates the child/worker and denies. |
159
+ | `auth.plugin.resourceLimits` | `{ maxOldGenerationSizeMb }` | — | **`worker` only** — `worker_threads` heap bound. N/A for `process`. |
160
+ | `auth.plugin.netEnforcement` | `require-permission` | `require-permission` | **`process` only** — network-containment policy. `require-permission` **fails closed** (refuses to construct) on a Node without `--allow-net`. |
161
+ | `auth.plugin.keyMaterial` | `{ url (https), ttlMs?, cooldownMs? }` | unset | **`process` only** — optional operator-declared key document the **host** fetches (SSRF-guarded, TTL+cooldown) and injects to a custom-credential plugin. The plugin never names a URL. |
162
+ | `auth.plugin.pin` | `{ version?, entrySha256?, manifestSha256? }` | unset | Exact-match pin (anti malicious-update / rollback). |
163
+ | `auth.plugin.revoked` | `{ signerKeyIds?, entrySha256? }` | unset | Revocation denylists (fail-closed at load). |
164
+ | `auth.plugin.versionFloor` | `{ <pluginId>: version }` | unset | Per-plugin minimum version (anti-rollback). |
165
+ | `auth.plugin.maxPendingCalls` / `maxMessageBytes` | positive int | `8` / `16384` | Concurrency + wire bounds (excess/oversized → deny). |
166
+
148
167
  ## `policy` profiles & limits
149
168
 
150
169
  Per-client controls layered on top of the base `policy`. See [Named profiles](#named-profiles).
@@ -227,7 +246,7 @@ The proxy refuses non-loopback hosts unless the CLI flag is passed explicitly
227
246
  haechi proxy --config haechi.config.json --host 0.0.0.0 --allow-remote-bind
228
247
  ```
229
248
 
230
- **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 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:11016:11016`), or front it with a firewall/VPN/authenticating reverse proxy.
231
250
 
232
251
  ## Validation cheatsheet
233
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 |
@@ -0,0 +1,128 @@
1
+ # Haechi 1.1 구현 범위
2
+
3
+ - 상태: **구현 + 출시 완료** (2026-06-12; PR #54/#55/#56 + 이 릴리스 컷, core 1.0.0 → 1.1.0). 설계는 Node 26 실측을 동반한 3-렌즈 적대적 검토 후 강화됨.
4
+ - 구현 노트(설계 원문 대비 차이):
5
+ - **fail-closed `--allow-net` 기능 탐지**(`netEnforcementSupported` + 기본값 `netEnforcement: "require-permission"`)는 PR3가 아니라 **PR1**에서 출시됨 — 런타임 안전에 본질적이기 때문: Node 22(--allow-net 없음) CI가 이것 없이는 런타임이 net 미봉쇄로 동작함을 입증했고, 이는 이 설계가 거부하는 "봉쇄하는 척" 실패다. 탐지는 동작을 probe한다(`--permission` 자식에서 `net.connect`가 거부되어야 함) — 플래그가 나열됐지만 강제되지 않는 Node에 면역.
6
+ - 승격된 SSRF 가드의 **satellite 재import**(§2.3)는 **연기됨**: `haechi-auth-jwt`/`haechi-auth-oidc`/`haechi-crypto-kms`가 `haechi/ssrf`를 import하게 하면 그들의 `haechi` peer floor가 1.1로 올라가고, 의도적 "교차 패키지 SSRF 결합 금지" 결정(`crypto-kms/ssrf-parity.test.mjs`)을 뒤집는다. 대신 코어 복사본은 코어-대-`auth-jwt` parity 테스트로 정직하게 유지된다; drift는 제거가 아니라 가드됨.
7
+ - 리스크 ID는 **P1-SEC-026/027 → P1-SEC-027/028**로 재번호됨(제안된 P1-SEC-026이 기존 0.9 OIDC 브로커 리스크와 충돌).
8
+ - 날짜: 2026-06-11
9
+ - 대상 버전: 1.1.0 (1.0.0 이후)
10
+ - 유형: 플러그인 샌드박스의 capability **강제(enforcement)** (1.0의 정직한 잔여 위험을 닫음)
11
+
12
+ ## 1. 릴리스 목표
13
+
14
+ 1.1은 1.0 플러그인 샌드박스의 대표적인 **정직한 잔여 위험(honest residual)** 을 닫는다. 1.0은 `node:worker_threads`가 **메모리/크래시 격리일 뿐 capability 샌드박스가 아니다** 라고 명시했다 — 악성 *서명된* 플러그인이 여전히 `fs`/`net`을 사용해 전달받은 자격증명을 유출할 수 있었다. 1.1은 **더 강한 opt-in `process-isolated` 런타임**을 추가한다. 이 런타임은 서명된 `authProvider` 플러그인을 **Node 권한 모델(`--permission`) 하의 자식 프로세스**에서 실행하며, **`--allow-net`을 fail-closed로 요구하는 네트워크 봉쇄**, **모든 stdio 무시**(stdout/stderr/fd 유출 채널 없음), 그리고 플러그인을 **파일시스템 권한이 전혀 없는 `data:` URL에서 로드**한다 — 따라서 악성 서명 플러그인은 **호스트 파일시스템을 읽을 수도, 프로세스를 spawn할 수도, 네트워크에 도달할 수도, 호스트가 볼 수 있는 어떤 sink에 쓸 수도 없고**, 그러므로 **자격증명을 유출할 수 없다**.
15
+
16
+ Draft 0.1에 대한 적대적 검토(Node 26 실측)가 이 설계를 재구성했고, 그 교정 사항은 아래에 반영되어 있다:
17
+
18
+ - **"`node:net`/fetch를 삭제하는" 하니스는 봉쇄가 아니다.** `process.binding('tcp_wrap')`은 살아있는 소켓을 열고, `import('node:net')`은 캐시 삭제와 무관하게 새 builtin을 재해석한다. 따라서 네트워크 봉쇄는 JS 하니스가 아니라 **커널이 강제하는 `--allow-net` 거부**여야 한다. `--allow-net`이 없는 Node(Node 22 LTS에는 없음)에서는 `process-isolated`가 봉쇄하는 척하지 않고 **fail-closed로 동작**한다.
19
+ - **자식 프로세스는 `--allow-net`이 막지 못하는 stdout/stderr/상속 fd 쓰기 채널을 추가한다.** 이들을 명시적으로 닫지 않으면(stdio 무시 + 전용 IPC 채널) 자격증명이 로그로 유출된다.
20
+ - **임시 디렉터리에 `--allow-fs-read`를 주는 것은 TOCTOU + macOS realpath/symlink 실패 + 숨은 번들링 요구를 부른다.** 검증된 바이트를 **`data:` URL**(1.0 worker가 이미 쓰는 방식)에서 로드하면 **fs 권한이 전혀 필요 없고**, TOCTOU/symlink 표면 전체가 사라지며, 자족적 단일 파일 플러그인을 구조적으로 강제한다.
21
+
22
+ **범위 결정(2026-06-11, 메인테이너 확정; 아래 network/mode/credential/scope 선택은 검토로 다듬은 네 가지 권장 답변이다):**
23
+
24
+ 1. **격리:** `process-isolated` = `--permission` 하의 자식 `node` 프로세스로, **기본적으로 아무것도 부여하지 않으며**(fs 없음, child-process 없음, worker 없음, addons 없음, wasi 없음), 플러그인을 `data:` URL에서 로드하고, `stdio: ['ignore','ignore','ignore','ipc']` + 정화된 `env`로 spawn한다.
25
+ 2. **네트워크 = fail-closed `--allow-net`.** 네트워크 봉쇄는 권한 모델의 `--allow-net` 거부이며 **기능 탐지 + fail-closed**이다: 실행 중인 Node가 `--allow-net` 강제를 입증하지 못하면 `process-isolated`는 **생성을 거부**한다(기본값 `netEnforcement: "require-permission"`). 비-봉쇄 best-effort 대체는 오직 명시적 `allow-harness` opt-in 뒤에서, **악성 플러그인을 봉쇄하지 못한다**는 요란한 경고와 함께만 존재한다.
26
+ 3. **자격증명 처리:** **표준 JWT/JWKS** 자격증명은 **호스트**가 감사되는 `createJwtVerifier`(satellite 경로 재사용)를 실행하므로 플러그인이 **필요 없다**; `process-isolated` 플러그인은 플러그인이 직접 파싱해야 하는 **커스텀/불투명 자격증명**용이며, 거기서 플러그인은 원본 자격증명을 보지만 **net + stdio + fs 거부**로 봉쇄된다(유출 불가). 커스텀 플러그인이 필요로 하는 키 자료는 **호스트가 fetch해 주입**한다(플러그인이 URL을 고르지 않음 → 플러그인 주도 SSRF 없음).
27
+ 4. **모드 + 범위:** `process-isolated`는 변경되지 않은 1.0 `worker-isolated`와 **나란히 존재하는 새롭고 더 강한 opt-in** 런타임이다; 1.1은 이 capability 강제 런타임에 **집중**한다. Classifier/crypto 플러그인, 라이브 CRL, 레지스트리는 이후 마이너에 남긴다.
28
+
29
+ 코어는 **런타임 의존성 0**(`node:child_process` + `--permission` + `node:crypto`/`node:dns`)을 유지한다. 1.1은 additive + opt-in이며, 새 모듈 외 유일한 코어 변경은 **SSRF `isBlockedAddress` 가드를 코어의 node:-only 헬퍼로 승격**하는 것이다(§2.3) — 호스트 중개 fetch가 그것을 쓸 수 있도록(코어는 satellite를 import할 수 없음).
30
+
31
+ ## 2. 범위
32
+
33
+ ### 2.1 `process-isolated` authProvider 런타임 (커널 강제 capability, fs 없음, stdio 없음)
34
+
35
+ 새 매니페스트 `runtime: "process-isolated"`(`kind: "authProvider"`용, `worker-isolated`와 나란히). `createProcessIsolatedAuthProvider(options)`는 `authenticate()`를 자식 `node` 프로세스로 프록시하는 호스트측 `authProvider`(frozen 계약)를 반환한다.
36
+
37
+ - **로드 게이트 우선(PR2 게이트, fail-closed, 감사됨):** 엔트리 바이트를 **메모리에서** 두고 `verifySignedPlugin`(`entrySha256` + kind/capabilities/window에 대한 Ed25519, trust-anchor 전용 해석, pin/version-floor/revocation).
38
+ - **`data:` URL로 로드 — fs 권한 없음, TOCTOU 없음.** 자식은 검증된 바이트를 `data:text/javascript;base64,…` URL로 import한다(1.0 worker가 이미 쓰는 메커니즘). 자식은 **`--allow-fs-read`가 전혀 없이** spawn된다 → 호스트 파일시스템을 읽을 수 없다. 이로써 temp-dir / realpath / symlink / TOCTOU 표면이 통째로 사라지고, **자족적 단일 파일 플러그인**(런타임 `import`/`require`로 호스트 파일을 끌어오지 않음)을 구조적으로 요구한다; 로드 게이트는 소스가 정적으로 비-`data:` specifier를 참조하는 엔트리를 추가로 거부한다.
39
+ - **허용 목록 capability만 부여하는 `--permission` spawn:** `process.execPath` + `--permission`, **`--allow-fs-read`/`--allow-fs-write`/`--allow-child-process`/`--allow-worker`/`--allow-addons`/`--allow-wasi` 없음**. `env`는 최소 고정 집합으로 **정화**된다(상속된 호스트 비밀 없음 — `--permission`은 상속 env를 보호하지 않음; env 정화가 한다). `--disable-proto=delete`.
40
+ - **stdio 완전 차단(검토가 드러낸 새롭고 핵심적인 통제):** `stdio: ['ignore','ignore','ignore','ipc']` — **stdout 없음, stderr 없음, 추가 상속 fd 없음**; 유일한 채널은 전용 IPC다. 호스트는 자식 stdout/stderr를 **결코** 전달/로깅/감사하지 않는다(자격증명을 stderr에 쓰는 플러그인은 그렇지 않으면 운영자 로그로 유출된다). `sendHandle`/fd 전달 없음.
41
+ - **JSON-문자열 전용 IPC(structured clone 없음, fd 전달 없음).** `child_process` IPC는 advanced(structured-clone) 직렬화 + 핸들 전달을 지원하는데, 이는 1.0 sanitizer가 막으려 했던 object/proto/transferable 밀반입을 다시 연다. 런타임은 IPC로 **JSON 문자열만** 송수신하며(`serialization: "json"`), correlation-id + null-proto 허용 목록 sanitizer + 호스트측 `buildExternalIdentity`는 1.0 worker 경로와 정확히 동일하다.
42
+ - **단일 점유 + fail-closed 매트릭스**(timeout → kill, `maxPendingCalls`, `maxMessageBytes`, kill-switch)는 §2.4의 프로세스 수명주기 추가와 함께 그대로 이어진다.
43
+ - **로드 시 적합성**은 샌드박스 자식에 대해 `assertAuthProviderConformance`(무작위 벡터)를 실행한다.
44
+
45
+ ### 2.2 네트워크 봉쇄 = fail-closed `--allow-net` (하니스는 봉쇄가 아니다)
46
+
47
+ - **`--allow-net`만이 진짜 네트워크 통제다.** 네트워크가 필요 없는 `process-isolated` 플러그인은 자식이 **`--allow-net` 없이** spawn된다; 이를 강제하는 Node에서 `net.connect`/`fetch`/`dns` → `ERR_ACCESS_DENIED`(커널 강제). 이것이 실제로 자격증명 유출을 막는다.
48
+ - **기능 탐지 + fail-closed, 버전 파싱 없음.** 생성 시 런타임은 `process.allowedNodeEnvironmentFlags.has('--allow-net')`로 `--allow-net` 지원을 탐지하고, **spawn-probe로 한 번 확인**(`node --permission --allow-net -e 0` → exit 0 = 지원, exit 9 = 미지원)한 뒤 런타임 수명 동안 캐시한다. 기본값 **`netEnforcement: "require-permission"`**: 지원이 입증되지 않으면 `createRuntime`/`normalizeConfig`가 **throw**(시작 거부)하며 조용히 격하되지 않는다. 따라서 자격증명 봉쇄 보장은 `--allow-net` Node(그것을 탑재한 버전 이상)를 요구한다; 그것이 없는 Node 22 LTS → fail closed.
49
+ - **하니스는 best-effort 전용이며 그렇게 라벨된다.** *순진한/우발적* egress를 위한 이식성 있는 `allow-harness` opt-in이 존재할 수 있지만, 설계는 문서·감사(`netEnforcement: "harness"` + 시작 **경고**)·위협 모델에서 평이하게 명시한다 — **악성 서명 플러그인을 봉쇄하지 못한다**(`process.binding('tcp_wrap')`와 새 `import('node:net')`이 모두 네트워크에 도달). 추가로 `process.binding`/`internalBinding`을 stub해야 하지만, 그래도 견고하지 않다. 고확신 운영자는 `require-permission`(기본값)을 쓴다.
50
+
51
+ ### 2.3 자격증명 처리 — 호스트측 JWT, 호스트 중개 키 자료, 코어의 SSRF 가드
52
+
53
+ - **표준 JWT/JWKS: 호스트가 검증하고, 어떤 플러그인도 원본 자격증명을 보지 않는다.** 흔한 JWT의 경우 **호스트**가 감사되는 `createJwtVerifier`(satellite 경로)를 실행하므로 `process-isolated` 플러그인은 **불필요**하다 — 호스트 검증기를 직접 쓴다(`auth.provider: "external"`/satellite). 1.1은 원본 JWT를 자식으로 라우팅하지 않는다.
54
+ - **커스텀/불투명 자격증명: 플러그인이 원본을 보지만 egress 거부로 봉쇄된다.** `process-isolated` 플러그인은 플러그인이 파싱해야 하는 비표준 자격증명을 위해 존재한다. 검증을 위해 IPC로 원본 자격증명을 받지만(받아야만 한다), **net + stdio + fs가 모두 거부**되므로 그것을 **유출할 수 없다**. 플러그인은 원본 claims를 반환하고, 호스트가 정화 + keyed-HMAC 신원을 구축한다(crypto 키는 결코 넘어가지 않음).
55
+ - **호스트 중개 키 자료(플러그인 주도 SSRF 없음).** 커스텀 플러그인이 필요로 하는 키 자료(예: JWKS 유사 문서)는 **운영자가 선언한** URL에서 — 플러그인이 고른 URL이 아니라 — **호스트**가 **SSRF 강화된 가드 fetch**로 가져와 IPC로 주입한다. kid 기반 재fetch는 **rate-limit/cooldown으로 제한**(bearer satellite가 이미 하듯)되어 공격자의 자격증명이 호스트의 아웃바운드 요청을 펌프질할 수 없다.
56
+ - **SSRF 가드가 코어로 이동한다.** `isBlockedAddress` + 가드 fetch 패턴(DNS 후 재확인, HTTPS 전용, 본문 제한, fetch timeout, `redirect:"error"`)은 현재 `haechi-auth-jwt` satellite에만 있고 코어는 그것을 import할 수 없다. 1.1은 **node:-only `isBlockedAddress`/`guardedFetch`를 코어 모듈로 승격**(코어는 의존성 0 유지)하며, satellite들(`auth-jwt`, `auth-oidc`, `crypto-kms`의 Vault 복사본)과 호스트 fetch가 그 하나의 코어 헬퍼를 import해 drift를 끝낸다. 알려진 DNS-rebinding 창(resolve-then-connect)은 잔여로 문서화하며, 운영자 선언 host-JWKS의 경우 single-origin/issuer 결합은 완화한다.
57
+
58
+ ### 2.4 프로세스 수명주기(anti-DoS) — 서킷 브레이커 + 워밍된 자식
59
+
60
+ 호출마다 새 `node --permission` spawn은 수십 ms이며, timeout이 나는 플러그인은 모든 인증 시도를 콜드 spawn으로 바꿔 증폭 DoS를 만들 수 있다. 그래서:
61
+
62
+ - 호출 전반에 재사용되는 **워밍된 장수 자식**(단일 점유 직렬화 유지), 한 번 spawn해 준비 상태로 유지.
63
+ - timeout/크래시 시 재spawn은 **서킷 브레이커**로 통제된다: T초 내 N회 kill이면 **영구 fail-closed deny로 trip**(`plugin.worker.terminated{cause:"respawn-storm"}`, 운영자 reset 필요)하고 재spawn 사이에 **지수 백오프**를 둔다 — 플래핑하는 플러그인이 spawn 폭풍이 될 수 없다.
64
+ - `maxPendingCalls`/`maxMessageBytes`와 kill-switch(`plugins.enabled:false`)가 적용된다.
65
+
66
+ ### 2.5 설정 + 감사(호스트 계산 필드만)
67
+
68
+ - `auth.provider:"plugin"`에 `plugin.isolation: "worker" | "process"`와 `plugin.netEnforcement: "require-permission" | "allow-harness"`(기본 `"require-permission"`)가 추가된다. `normalizeConfig`는 fail-closed로 검증한다: `process`는 `process-isolated` 매니페스트 + capability 허용 목록을 요구하고; `--allow-net` 없는 Node에서의 `require-permission`은 **throw**하며; 호스트 fetch URL(커스텀 플러그인이 키 자료를 필요로 할 때)은 운영자 선언이어야 한다. `worker`-vs-`process` 기본값은 1.0 하위호환을 위해 `worker`로 남지만 **문서는 새 고확신 운영자를 `process` + `require-permission`으로 안내**하며, 선택된 모드는 감사에 기록된다.
69
+ - **감사 필드는 호스트 계산/enum 전용(결코 자식 공급 아님).** 수명주기 이벤트에 additive `isolation`, `grants`(**호스트가 계산한** 부여 권한 집합, 플러그인 입력의 에코가 아님), `netEnforcement`가 추가된다 — 모두 고정 enum/호스트 값. 자식 크래시/권한 거부 진단은 `error.message`/자식 출력이 아니라 **고정 reason enum**(`PLUGIN_LOAD_REASONS` 확장)으로 매핑된다(코어 감사 sanitizer는 값이 아니라 키 *이름*으로 거른다 — 자유 텍스트 필드는 자격증명을 해시 체인에 쓸 수 있으므로 모든 새 필드는 허용 목록/enum). 이들은 `plugin.*` 수명주기 이벤트에 있고 frozen 코어 protect-event 스키마 **밖**이므로 1.0 `api-contract.test.mjs` freeze 가드는 영향받지 않는다(이 문서가 *이유*를 명시해 미래 메인테이너가 수명주기 이벤트를 잘못 freeze하지 않도록 한다).
70
+
71
+ ### 2.6 정직한 모델 — 1.1이 닫는 것과 닫지 않는 것
72
+
73
+ **`--allow-net` Node에서 `process-isolated` + `require-permission`** 의 경우, 악성 서명 플러그인은 봉쇄된다:
74
+
75
+ - **fs / exec / worker / addons:** 커널 **강제** 거부(`--permission`, 부여 없음); 플러그인은 fs가 전혀 없는 `data:` URL에서 로드된다.
76
+ - **network:** 커널 **강제** 거부(`--allow-net` 부재) → **네트워크 통한 자격증명 유출 없음**.
77
+ - **stdio / fd:** **차단**(`ignore` + 전용 IPC, 상속 fd 없음) → 로그/stderr 유출 없음.
78
+ - **env 비밀:** 정화됨.
79
+
80
+ **잔여 표면(이 이상으로 과신 금지):** (a) **`--allow-net`이 없는** Node는 **네트워크 봉쇄가 없다** — 운영자가 비-봉쇄 `allow-harness`를 명시 수용하지 않는 한 `process-isolated`는 거기서 fail closed; (b) 정당하게 **`networkEgress:true`** 가 필요한 플러그인은 봉쇄되지 않음; (c) 호스트 fetch SSRF 가드에 **DNS-rebinding** 창이 있음; (d) **자격증명 + 주입된 키 자료가 자식 메모리에** 존재 — core-dump/swap 노출은 범위 밖; (e) `--permission`은 OS 샌드박스가 아니라 Node 런타임 통제 — Node/V8 탈출은 이를 무력화한다. `worker-isolated`(1.0) 모드는 **불변** — 그 trust-only 잔여는 그대로다.
81
+
82
+ ## 3. 명시적 비범위(이후 마이너)
83
+ - Classifier/filter 및 crypto 플러그인 로딩(authProvider 전용).
84
+ - 라이브 revocation 피드 / CRL; 플러그인 레지스트리.
85
+ - `allow-harness` 대체를 실제 봉쇄로 강화(불가능 — `--allow-net` 없는 Node에서는; 답은 `require-permission`).
86
+ - Node 권한 모델을 넘는 OS 수준 샌드박싱(seccomp/namespaces/sandbox-exec).
87
+ - `worker-isolated` 대체.
88
+
89
+ ## 4. 하위 호환성
90
+ Additive + opt-in. `worker-isolated`, injection, 모든 provider 계약, frozen 1.0 API/audit/config 스키마는 불변. `process-isolated`는 새 매니페스트 런타임 + 새 `plugin.isolation`/`plugin.netEnforcement` 설정(기본값은 1.0 동작 보존). `plugin.*` 수명주기 감사 이벤트는 additive 호스트 계산 필드를 얻는다(frozen protect-event 스키마 밖 — 계약 테스트는 영향 없음). `isBlockedAddress`를 코어 node:-only 모듈로 승격하는 것은 additive(satellite들이 재import; 코어는 런타임 의존성 0 유지). 엄격한 1.0 semver상 1.1은 **마이너**.
91
+
92
+ ## 5. 1.1 관계
93
+ 1.1은 플러그인 샌드박스를 **신뢰 기반**(1.0 worker: 서명자를 신뢰)에서 **capability 강제**(1.1 process: OS/런타임이 서명된 코드를 제한)로, 새 opt-in 모드에 대해 강화하여 가장 많이 인용된 1.0 잔여를 *정직하게* 닫는다 — 첫 초안이 틀렸던 부분(하니스는 봉쇄가 아니다; stdio는 유출 채널이다; fail-closed 기능 탐지) 포함. 의존성 0, fail-closed 코어 약속을 유지한다.
94
+
95
+ ## 6. 위협 모델 & 리스크 레지스터 델타
96
+
97
+ | 표면(1.1) | 통제 | 잔여 |
98
+ |---|---|---|
99
+ | 악성 서명 플러그인의 호스트 fs/exec/worker/addons 남용 | `--permission` 자식, **부여 0**, `data:`-URL 로드(fs 없음) | `--permission` Node에서는 없음; V8/Node 탈출은 모든 런타임 통제를 무력화 |
100
+ | 네트워크 통한 자격증명 유출 | `--allow-net` **거부**, **fail-closed 기능 탐지**(`require-permission` → 미지원 시 throw) | `--allow-net` 없는 Node → fail closed(또는 명시적 비-봉쇄 `allow-harness`); `networkEgress:true` 플러그인 |
101
+ | **stdout/stderr/fd** 통한 자격증명 유출 | `stdio:['ignore','ignore','ignore','ipc']`, 상속 fd 없음, 호스트가 자식 출력을 로깅 안 함 | 실질적으로 없음 |
102
+ | `child_process` IPC 통한 object/proto/fd 밀반입 | JSON-문자열 전용 IPC(`serialization:"json"`), null-proto 허용 목록 sanitizer | 실질적으로 없음 |
103
+ | 플러그인 주도 SSRF / 아웃바운드 펌프 | 호스트가 가져오는 **운영자 선언** URL만(코어 SSRF 가드), kid-refetch cooldown | 가드의 DNS-rebinding 창 |
104
+ | 새 필드 통한 감사 평문 유출 | 호스트 계산/enum 전용 필드, 고정 reason enum, 자식 자유 텍스트 없음 | 실질적으로 없음 |
105
+ | Spawn-storm DoS | 워밍 자식 + 서킷 브레이커 + 백오프 | trip된 브레이커는 운영자 reset까지 거부(fail-closed) |
106
+
107
+ 리스크 ID(최종): **P1-SEC-027**(process-isolated capability **강제** — P1-SEC-024의 worker 잔여를 강화: fs/exec/net/stdio가 이제 강제됨), **P1-SEC-028**(호스트 중개 키 자료 + 코어 SSRF 가드). *(제안된 026/027에서 재번호 — P1-SEC-026은 기존 0.9 OIDC 브로커 리스크.)* 1.0 P1-SEC-024 행에 "`--allow-net` Node의 `process-isolated`에 대해 1.1에서 강제됨" 주석. 새 §4 제외: `--allow-net` 없는 Node에서의 네트워크 봉쇄(fail-closed), `networkEgress:true` 플러그인, core-dump/swap, OS 수준 탈출.
108
+
109
+ ## 7. 테스트 기준(PR 분해에 매핑)
110
+
111
+ ### 7.1 PR1 — `process-isolated` 런타임(capability + stdio + data-URL + fail-closed net)
112
+ - `process-isolated` 모드의 계측된 서명 플러그인은 `fs.readFileSync('/etc/hosts')`가 **거부**되고(`ERR_ACCESS_DENIED`), 자식/worker를 spawn할 수 없으며, **fs 권한이 없다**(`data:` URL에서 로드).
113
+ - **Net 레드팀:** `--allow-net` Node에서 플러그인의 `net.connect` / `fetch` / `dns`와 `process.binding('tcp_wrap')` 소켓이 모두 **실패**(커널 거부); `--allow-net`이 **없는** Node에서 `require-permission`인 `createRuntime`는 **생성 시 throw**(fail-closed) — 조용한 하니스 격하가 아님.
114
+ - **stdio/fd 레드팀:** 자격증명을 `stdout`/`stderr`/`console.error`/fd3에 쓰는 플러그인은 **호스트가 볼 수 있는 어떤 sink에도 도달하지 못함**(stdio 무시; 호스트는 아무것도 캡처 안 함).
115
+ - IPC는 JSON-문자열 전용(핸들/structured-clone 객체 전달 시도는 거부); 로드 게이트 + 적합성 + fail-closed 매트릭스(timeout→kill, sanitizer, 단일 점유, kill-switch)가 프로세스 모드에서 유지; macOS 포함 크로스 플랫폼 실행.
116
+
117
+ ### 7.2 PR2 — 자격증명 봉쇄 + 호스트 중개 키 자료 + 코어 SSRF 가드
118
+ - 커스텀 자격증명 플러그인이 원본 자격증명으로 인증하되, net+stdio+fs 거부 하에서 계측된 유출 시도(네트워크 AND stderr AND fd)가 **어떤 sink에도 도달하지 못함**(자격증명이 결코 떠나지 않음을 단언).
119
+ - 호스트 중개 fetch가 **승격된 코어** `isBlockedAddress`를 사용(사설/메타데이터 범위로 resolve되는 `jwksUri`는 거부; 플러그인은 URL을 명명하지 않음); kid-refetch cooldown이 아웃바운드 비율을 제한; satellite들은 코어 가드를 import하며 자신의 스위트를 여전히 통과.
120
+
121
+ ### 7.3 PR3 — 기능 탐지 + 수명주기 + 감사 + 1.1.0 릴리스 컷
122
+ - `process.allowedNodeEnvironmentFlags` + spawn-probe를 통한 `--allow-net` 탐지가 dev Node에서 정확하고 미지원 시 fail-closed; `netEnforcement` 감사됨; spawn 서킷 브레이커가 respawn 폭풍에 trip(감사함); `normalizeConfig` `plugin.isolation`/`netEnforcement` fail-closed 테스트.
123
+ - 수명주기 감사 additive 필드가 호스트 계산/enum 전용(플러그인이 값을 밀반입할 수 없음); 1.0 `api-contract.test.mjs`가 여전히 통과(additive, frozen protect-event 스키마 밖). 위협 모델/리스크 레지스터 델타(P1-SEC-026/027), wiki, README; 코어를 **1.1.0**으로 bump; 검증 게시.
124
+
125
+ ## 8. 제안 PR 분해(스택)
126
+ 1. **`process-isolated` 런타임** — `createProcessIsolatedAuthProvider`: `data:`-URL 로드(fs 없음), `--permission` 부여-0 spawn, `stdio:['ignore','ignore','ignore','ipc']` + 정화 env, JSON-문자열 IPC, 데이터 최소화 wire + 호스트 신원, fail-closed + stdio/net 레드팀 테스트. → §7.1
127
+ 2. **자격증명 봉쇄 + 코어 SSRF 가드** — `isBlockedAddress`/`guardedFetch`를 코어 node:-only 모듈로 승격(satellite들이 재import); 호스트 중개 운영자 선언 키 fetch + IPC 주입 + kid cooldown; exfil-blocked + no-SSRF 테스트. → §7.2
128
+ 3. **기능 탐지 + 수명주기 + 감사 + 1.1.0 컷** — `--allow-net` 탐지 + `netEnforcement`(fail-closed `require-permission` 기본값), 워밍 자식 + 서킷 브레이커, 호스트 계산 감사 필드; `plugin.isolation`/`netEnforcement` 설정; 문서 EN/KO(이 문서, 위협 모델 + 리스크 레지스터 P1-SEC-026/027, 정직한 모델 갱신), wiki, README; 코어 → 1.1.0, 검증 게시. → §7.3