@chatpanel/gateway 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/ner-assets.js +8 -0
- package/src/ner.js +31 -5
- package/src/redact.js +6 -2
- package/src/server.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Local privacy gateway — redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"start": "node bin/chatpanel-gateway.js",
|
|
15
15
|
"test": "node --test",
|
|
16
16
|
"typecheck": "tsc -p tsconfig.json",
|
|
17
|
-
"build:bin": "bash scripts/build-binaries.sh"
|
|
17
|
+
"build:bin": "bash scripts/build-binaries.sh",
|
|
18
|
+
"gen:ner": "node scripts/gen-ner-assets.mjs",
|
|
19
|
+
"prepublishOnly": "node scripts/gen-ner-assets.mjs"
|
|
18
20
|
},
|
|
19
21
|
"files": [
|
|
20
22
|
"src",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// GENERATED by scripts/gen-ner-assets.mjs — do NOT edit. Edit ner/* and re-run.
|
|
2
|
+
// Base64 of the bundled spaCy NER server, embedded so a standalone binary can
|
|
3
|
+
// write a runnable ner/ directory to disk (the binary has no ner/ on the FS).
|
|
4
|
+
export const NER_ASSETS = {
|
|
5
|
+
"run.sh": "IyEvdXNyL2Jpbi9lbnYgYmFzaAojIE9uZS1zaG90OiBjcmVhdGUgdGhlIHZlbnYgKGlmIG5lZWRlZCksIGluc3RhbGwgZGVwcyArIHRoZSBzcGFDeSBtb2RlbCwgYW5kIHNlcnZlLgojIFVzYWdlOiAgLi9ydW4uc2ggICAgICAgIChkZWZhdWx0cyB0byBwb3J0IDkwMDkpCiMgICAgICAgICBQT1JUPTkxMDAgLi9ydW4uc2gKc2V0IC1ldW8gcGlwZWZhaWwKY2QgIiQoZGlybmFtZSAiJDAiKSIKCmlmIFsgISAtZCAudmVudiBdOyB0aGVuCiAgZWNobyAi4oaSIGNyZWF0aW5nIHZpcnR1YWwgZW52ICgudmVudinigKYiCiAgcHl0aG9uMyAtbSB2ZW52IC52ZW52CmZpCiMgc2hlbGxjaGVjayBkaXNhYmxlPVNDMTA5MQpzb3VyY2UgLnZlbnYvYmluL2FjdGl2YXRlCgplY2hvICLihpIgaW5zdGFsbGluZyBkZXBlbmRlbmNpZXPigKYiCnBpcCBpbnN0YWxsIC1xIC0tdXBncmFkZSBwaXAKcGlwIGluc3RhbGwgLXEgLXIgcmVxdWlyZW1lbnRzLnR4dApweXRob24gLWMgImltcG9ydCBlbl9jb3JlX3dlYl9zbSIgPi9kZXYvbnVsbCAyPiYxIHx8IHB5dGhvbiAtbSBzcGFjeSBkb3dubG9hZCBlbl9jb3JlX3dlYl9zbQoKZWNobyAi4oaSIHNlcnZpbmcgb24gaHR0cDovLzEyNy4wLjAuMToke1BPUlQ6LTkwMDl9L25lciAgKEN0cmwtQyB0byBzdG9wKSIKZXhlYyB1dmljb3JuIHNlcnZlcjphcHAgLS1wb3J0ICIke1BPUlQ6LTkwMDl9Igo=",
|
|
6
|
+
"server.py": "IiIiCkNoYXRQYW5lbCBsb2NhbCBORVIgaGVscGVyIOKAlCBhIHRpbnkgb24tZGV2aWNlIGVudGl0eSBkZXRlY3RvciBmb3IgUElJIHJlZGFjdGlvbi4KCkNoYXRQYW5lbCdzIFByaXZhY3kg4oaSICJBSSBkZXRlY3Rpb24iIGNhbiBwb2ludCBhdCBhbnkgTE9DQUwgc2VydmljZSB0aGF0IGFjY2VwdHMKICAgIFBPU1QgeyJ0ZXh0IjogIi4uLiJ9CmFuZCByZXR1cm5zCiAgICB7ImVudGl0aWVzIjogW3sidmFsdWUiOiAiLi4uIiwgInR5cGUiOiAiUEVSU09OfE9SR3xHUEV8RU1BSUx8UEhPTkV8Li4uIn1dfQoKVGhpcyB3cmFwcyBzcGFDeSAocGVvcGxlIC8gb3JnYW5pemF0aW9ucyAvIGxvY2F0aW9ucykgQU5EIGFkZHMgYSByZWdleCBwYXNzIGZvcgp0aGUgc3RydWN0dXJlZCBpZGVudGlmaWVycyBzcGFDeSBkb2Vzbid0IGVtaXQgKGVtYWlscywgcGhvbmUgbnVtYmVycywgU1NOcywgY2FyZHMsCklQcyksIHNvIHRoZSBkZXRlY3RvciBpcyBjb21wcmVoZW5zaXZlIG9uIGl0cyBvd24uIE9ubHkgdGhlIHJlZGFjdGVkIHBsYWNlaG9sZGVycwooZS5nLiBbW1BFUlNPTl8xXV0pIGV2ZXIgcmVhY2ggdGhlIGNoYXQgbW9kZWwg4oCUIHRoZSByYXcgdGV4dCBuZXZlciBsZWF2ZXMgeW91ciBib3guCgotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpTZXR1cCAgKG1vc3QgbWFjaGluZXMgYmxvY2sgZ2xvYmFsIHBpcCBpbnN0YWxscywgc28gdXNlIGEgdmlydHVhbCBlbnYpCgogICAgcHl0aG9uMyAtbSB2ZW52IC52ZW52CiAgICBzb3VyY2UgLnZlbnYvYmluL2FjdGl2YXRlICAgICAgICAgICMgV2luZG93czogLnZlbnZcXFNjcmlwdHNcXGFjdGl2YXRlCiAgICBwaXAgaW5zdGFsbCAtciByZXF1aXJlbWVudHMudHh0CiAgICBweXRob24gLW0gc3BhY3kgZG93bmxvYWQgZW5fY29yZV93ZWJfc20KClJ1biAgKHRoZSBmaWxlIGlzIHNlcnZlci5weSwgc28gdGhlIHV2aWNvcm4gaW1wb3J0IHBhdGggaXMgInNlcnZlcjphcHAiKQoKICAgIHV2aWNvcm4gc2VydmVyOmFwcCAtLXBvcnQgOTAwOQoKVGhlbiBpbiBDaGF0UGFuZWwg4oaSIFNldHRpbmdzIOKGkiBQcml2YWN5OgogICAgUmVkYWN0aW9uIDogT24g4oCUICsgQUkgZGV0ZWN0aW9uCiAgICBEZXRlY3RvciAgOiBMb2NhbCBORVIgc2VydmljZSAoc3BhQ3kgLyBQcmVzaWRpbykKICAgIFVSTCAgICAgICA6IGh0dHA6Ly8xMjcuMC4wLjE6OTAwOS9uZXIKClRpcDogZW5fY29yZV93ZWJfc20gaXMgc21hbGwgKyBmYXN0LiBGb3IgYmV0dGVyIGFjY3VyYWN5IHVzZSBlbl9jb3JlX3dlYl9tZCBvcgplbl9jb3JlX3dlYl90cmYgKGRvd25sb2FkIHRoZSBzYW1lIHdheSwgdGhlbiBjaGFuZ2UgdGhlIGxvYWQgYmVsb3cpLgotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoiIiIKZnJvbSBmYXN0YXBpIGltcG9ydCBGYXN0QVBJCgppbXBvcnQgcmUKaW1wb3J0IHNwYWN5CgpNT0RFTCA9ICJlbl9jb3JlX3dlYl9zbSIKbmxwID0gc3BhY3kubG9hZChNT0RFTCkKCmFwcCA9IEZhc3RBUEkodGl0bGU9IkNoYXRQYW5lbCBORVIgaGVscGVyIikKCiMgc3BhQ3kncyBORVIgZW1pdHMgbmFtZXMgLyBvcmdzIC8gbG9jYXRpb25zIGJ1dCBOT1Qgc3RydWN0dXJlZCBpZGVudGlmaWVycywgc28gYWRkCiMgYSByZWdleCBwYXNzIGZvciB0aG9zZS4gKENoYXRQYW5lbCBhbHNvIGNhdGNoZXMgdGhlc2Ugb24tZGV2aWNlLCBidXQgZW1pdHRpbmcgdGhlbQojIGhlcmUga2VlcHMgdGhlIGRldGVjdG9yIHNlbGYtY29udGFpbmVkIOKAlCB3aGF0IHlvdSB0ZXN0IGlzIHdoYXQgeW91IGdldC4pIE9yZGVyCiMgbWF0dGVyczogbW9yZSBzcGVjaWZpYyBwYXR0ZXJucyBydW4gZmlyc3Qgc28gYSBjYXJkIC8gU1NOIGlzbid0IHJlLW1hdGNoZWQgYXMgYQojIHBob25lIG51bWJlci4KX1BBVFRFUk5TID0gWwogICAgKCJFTUFJTCIsIHJlLmNvbXBpbGUociJbQS1aYS16MC05Ll8lK1wtXStAW0EtWmEtejAtOS5cLV0rXC5bQS1aYS16XXsyLH0iKSksCiAgICAoIlNTTiIsIHJlLmNvbXBpbGUociJcYlxkezN9LVxkezJ9LVxkezR9XGIiKSksCiAgICAoIkNSRURJVF9DQVJEIiwgcmUuY29tcGlsZShyIlxiKD86XGRbIC1dPyl7MTMsMTl9XGIiKSksCiAgICAoIklQIiwgcmUuY29tcGlsZShyIlxiKD86XGR7MSwzfVwuKXszfVxkezEsM31cYiIpKSwKICAgICgiUEhPTkUiLCByZS5jb21waWxlKHIiKD88IVxkKSg/OlwrP1xkezEsMn1bIC5cLV0/KT9cKD9cZHszfVwpP1sgLlwtXT9cZHszfVsgLlwtXT9cZHs0fSg/IVxkKSIpKSwKXQoKCmRlZiBfcmVnZXhfZW50aXRpZXModGV4dCk6CiAgICBvdXQsIHRha2VuID0gW10sIFtdCiAgICBmb3IgbGFiZWwsIHJ4IGluIF9QQVRURVJOUzoKICAgICAgICBmb3IgbSBpbiByeC5maW5kaXRlcih0ZXh0KToKICAgICAgICAgICAgc3RhcnQsIGVuZCA9IG0uc3RhcnQoKSwgbS5lbmQoKQogICAgICAgICAgICBpZiBhbnkoc3RhcnQgPCB0ZSBhbmQgdHMgPCBlbmQgZm9yIHRzLCB0ZSBpbiB0YWtlbik6CiAgICAgICAgICAgICAgICBjb250aW51ZSAgIyBzcGFuIGFscmVhZHkgY2xhaW1lZCBieSBhIG1vcmUgc3BlY2lmaWMgcGF0dGVybgogICAgICAgICAgICB0YWtlbi5hcHBlbmQoKHN0YXJ0LCBlbmQpKQogICAgICAgICAgICBvdXQuYXBwZW5kKHsidmFsdWUiOiBtLmdyb3VwKDApLnN0cmlwKCksICJ0eXBlIjogbGFiZWx9KQogICAgcmV0dXJuIG91dAoKCkBhcHAuZ2V0KCIvaGVhbHRoIikKZGVmIGhlYWx0aCgpOgogICAgcmV0dXJuIHsib2siOiBUcnVlLCAibW9kZWwiOiBNT0RFTH0KCgpAYXBwLnBvc3QoIi9uZXIiKQpkZWYgbmVyKHBheWxvYWQ6IGRpY3QpOgogICAgIiIiUmV0dXJuIHNwYUN5IGVudGl0aWVzICsgcmVnZXggaWRlbnRpZmllcnMgaW4gQ2hhdFBhbmVsJ3MgZXhwZWN0ZWQgc2hhcGUuCgogICAgQ2hhdFBhbmVsIG1hcHMgY29tbW9uIGxhYmVscyBpdHNlbGYgKFBFUuKGklBFUlNPTiwgR1BF4oaSTE9DQVRJT04sIOKApikgYW5kIGtlZXBzCiAgICBvbmx5IHRoZSBjYXRlZ29yaWVzIHlvdSBlbmFibGUgaW4gdGhlIFByaXZhY3kgdGFiLCBzbyBpdCdzIGZpbmUgdG8gcmV0dXJuIGFsbAogICAgb2YgdGhlbSBoZXJlLgogICAgIiIiCiAgICB0ZXh0ID0gKHBheWxvYWQgb3Ige30pLmdldCgidGV4dCIsICIiKSBvciAiIgogICAgZG9jID0gbmxwKHRleHQpCiAgICBlbnRzID0gW3sidmFsdWUiOiBlbnQudGV4dCwgInR5cGUiOiBlbnQubGFiZWxffSBmb3IgZW50IGluIGRvYy5lbnRzXQogICAgZW50cy5leHRlbmQoX3JlZ2V4X2VudGl0aWVzKHRleHQpKQogICAgIyBEZS1kdXAgaWRlbnRpY2FsIHZhbHVlK3R5cGUgKHNwYUN5IGFuZCBhIHJlZ2V4IGNhbiBib3RoIHN1cmZhY2UgdGhlIHNhbWUgc3BhbikuCiAgICBzZWVuLCBvdXQgPSBzZXQoKSwgW10KICAgIGZvciBlIGluIGVudHM6CiAgICAgICAga2V5ID0gKGVbInR5cGUiXSwgZVsidmFsdWUiXS5sb3dlcigpKQogICAgICAgIGlmIGVbInZhbHVlIl0gYW5kIGtleSBub3QgaW4gc2VlbjoKICAgICAgICAgICAgc2Vlbi5hZGQoa2V5KQogICAgICAgICAgICBvdXQuYXBwZW5kKGUpCiAgICByZXR1cm4geyJlbnRpdGllcyI6IG91dH0K",
|
|
7
|
+
"requirements.txt": "ZmFzdGFwaQp1dmljb3JuW3N0YW5kYXJkXQpzcGFjeQo=",
|
|
8
|
+
};
|
package/src/ner.js
CHANGED
|
@@ -9,10 +9,32 @@
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
|
-
import { existsSync } from 'node:fs';
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'node:fs';
|
|
13
13
|
import os from 'node:os';
|
|
14
|
+
import { NER_ASSETS } from './ner-assets.js';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
// Where the NER server lives ON DISK. We always run from a real, writable user dir
|
|
17
|
+
// (~/.chatpanel/ner) rather than the package's ner/ — because a STANDALONE BINARY
|
|
18
|
+
// has no ner/ on the filesystem at all (it's inside the SEA snapshot), and a global
|
|
19
|
+
// npm dir may be read-only. The venv + installed deps persist here across restarts.
|
|
20
|
+
function materializeNerDir() {
|
|
21
|
+
const dir = join(os.homedir(), '.chatpanel', 'ner');
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
for (const [name, b64] of Object.entries(NER_ASSETS)) {
|
|
25
|
+
const target = join(dir, name);
|
|
26
|
+
const want = Buffer.from(b64, 'base64');
|
|
27
|
+
// Refresh the scripts if changed (pick up updates), but never touch .venv.
|
|
28
|
+
const have = existsSync(target) ? readFileSync(target) : null;
|
|
29
|
+
if (!have || !have.equals(want)) writeFileSync(target, want);
|
|
30
|
+
if (name.endsWith('.sh')) { try { chmodSync(target, 0o755); } catch { /* best effort */ } }
|
|
31
|
+
}
|
|
32
|
+
return dir;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.log(`[ner] could not materialize NER dir at ${dir} (${e.message})`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
// A login service (LaunchAgent / systemd) inherits a MINIMAL PATH — not your
|
|
18
40
|
// shell's — so `bash` and a pyenv/homebrew `python3` aren't found. Resolve bash
|
|
@@ -86,10 +108,14 @@ export function startNer(cfg) {
|
|
|
86
108
|
// 1) Adopt an existing NER already serving on the port (e.g. the user's own).
|
|
87
109
|
if (await nerReachable(port, ac.signal)) { wire('using existing NER'); return; }
|
|
88
110
|
|
|
89
|
-
// 2) Otherwise launch the bundled spaCy server.
|
|
111
|
+
// 2) Otherwise launch the bundled spaCy server — materializing run.sh +
|
|
112
|
+
// server.py + requirements.txt to a real on-disk dir first (the binary has
|
|
113
|
+
// no ner/ on the FS). run.sh then creates the venv, installs deps, and runs.
|
|
114
|
+
const nerDir = materializeNerDir();
|
|
115
|
+
if (!nerDir) { console.log('[ner] no writable NER dir — deterministic redaction only'); return; }
|
|
90
116
|
try {
|
|
91
117
|
child = spawn(resolveBash(), ['run.sh'], {
|
|
92
|
-
cwd:
|
|
118
|
+
cwd: nerDir,
|
|
93
119
|
env: {
|
|
94
120
|
...process.env,
|
|
95
121
|
PORT: String(port),
|
|
@@ -129,7 +155,7 @@ export function startNer(cfg) {
|
|
|
129
155
|
if (await nerReachable(port, ac.signal)) { wire('ready'); return; }
|
|
130
156
|
await sleep(1000);
|
|
131
157
|
}
|
|
132
|
-
if (!stopped) console.log(
|
|
158
|
+
if (!stopped) console.log(`[ner] not ready after 300s — continuing deterministic-only (run ${join(os.homedir(), '.chatpanel', 'ner', 'run.sh')} manually to debug).`);
|
|
133
159
|
})();
|
|
134
160
|
|
|
135
161
|
const stop = () => {
|
package/src/redact.js
CHANGED
|
@@ -29,9 +29,13 @@ export async function redactSegments(segments, redactionCfg, { signal, isPro = t
|
|
|
29
29
|
if (tier === 'full' && redactionCfg.detection?.backend && redactionCfg.detection.backend !== 'off') {
|
|
30
30
|
// One detection pass over the joined text — the detector is cached + fail-open,
|
|
31
31
|
// so a slow/broken NER service never blocks the request (deterministic layer
|
|
32
|
-
// still runs).
|
|
32
|
+
// still runs). Give it a generous CEILING (matching the extension): a fast NER
|
|
33
|
+
// returns in well under a second, but a slow/cold detector must be allowed to
|
|
34
|
+
// finish — if it times out, the turn falls back to dictionary/deterministic-only
|
|
35
|
+
// redaction (permanent pseudonyms the reply-restorer can't undo).
|
|
36
|
+
const detection = { ...redactionCfg.detection, timeoutMs: Math.max(Number(redactionCfg.detection.timeoutMs) || 0, 30000) };
|
|
33
37
|
try {
|
|
34
|
-
entities = await detectEntities(texts.join('\n\n'), { detection
|
|
38
|
+
entities = await detectEntities(texts.join('\n\n'), { detection }, { signal });
|
|
35
39
|
} catch {
|
|
36
40
|
entities = [];
|
|
37
41
|
}
|
package/src/server.js
CHANGED
|
@@ -33,7 +33,7 @@ import * as openai from './openai.js';
|
|
|
33
33
|
import * as responses from './responses.js';
|
|
34
34
|
import * as anthropic from './anthropic.js';
|
|
35
35
|
|
|
36
|
-
export const VERSION = '0.4.
|
|
36
|
+
export const VERSION = '0.4.2';
|
|
37
37
|
|
|
38
38
|
const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
|
|
39
39
|
|