haechi 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.ko.md +46 -11
  2. package/README.md +46 -11
  3. package/SECURITY.md +7 -1
  4. package/docs/README.md +2 -0
  5. package/docs/current/compliance-mapping.ko.md +53 -0
  6. package/docs/current/compliance-mapping.md +53 -0
  7. package/docs/current/config-version.ko.md +30 -0
  8. package/docs/current/config-version.md +51 -0
  9. package/docs/current/configuration.ko.md +165 -9
  10. package/docs/current/configuration.md +165 -9
  11. package/docs/current/operations-runbook.ko.md +155 -0
  12. package/docs/current/operations-runbook.md +241 -0
  13. package/docs/current/release-process.ko.md +5 -1
  14. package/docs/current/release-process.md +5 -1
  15. package/docs/current/risk-register-release-gate.ko.md +5 -3
  16. package/docs/current/risk-register-release-gate.md +13 -3
  17. package/docs/current/security-whitepaper.ko.md +102 -0
  18. package/docs/current/security-whitepaper.md +102 -0
  19. package/docs/current/shared-responsibility.ko.md +2 -2
  20. package/docs/current/shared-responsibility.md +2 -2
  21. package/docs/current/threat-model.ko.md +4 -2
  22. package/docs/current/threat-model.md +4 -2
  23. package/examples/local-proxy-demo/README.md +51 -0
  24. package/examples/local-proxy-demo/demo.mjs +144 -0
  25. package/examples/local-proxy-demo/demo.tape +19 -0
  26. package/examples/local-proxy-demo/live-demo.mjs +121 -0
  27. package/examples/local-proxy-demo/live-demo.tape +25 -0
  28. package/haechi.config.example.json +20 -3
  29. package/package.json +7 -2
  30. package/packages/audit/index.mjs +26 -2
  31. package/packages/cli/bin/haechi.mjs +57 -10
  32. package/packages/cli/runtime.mjs +402 -10
  33. package/packages/core/index.mjs +143 -8
  34. package/packages/filter/index.mjs +975 -12
  35. package/packages/metrics/index.mjs +181 -0
  36. package/packages/privacy-profiles/index.mjs +72 -3
  37. package/packages/protocol-adapters/index.mjs +99 -1
  38. package/packages/proxy/index.mjs +525 -40
  39. package/packages/stream-filter/index.mjs +69 -7
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ // Live end-to-end demo against a REAL upstream model (vLLM / Ollama / any
3
+ // OpenAI-compatible server). Unlike demo.mjs (which uses a deterministic stub),
4
+ // this proves protection against an actual model: it asks the model to repeat the
5
+ // phone number it was given, and the model can only return the *masked* form —
6
+ // the real number never reached it.
7
+ //
8
+ // HAECHI_LIVE_UPSTREAM=http://127.0.0.1:8000 \
9
+ // HAECHI_LIVE_MODEL="Qwen/Qwen3.6-35B-A3B-FP8" \
10
+ // node examples/local-proxy-demo/live-demo.mjs
11
+ //
12
+ // Defaults: type=vllm-openai. HAECHI_LIVE_TYPE and HAECHI_LIVE_MODEL override.
13
+ // Zero dependencies — only node: builtins + the in-repo haechi packages.
14
+
15
+ import { mkdtemp, readFile } from "node:fs/promises";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+
19
+ import { createRuntime } from "../../packages/cli/runtime.mjs";
20
+ import { createHaechiProxy } from "../../packages/proxy/index.mjs";
21
+ import { initLocalKeyFile } from "../../packages/crypto/index.mjs";
22
+
23
+ const B = "\x1b[1m", D = "\x1b[2m", G = "\x1b[32m", Y = "\x1b[33m", C = "\x1b[36m", R = "\x1b[31m", X = "\x1b[0m";
24
+ const rule = () => console.log(D + "─".repeat(64) + X);
25
+ const scene = (n, t) => { console.log(); rule(); console.log(`${B}${C} ${n}. ${t}${X}`); rule(); };
26
+ const pause = (ms) => new Promise((r) => setTimeout(r, ms));
27
+
28
+ const UPSTREAM = process.env.HAECHI_LIVE_UPSTREAM;
29
+ const TYPE = process.env.HAECHI_LIVE_TYPE || "vllm-openai";
30
+ const MODEL = process.env.HAECHI_LIVE_MODEL || "Qwen/Qwen3.6-35B-A3B-FP8";
31
+ if (!UPSTREAM) {
32
+ console.error("Set HAECHI_LIVE_UPSTREAM (e.g. http://127.0.0.1:8000) to a reachable OpenAI-compatible server.");
33
+ console.error("For a no-backend reproducible run, use: npm run demo");
34
+ process.exit(2);
35
+ }
36
+
37
+ async function chat(base, content, extra = {}) {
38
+ const t0 = Date.now();
39
+ const res = await fetch(`${base}/v1/chat/completions`, {
40
+ method: "POST", headers: { "content-type": "application/json" },
41
+ body: JSON.stringify({ model: MODEL, max_tokens: 128, temperature: 0,
42
+ // Qwen3 reasoning models: ask for a direct answer (no chain-of-thought) so
43
+ // the demo gets a terse content reply. Ignored by non-reasoning servers.
44
+ chat_template_kwargs: { enable_thinking: false },
45
+ messages: [{ role: "user", content }], ...extra })
46
+ });
47
+ const body = await res.json();
48
+ return { status: res.status, ms: Date.now() - t0, text: body.choices?.[0]?.message?.content ?? body.error?.message ?? "(no content)" };
49
+ }
50
+
51
+ async function main() {
52
+ console.log(`\n${B}🛡 Haechi — LIVE end-to-end demo${X} ${D}(real model: ${MODEL} via ${TYPE}, enforce mode)${X}`);
53
+
54
+ const dir = await mkdtemp(join(tmpdir(), "haechi-live-"));
55
+ const keyFile = join(dir, ".haechi", "dev.keys.json");
56
+ const auditPath = join(dir, ".haechi", "audit.jsonl");
57
+ await initLocalKeyFile(keyFile, { force: true });
58
+
59
+ const runtime = createRuntime({
60
+ mode: "enforce",
61
+ target: { type: TYPE, upstream: UPSTREAM },
62
+ policy: { mode: "enforce", presets: ["llm-redact"], actions: { email: "tokenize", phone: "mask", secret: "redact", api_key: "redact", card: "block" } },
63
+ tokenVault: { detokenizeResponses: true },
64
+ responseProtection: { enabled: true, mode: "enforce", failureMode: "fail-closed" },
65
+ keys: { keyFile }, audit: { path: auditPath }
66
+ });
67
+ const proxy = createHaechiProxy({ runtime, port: 0 });
68
+ const addr = await proxy.listen();
69
+ const base = `http://127.0.0.1:${addr.port}`;
70
+
71
+ // ── Scene 1 ────────────────────────────────────────────────────────────────
72
+ scene(1, "Ask a REAL model to repeat the phone number you give it");
73
+ const prompt = "Reply in one short line: repeat the phone number you were given. Phone: 010-1234-5678, email minji.kim@example.com";
74
+ console.log(`${Y}you send →${X} ${prompt}`);
75
+ await pause(700);
76
+ const r1 = await chat(base, prompt);
77
+
78
+ scene(2, "Haechi detected + protected the prompt BEFORE it left your machine");
79
+ const events = (await readFile(auditPath, "utf8")).trim().split("\n").map((l) => JSON.parse(l));
80
+ const ev = events.find((e) => Array.isArray(e.detections) && e.detections.length) ?? events[0];
81
+ console.log(`${D}detections:${X} ${(ev.detections ?? []).map((d) => `${G}${d.type}→${d.action}${X}`).join(" ")}`);
82
+ console.log(`${D}the model only ever saw:${X} email → ${C}[TOKEN:…]${X}, phone → ${C}01*********78${X}`);
83
+ await pause(700);
84
+
85
+ scene(3, "The real model replies — it can only return the MASKED phone");
86
+ console.log(`${G}${MODEL.split("/").pop()} →${X} ${B}${r1.text}${X} ${D}(${r1.ms} ms)${X}`);
87
+ console.log(`${D} your real number 010-1234-5678 never reached the model — it cannot reveal it.${X}`);
88
+ await pause(700);
89
+
90
+ // ── Scene 4 ────────────────────────────────────────────────────────────────
91
+ scene(4, "The audit log — hash-chained, and never any plaintext");
92
+ const auditRaw = await readFile(auditPath, "utf8");
93
+ console.log(`${D}leaks the real email/phone?${X} ${/minji\.kim@example|010-1234-5678/.test(auditRaw) ? R + "YES" + X : G + "no — clean" + X}`);
94
+ await pause(700);
95
+
96
+ // ── Scene 5 ────────────────────────────────────────────────────────────────
97
+ scene(5, "Day-2 operability — live readiness + Prometheus metrics");
98
+ const ready = await (await fetch(`${base}/__haechi/ready`)).json();
99
+ console.log(`${D}/__haechi/ready →${X} ${ready.ready ? G + "ready" : R + "not ready"}${X}`);
100
+ const metrics = await (await fetch(`${base}/__haechi/metrics`)).text();
101
+ for (const line of metrics.split("\n").filter((l) => /^haechi_requests_total\{/.test(l)).slice(0, 3)) {
102
+ console.log(`${D}metric:${X} ${line}`);
103
+ }
104
+ await pause(700);
105
+
106
+ // ── Scene 6 ────────────────────────────────────────────────────────────────
107
+ scene(6, "A card number is blocked before it ever reaches the model");
108
+ const r2 = await chat(base, "charge card 4242 4242 4242 4242 now");
109
+ console.log(`${Y}you send →${X} "charge card 4242 4242 4242 4242 now"`);
110
+ console.log(`${G}proxy →${X} HTTP ${r2.status} ${r2.status === 403 ? R + B + "BLOCKED" + X : ""} ${D}(no upstream call made)${X}`);
111
+
112
+ console.log();
113
+ rule();
114
+ console.log(`${B}${G} ✓ live${X} ${D}— a real model, and your PII never left the gateway in the clear.${X}`);
115
+ rule();
116
+ console.log();
117
+
118
+ await proxy.close();
119
+ }
120
+
121
+ main().then(() => process.exit(0)).catch((e) => { console.error("live demo failed:", e); process.exit(1); });
@@ -0,0 +1,25 @@
1
+ # VHS tape for the Haechi LIVE demo (real upstream model).
2
+ # Regenerate the README GIF with:
3
+ # HAECHI_LIVE_UPSTREAM is set below via Env so it stays out of the recording.
4
+ # vhs examples/local-proxy-demo/live-demo.tape (run from the repo root)
5
+
6
+ Output docs/assets/haechi-demo.gif
7
+
8
+ Set Shell "bash"
9
+ Set FontSize 15
10
+ Set Width 1180
11
+ Set Height 840
12
+ Set Padding 18
13
+ Set Theme "Catppuccin Mocha"
14
+ Set TypingSpeed 55ms
15
+
16
+ # Point these at a reachable OpenAI-compatible server before recording. Using Env
17
+ # (not the typed command) keeps the upstream URL out of the captured GIF.
18
+ Env HAECHI_LIVE_UPSTREAM "http://127.0.0.1:8000"
19
+ Env HAECHI_LIVE_MODEL "Qwen/Qwen3.6-35B-A3B-FP8"
20
+
21
+ Sleep 500ms
22
+ Type "node examples/local-proxy-demo/live-demo.mjs"
23
+ Sleep 600ms
24
+ Enter
25
+ Sleep 9s
@@ -1,4 +1,5 @@
1
1
  {
2
+ "configVersion": 1,
2
3
  "mode": "dry-run",
3
4
  "target": {
4
5
  "type": "llm-http",
@@ -7,7 +8,9 @@
7
8
  },
8
9
  "proxy": {
9
10
  "host": "127.0.0.1",
10
- "port": 11016
11
+ "port": 11016,
12
+ "tls": null,
13
+ "trustForwardedProto": false
11
14
  },
12
15
  "responseProtection": {
13
16
  "enabled": false,
@@ -25,7 +28,12 @@
25
28
  },
26
29
  "limits": {
27
30
  "maxRequestBytes": 1048576,
28
- "upstreamTimeoutMs": 120000
31
+ "maxNestingDepth": 256,
32
+ "upstreamTimeoutMs": 120000,
33
+ "maxInFlight": 0,
34
+ "shutdownGraceMs": 10000,
35
+ "requestTimeoutMs": null,
36
+ "headersTimeoutMs": null
29
37
  },
30
38
  "policy": {
31
39
  "mode": "dry-run",
@@ -40,7 +48,10 @@
40
48
  }
41
49
  },
42
50
  "filters": {
43
- "customRules": []
51
+ "customRules": [],
52
+ "minConfidence": 0,
53
+ "allowlist": [],
54
+ "decodeAndRescan": false
44
55
  },
45
56
  "keys": {
46
57
  "provider": "local",
@@ -67,6 +78,12 @@
67
78
  "privacy": {
68
79
  "profile": null
69
80
  },
81
+ "logging": {
82
+ "format": "text"
83
+ },
84
+ "metrics": {
85
+ "enabled": true
86
+ },
70
87
  "auth": {
71
88
  "provider": "none",
72
89
  "store": ".haechi/auth.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haechi",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Self-hosted AI context enforcement across LLM, MCP, vLLM, Ollama, and agent traffic — a stable, zero-dependency security gateway.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -51,7 +51,8 @@
51
51
  "./token-vault": "./packages/token-vault/index.mjs",
52
52
  "./stream-filter": "./packages/stream-filter/index.mjs",
53
53
  "./auth": "./packages/auth/index.mjs",
54
- "./ssrf": "./packages/ssrf/index.mjs"
54
+ "./ssrf": "./packages/ssrf/index.mjs",
55
+ "./metrics": "./packages/metrics/index.mjs"
55
56
  },
56
57
  "files": [
57
58
  "README.md",
@@ -74,10 +75,14 @@
74
75
  "sbom": "node scripts/generate-sbom.mjs",
75
76
  "checksums": "node scripts/release-checksums.mjs",
76
77
  "bench:payload": "node scripts/bench-payload.mjs",
78
+ "bench:detection": "node scripts/bench-detection.mjs",
79
+ "bench:throughput": "node scripts/bench-throughput.mjs",
80
+ "scan:detection": "node scripts/bench-detection.mjs --gate",
77
81
  "check:peer-ranges": "node scripts/check-satellite-peer-ranges.mjs",
78
82
  "release:preflight": "node scripts/release-preflight.mjs && node scripts/check-satellite-peer-ranges.mjs",
79
83
  "release:preflight:npm": "node scripts/release-preflight.mjs --require-npm-auth && node scripts/check-satellite-peer-ranges.mjs",
80
84
  "haechi": "node packages/cli/bin/haechi.mjs",
85
+ "demo": "node examples/local-proxy-demo/demo.mjs",
81
86
  "demo:init": "node packages/cli/bin/haechi.mjs init --force",
82
87
  "demo:protect": "node packages/cli/bin/haechi.mjs protect examples/llm-prompt-filtering/input.json --config haechi.config.json",
83
88
  "demo:report": "node packages/cli/bin/haechi.mjs report --audit .haechi/audit.jsonl"
@@ -1,5 +1,5 @@
1
- import { createReadStream } from "node:fs";
2
- import { appendFile, mkdir, open, stat, unlink } from "node:fs/promises";
1
+ import { createReadStream, constants as fsConstants } from "node:fs";
2
+ import { access, appendFile, mkdir, open, stat, unlink } from "node:fs/promises";
3
3
  import { createHash } from "node:crypto";
4
4
  import { dirname } from "node:path";
5
5
  import { createInterface } from "node:readline";
@@ -91,6 +91,30 @@ export function createJsonlAuditSink({ path, anchor = null }) {
91
91
  });
92
92
  writeQueue = write.catch(() => {});
93
93
  await write;
94
+ },
95
+
96
+ // WS4-A readiness probe: a CHEAP writability check used by /__haechi/ready.
97
+ // A security gateway that cannot append to its audit log is NOT ready
98
+ // (fail-closed), so this confirms the audit directory exists and is writable
99
+ // WITHOUT writing an event (no audit-chain side effect). It returns the bare
100
+ // boolean and an enum reason — never a path value or any payload/PII.
101
+ async ready() {
102
+ try {
103
+ const dir = dirname(path);
104
+ await mkdir(dir, { recursive: true });
105
+ await access(dir, fsConstants.W_OK);
106
+ // If the audit file already exists, confirm it is writable too.
107
+ try {
108
+ await access(path, fsConstants.W_OK);
109
+ } catch (error) {
110
+ if (error.code !== "ENOENT") {
111
+ return { ok: false, reason: "audit_file_not_writable" };
112
+ }
113
+ }
114
+ return { ok: true };
115
+ } catch {
116
+ return { ok: false, reason: "audit_dir_not_writable" };
117
+ }
94
118
  }
95
119
  };
96
120
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import { readAuditSummary, verifyAuditChain } from "../../audit/index.mjs";
4
- import { DEFAULT_PROXY_PORT, createHaechiProxy } from "../../proxy/index.mjs";
4
+ import { DEFAULT_PROXY_PORT, HAECHI_VERSION, createHaechiProxy } from "../../proxy/index.mjs";
5
5
  import { signPolicyBundleFile, verifyPolicyBundleFile } from "../../policy-bundle/index.mjs";
6
6
  import { validatePluginManifestFile } from "../../plugin/index.mjs";
7
7
  import { runMcpStdioFilter, wrapMcpChild } from "../../mcp-stdio/index.mjs";
@@ -283,25 +283,69 @@ async function proxyCommand(argv) {
283
283
  const port = parsePort(options.port ?? config.proxy.port);
284
284
  const host = options.host ?? config.proxy.host;
285
285
  const allowRemoteBind = Boolean(options["allow-remote-bind"]);
286
+ // proxy.tls / proxy.trustForwardedProto come from the normalized config (the
287
+ // TLS material is loaded from file paths at load time); createHaechiProxy reads
288
+ // them from runtime.config.proxy, so the CLI does not re-pass them. The bind
289
+ // guard inside createHaechiProxy throws fail-closed for a remote bind without
290
+ // TLS and without trustForwardedProto.
286
291
  const proxy = createHaechiProxy({ runtime, port, host, allowRemoteBind });
287
292
  const address = await proxy.listen();
293
+ const scheme = address.tls ? "https" : "http";
288
294
 
289
295
  const effectiveMode = config.policy.mode ?? config.mode;
290
- console.log(`Haechi proxy listening on http://${address.host}:${address.port}`);
291
- console.log(`Upstream: ${config.target.upstream}`);
292
- console.log(`Mode: ${effectiveMode}`);
296
+ const jsonLogs = config.logging?.format === "json";
297
+ // Structured startup/shutdown logs honor logging.format. JSON mode emits one
298
+ // line per event carrying only non-secret operational fields (host/port/mode/
299
+ // version/warning codes) — never a payload, token, or PII value.
300
+ const logEvent = (level, event, fields = {}) => {
301
+ if (jsonLogs) {
302
+ const stream = level === "warn" ? process.stderr : process.stdout;
303
+ stream.write(`${JSON.stringify({ level, event, ...fields })}\n`);
304
+ }
305
+ };
306
+
307
+ if (jsonLogs) {
308
+ logEvent("info", "proxy_listening", {
309
+ host: address.host,
310
+ port: address.port,
311
+ scheme,
312
+ tls: Boolean(address.tls),
313
+ upstream: config.target.upstream,
314
+ mode: effectiveMode,
315
+ version: HAECHI_VERSION
316
+ });
317
+ } else {
318
+ console.log(`Haechi proxy listening on ${scheme}://${address.host}:${address.port}`);
319
+ console.log(`Upstream: ${config.target.upstream}`);
320
+ console.log(`Mode: ${effectiveMode}`);
321
+ }
293
322
  if (allowRemoteBind) {
294
- console.error("warning: --allow-remote-bind exposes the proxy beyond loopback. Put Haechi behind explicit network access controls.");
323
+ if (jsonLogs) {
324
+ logEvent("warn", "remote_bind_enabled", { tls: Boolean(address.tls), trustForwardedProto: Boolean(config.proxy?.trustForwardedProto) });
325
+ } else if (address.tls) {
326
+ console.error("warning: --allow-remote-bind exposes the proxy beyond loopback (TLS terminated by Haechi). Put Haechi behind explicit network access controls.");
327
+ } else {
328
+ console.error("warning: --allow-remote-bind exposes the proxy beyond loopback behind a trusted TLS-terminating reverse proxy (proxy.trustForwardedProto). Requests without X-Forwarded-Proto: https are refused. Put Haechi behind explicit network access controls.");
329
+ }
295
330
  }
296
331
  if (effectiveMode !== "enforce") {
297
- console.error(`warning: policy mode is ${effectiveMode}. Payloads are inspected and audited but NOT modified or blocked. Set policy.mode to "enforce" to protect traffic.`);
332
+ if (jsonLogs) {
333
+ logEvent("warn", "non_enforce_mode", { mode: effectiveMode });
334
+ } else {
335
+ console.error(`warning: policy mode is ${effectiveMode}. Payloads are inspected and audited but NOT modified or blocked. Set policy.mode to "enforce" to protect traffic.`);
336
+ }
298
337
  }
299
338
  if (!config.responseProtection.enabled) {
300
- console.error("warning: responseProtection.enabled is false. Upstream responses are forwarded without inspection.");
339
+ if (jsonLogs) {
340
+ logEvent("warn", "response_protection_disabled");
341
+ } else {
342
+ console.error("warning: responseProtection.enabled is false. Upstream responses are forwarded without inspection.");
343
+ }
301
344
  }
302
345
 
303
346
  for (const signal of ["SIGINT", "SIGTERM"]) {
304
347
  process.once(signal, async () => {
348
+ logEvent("info", "proxy_shutdown", { signal });
305
349
  await proxy.close();
306
350
  process.exit(0);
307
351
  });
@@ -599,7 +643,7 @@ const COMMAND_HELP = {
599
643
  proxy: {
600
644
  usage: `haechi proxy [--config haechi.config.json] [--host 127.0.0.1] [--port ${DEFAULT_PROXY_PORT}] [--allow-remote-bind]`,
601
645
  summary: "Run the local HTTP JSON proxy in front of an upstream LLM.",
602
- detail: "Binds loopback by default; --allow-remote-bind is required (and must be a CLI flag, not config) to bind non-loopback hosts. There is no client auth yet — see 'haechi config'."
646
+ detail: "Binds loopback (plain http) by default; --allow-remote-bind is required (and must be a CLI flag, not config) to bind non-loopback hosts. A remote bind additionally requires TLS: set proxy.tls ({ keyFile, certFile } or { pfxFile, passphrase? }) so Haechi serves https, OR set proxy.trustForwardedProto: true when a trusted reverse proxy terminates TLS in front of Haechi (Haechi then refuses any request without X-Forwarded-Proto: https). Configure client auth via auth.provider — see 'haechi config'."
603
647
  },
604
648
  "policy-sign": {
605
649
  usage: "haechi policy-sign <policy.json> [--config haechi.config.json] [--out policy.bundle.json]",
@@ -693,7 +737,8 @@ Enforcement
693
737
 
694
738
  Upstream + proxy
695
739
  target.type llm-http | openai-compatible | vllm-openai |
696
- ollama | llama-cpp (unknown = fail)
740
+ ollama | llama-cpp | anthropic |
741
+ gemini (unknown = fail)
697
742
  target.upstream the only upstream the proxy forwards to
698
743
  proxy.host / proxy.port 127.0.0.1 / ${DEFAULT_PROXY_PORT}
699
744
  non-loopback host needs --allow-remote-bind (CLI flag)
@@ -712,6 +757,8 @@ Detection policy
712
757
  policy.defaultAction allow | redact | mask | tokenize | encrypt | block
713
758
  policy.actions per-type overrides; merges may strengthen, not weaken
714
759
  filters.customRules extra regex rules (ReDoS-screened)
760
+ filters.minConfidence [0,1] drop soft detections below this (not hard-block)
761
+ filters.allowlist FP exceptions [value|{value?,path?}] (not hard-block)
715
762
 
716
763
  Tokenization (model sees token, caller sees plaintext)
717
764
  tokenVault.revealPolicy disabled | local-dev (manual reveal gate)
@@ -731,7 +778,7 @@ Audit integrity
731
778
  audit.anchor.everyRecords anchor cadence (default 1)
732
779
 
733
780
  Privacy + MCP
734
- privacy.profile kr-pipa | eu-gdpr | us-general | null
781
+ privacy.profile kr-pipa | eu-gdpr | asia-pdpa | us-general | jp-appi | null
735
782
  mcp.allowedMethods client-callable method allowlist
736
783
 
737
784
  Binding beyond loopback (0.0.0.0):