@chatpanel/gateway 0.1.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/LICENSE +168 -0
- package/README.md +150 -0
- package/bin/chatpanel-gateway.js +43 -0
- package/gateway.config.example.json +33 -0
- package/ner/README.md +84 -0
- package/ner/requirements.txt +3 -0
- package/ner/run.sh +21 -0
- package/ner/server.py +95 -0
- package/package.json +53 -0
- package/src/anthropic.js +75 -0
- package/src/bridge.js +118 -0
- package/src/config.js +120 -0
- package/src/configstore.js +65 -0
- package/src/entitlement.js +81 -0
- package/src/freegate.js +44 -0
- package/src/ner.js +99 -0
- package/src/openai.js +53 -0
- package/src/redact.js +57 -0
- package/src/responses.js +74 -0
- package/src/server.js +297 -0
- package/src/service.js +145 -0
- package/src/shape.js +97 -0
- package/src/stream.js +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# PolyForm Shield License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/shield/1.0.0>
|
|
4
|
+
|
|
5
|
+
Required Notice: Copyright © 2026 ChatPanel (https://chatpanel.net)
|
|
6
|
+
|
|
7
|
+
Licensor Line of Business: ChatPanel — an AI browser side-panel, its local
|
|
8
|
+
bridge, and related developer tools and services (https://chatpanel.net)
|
|
9
|
+
|
|
10
|
+
## Acceptance
|
|
11
|
+
|
|
12
|
+
In order to get any license under these terms, you must agree
|
|
13
|
+
to them as both strict obligations and conditions to all
|
|
14
|
+
your licenses.
|
|
15
|
+
|
|
16
|
+
## Copyright License
|
|
17
|
+
|
|
18
|
+
The licensor grants you a copyright license for the
|
|
19
|
+
software to do everything you might do with the software
|
|
20
|
+
that would otherwise infringe the licensor's copyright
|
|
21
|
+
in it for any permitted purpose. However, you may
|
|
22
|
+
only distribute the software according to [Distribution
|
|
23
|
+
License](#distribution-license) and make changes or new works
|
|
24
|
+
based on the software according to [Changes and New Works
|
|
25
|
+
License](#changes-and-new-works-license).
|
|
26
|
+
|
|
27
|
+
## Distribution License
|
|
28
|
+
|
|
29
|
+
The licensor grants you an additional copyright license to
|
|
30
|
+
distribute copies of the software. Your license to distribute
|
|
31
|
+
covers distributing the software with changes and new works
|
|
32
|
+
permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
33
|
+
|
|
34
|
+
## Notices
|
|
35
|
+
|
|
36
|
+
You must ensure that anyone who gets a copy of any part of
|
|
37
|
+
the software from you also gets a copy of these terms or the
|
|
38
|
+
URL for them above, as well as copies of any plain-text lines
|
|
39
|
+
beginning with `Required Notice:` that the licensor provided
|
|
40
|
+
with the software. For example:
|
|
41
|
+
|
|
42
|
+
> Required Notice: Copyright © 2026 ChatPanel (https://chatpanel.net)
|
|
43
|
+
|
|
44
|
+
## Changes and New Works License
|
|
45
|
+
|
|
46
|
+
The licensor grants you an additional copyright license to
|
|
47
|
+
make changes and new works based on the software for any
|
|
48
|
+
permitted purpose.
|
|
49
|
+
|
|
50
|
+
## Patent License
|
|
51
|
+
|
|
52
|
+
The licensor grants you a patent license for the software that
|
|
53
|
+
covers patent claims the licensor can license, or becomes able
|
|
54
|
+
to license, that you would infringe by using the software.
|
|
55
|
+
|
|
56
|
+
## Noncompete
|
|
57
|
+
|
|
58
|
+
Any purpose is a permitted purpose, except for providing any
|
|
59
|
+
product that competes with the software or any product the
|
|
60
|
+
licensor or any of its affiliates provides using the software.
|
|
61
|
+
|
|
62
|
+
## Competition
|
|
63
|
+
|
|
64
|
+
Goods and services compete even when they provide functionality
|
|
65
|
+
through different kinds of interfaces or for different technical
|
|
66
|
+
platforms. Applications can compete with services, libraries
|
|
67
|
+
with plugins, frameworks with development tools, and so on,
|
|
68
|
+
even if they're written in different programming languages
|
|
69
|
+
or for different computer architectures. Goods and services
|
|
70
|
+
compete even when provided free of charge. If you market a
|
|
71
|
+
product as a practical substitute for the software or another
|
|
72
|
+
product, it definitely competes.
|
|
73
|
+
|
|
74
|
+
## New Products
|
|
75
|
+
|
|
76
|
+
If you are using the software to provide a product that does
|
|
77
|
+
not compete, but the licensor or any of its affiliates brings
|
|
78
|
+
your product into competition by providing a new version of
|
|
79
|
+
the software or another product using the software, you may
|
|
80
|
+
continue using versions of the software available under these
|
|
81
|
+
terms beforehand to provide your competing product, but not
|
|
82
|
+
any later versions.
|
|
83
|
+
|
|
84
|
+
## Discontinued Products
|
|
85
|
+
|
|
86
|
+
You may begin using the software to compete with a product
|
|
87
|
+
or service that the licensor or any of its affiliates has
|
|
88
|
+
stopped providing, unless the licensor includes a plain-text
|
|
89
|
+
line beginning with `Licensor Line of Business:` with the
|
|
90
|
+
software that mentions that line of business. For example:
|
|
91
|
+
|
|
92
|
+
> Licensor Line of Business: ChatPanel — an AI browser side-panel, its local
|
|
93
|
+
> bridge, and related developer tools and services (https://chatpanel.net)
|
|
94
|
+
|
|
95
|
+
## Sales of Business
|
|
96
|
+
|
|
97
|
+
If the licensor or any of its affiliates sells a line of
|
|
98
|
+
business developing the software or using the software
|
|
99
|
+
to provide a product, the buyer can also enforce
|
|
100
|
+
Noncompete for that product.
|
|
101
|
+
|
|
102
|
+
## Fair Use
|
|
103
|
+
|
|
104
|
+
You may have "fair use" rights for the software under the
|
|
105
|
+
law. These terms do not limit them.
|
|
106
|
+
|
|
107
|
+
## No Other Rights
|
|
108
|
+
|
|
109
|
+
These terms do not allow you to sublicense or transfer any of
|
|
110
|
+
your licenses to anyone else, or prevent the licensor from
|
|
111
|
+
granting licenses to anyone else. These terms do not imply
|
|
112
|
+
any other licenses.
|
|
113
|
+
|
|
114
|
+
## Patent Defense
|
|
115
|
+
|
|
116
|
+
If you make any written claim that the software infringes or
|
|
117
|
+
contributes to infringement of any patent, your patent license
|
|
118
|
+
for the software granted under these terms ends immediately. If
|
|
119
|
+
your company makes such a claim, your patent license ends
|
|
120
|
+
immediately for work on behalf of your company.
|
|
121
|
+
|
|
122
|
+
## Violations
|
|
123
|
+
|
|
124
|
+
The first time you are notified in writing that you have
|
|
125
|
+
violated any of these terms, or done anything with the software
|
|
126
|
+
not covered by your licenses, your licenses can nonetheless
|
|
127
|
+
continue if you come into full compliance with these terms,
|
|
128
|
+
and take practical steps to correct past violations, within
|
|
129
|
+
32 days of receiving notice. Otherwise, all your licenses
|
|
130
|
+
end immediately.
|
|
131
|
+
|
|
132
|
+
## No Liability
|
|
133
|
+
|
|
134
|
+
***As far as the law allows, the software comes as is, without
|
|
135
|
+
any warranty or condition, and the licensor will not be liable
|
|
136
|
+
to you for any damages arising out of these terms or the use
|
|
137
|
+
or nature of the software, under any kind of legal claim.***
|
|
138
|
+
|
|
139
|
+
## Definitions
|
|
140
|
+
|
|
141
|
+
The **licensor** is the individual or entity offering these
|
|
142
|
+
terms, and the **software** is the software the licensor makes
|
|
143
|
+
available under these terms.
|
|
144
|
+
|
|
145
|
+
A **product** can be a good or service, or a combination
|
|
146
|
+
of them.
|
|
147
|
+
|
|
148
|
+
**You** refers to the individual or entity agreeing to these
|
|
149
|
+
terms.
|
|
150
|
+
|
|
151
|
+
**Your company** is any legal entity, sole proprietorship,
|
|
152
|
+
or other kind of organization that you work for, plus all
|
|
153
|
+
its affiliates.
|
|
154
|
+
|
|
155
|
+
**Affiliates** means the other organizations that an
|
|
156
|
+
organization has control over, is under the control of, or is
|
|
157
|
+
under common control with.
|
|
158
|
+
|
|
159
|
+
**Control** means ownership of substantially all the assets of
|
|
160
|
+
an entity, or the power to direct its management and policies
|
|
161
|
+
by vote, contract, or otherwise. Control can be direct or
|
|
162
|
+
indirect.
|
|
163
|
+
|
|
164
|
+
**Your licenses** are all the licenses granted to you for the
|
|
165
|
+
software under these terms.
|
|
166
|
+
|
|
167
|
+
**Use** means anything you do with the software requiring one
|
|
168
|
+
of your licenses.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# ChatPanel Privacy Gateway
|
|
2
|
+
|
|
3
|
+
A localhost server that puts **ChatPanel's PII redaction / pseudonymization in the
|
|
4
|
+
middle of two CLI agents** — so you can use the privacy features outside the
|
|
5
|
+
ChatPanel extension. Point [opencode](https://opencode.ai) / pi at the gateway,
|
|
6
|
+
and it drives **codex / Claude Code behind your existing subscription login**
|
|
7
|
+
(via the [bridge](https://github.com/chatpanel/chatpanel-bridge)), redacting on
|
|
8
|
+
the way out and restoring on the way back. The model only ever sees opaque
|
|
9
|
+
placeholders like `[[PERSON_1]]` / `[[EMAIL_2]]` — **the real values never leave
|
|
10
|
+
your machine.**
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
opencode / pi (configured with a custom provider → the gateway)
|
|
14
|
+
│ baseURL → http://127.0.0.1:4320/v1
|
|
15
|
+
▼
|
|
16
|
+
┌──────────────────────────────────────────────┐
|
|
17
|
+
│ ChatPanel Privacy Gateway │
|
|
18
|
+
│ 1. detect + redact → [[PERSON_1]] … │
|
|
19
|
+
│ 2. drive the agent behind your login │
|
|
20
|
+
│ 3. restore placeholders in the reply │
|
|
21
|
+
└──────────────────────────────────────────────┘
|
|
22
|
+
│ POST /chat (bridge: subscription-authed CLI)
|
|
23
|
+
▼
|
|
24
|
+
chatpanel-bridge ──spawns──▶ codex / claude (your ChatGPT / enterprise / Claude login)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Two backends (config `backend`):
|
|
28
|
+
|
|
29
|
+
- **`bridge`** (default) — drive the bridge's subscription-authed CLI agents
|
|
30
|
+
(`codex` / `claude` / `opencode` / `pi`). No API keys, uses your login. This is
|
|
31
|
+
the "privacy bridge between two agents" path above.
|
|
32
|
+
- **`api`** — forward redacted traffic to a native OpenAI/Anthropic-compatible
|
|
33
|
+
endpoint (local models, BYO keys). The client's own auth header passes through;
|
|
34
|
+
the gateway stores no keys.
|
|
35
|
+
|
|
36
|
+
The redaction engine is the **same code** the ChatPanel extension runs — the
|
|
37
|
+
[`chatpanel-pii`](https://github.com/chatpanel/chatpanel-pii) package is the
|
|
38
|
+
single source of truth, so a privacy feature added once is shared everywhere.
|
|
39
|
+
|
|
40
|
+
## Quick start (bridge backend)
|
|
41
|
+
|
|
42
|
+
You need the [ChatPanel bridge](https://github.com/chatpanel/chatpanel-bridge)
|
|
43
|
+
running and logged into codex/claude (the same bridge the extension uses).
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g chatpanel-gateway
|
|
47
|
+
chatpanel-gateway
|
|
48
|
+
# → ChatPanel Privacy Gateway v0.1.0 on http://127.0.0.1:4320
|
|
49
|
+
# backend : bridge (agent: codex, via http://127.0.0.1:4319)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then point your front-end agent at it. **opencode** (`opencode.json`):
|
|
53
|
+
|
|
54
|
+
```jsonc
|
|
55
|
+
{
|
|
56
|
+
"provider": {
|
|
57
|
+
"chatpanel": {
|
|
58
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
59
|
+
"options": { "baseURL": "http://127.0.0.1:4320/v1" },
|
|
60
|
+
"models": { "codex": {}, "claude": {} } // selects the agent behind the gateway
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Now opencode talks to codex **through** the gateway — every prompt is redacted
|
|
67
|
+
before codex sees it, and the reply is restored before opencode renders it. The
|
|
68
|
+
request's `model` (`codex`/`claude`/`opencode`/`pi`) picks which agent the bridge
|
|
69
|
+
drives; otherwise the configured default (`codex`) is used.
|
|
70
|
+
|
|
71
|
+
### Using the api backend instead (local models / BYO keys)
|
|
72
|
+
|
|
73
|
+
Set `backend: "api"` (see config) and the gateway forwards redacted traffic to a
|
|
74
|
+
real provider endpoint, passing your `Authorization` / `x-api-key` through:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export OPENAI_BASE_URL=http://127.0.0.1:4320/v1 # OpenAI / codex / aider / cursor
|
|
78
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:4320 # Claude Code / Anthropic SDK
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Name/org redaction is built in (bundled NER)
|
|
82
|
+
|
|
83
|
+
Deterministic redaction (emails, phones, cards, SSNs, API keys, IPs) needs no
|
|
84
|
+
setup. To also blind **names, organizations and locations**, the gateway bundles
|
|
85
|
+
a local spaCy NER server under [`ner/`](ner) and — with `ner.autostart` on (the
|
|
86
|
+
default) — **launches it for you on startup** and switches redaction to the
|
|
87
|
+
`full` tier once it's healthy. First run creates a venv and downloads the model
|
|
88
|
+
(needs `python3`); it's fail-open, so if Python isn't set up the gateway just runs
|
|
89
|
+
deterministic-only.
|
|
90
|
+
|
|
91
|
+
Prefer a local LLM or an external NER service? Set `redaction.detection` yourself
|
|
92
|
+
and the gateway won't autostart the bundled one.
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
Precedence: defaults < `gateway.config.json` (or `$CHATPANEL_GATEWAY_CONFIG`) <
|
|
97
|
+
env vars. See [`gateway.config.example.json`](gateway.config.example.json).
|
|
98
|
+
|
|
99
|
+
| Key | Env | Default | Meaning |
|
|
100
|
+
|-----|-----|---------|---------|
|
|
101
|
+
| `backend` | — | `bridge` | `bridge` (drive CLI agents via login) or `api` (forward to a provider) |
|
|
102
|
+
| `bridge.url` | — | `http://127.0.0.1:4319` | the ChatPanel bridge |
|
|
103
|
+
| `bridge.agent` | — | `codex` | default agent the bridge drives |
|
|
104
|
+
| `bridge.token` | — | _(auto)_ | bridge bearer token; empty = read `~/.chatpanel/bridge-token` |
|
|
105
|
+
| `host` / `port` | `CHATPANEL_GATEWAY_HOST` / `_PORT` | `127.0.0.1` / `4320` | bind address |
|
|
106
|
+
| `upstreams.openai.baseUrl` | `OPENAI_BASE_URL` | `https://api.openai.com` | api backend only |
|
|
107
|
+
| `upstreams.anthropic.baseUrl` | `ANTHROPIC_BASE_URL` | `https://api.anthropic.com` | api backend only |
|
|
108
|
+
| `redaction.tier` | `CHATPANEL_REDACTION_TIER` | `basic` | `basic` (regex) or `full` (+ NER + dictionary) |
|
|
109
|
+
| `redaction.detection` | — | _(auto via NER)_ | local detector; set to override the bundled one |
|
|
110
|
+
| `redaction.dictionary` | — | `[]` | custom `{ value\|pattern, type, alias? }` entries |
|
|
111
|
+
| `ner.autostart` / `ner.port` | — | `true` / `9009` | launch + wire the bundled spaCy NER |
|
|
112
|
+
|
|
113
|
+
## Endpoints
|
|
114
|
+
|
|
115
|
+
| Route | Behavior |
|
|
116
|
+
|-------|----------|
|
|
117
|
+
| `GET /health` | `{ ok, version, backend, tier }` |
|
|
118
|
+
| `GET /v1/models` | the agent(s) this gateway exposes |
|
|
119
|
+
| `POST /v1/chat/completions` | OpenAI protocol — redact → backend → restore |
|
|
120
|
+
| `POST /v1/responses` | OpenAI Responses protocol (Codex) |
|
|
121
|
+
| `POST /v1/messages` | Anthropic protocol (Claude Code) |
|
|
122
|
+
|
|
123
|
+
Streaming (SSE) is supported on all three: placeholders are restored on the fly,
|
|
124
|
+
holding back a tail so a token split across chunks (`[[PER` … `SON_1]]`) still
|
|
125
|
+
restores cleanly.
|
|
126
|
+
|
|
127
|
+
## How it fits with ChatPanel
|
|
128
|
+
|
|
129
|
+
The [extension](https://github.com/chatpanel/chatpanel-extension) redacts inside
|
|
130
|
+
the browser; the [bridge](https://github.com/chatpanel/chatpanel-bridge) lets the
|
|
131
|
+
browser drive local CLI agents. This gateway reuses the bridge to put the **same
|
|
132
|
+
redaction engine** ([`chatpanel-pii`](https://github.com/chatpanel/chatpanel-pii))
|
|
133
|
+
in front of *any* agent — so non-browser tools get the privacy too, and the
|
|
134
|
+
agent's own multi-turn loop is blinded, not just the first prompt.
|
|
135
|
+
|
|
136
|
+
## Caveats
|
|
137
|
+
|
|
138
|
+
- **Reversibility** is best-effort: if the model paraphrases a placeholder instead
|
|
139
|
+
of echoing it, that one reference shows the token. The privacy guarantee (the
|
|
140
|
+
real value never left the device) always holds.
|
|
141
|
+
- A dictionary **alias** is a *permanent* pseudonym — the agent sees the alias,
|
|
142
|
+
not the original, by design.
|
|
143
|
+
- **Code edits**: redacting values that appear inside source can affect round-trip
|
|
144
|
+
edits. The default tier touches only structured secrets and (in `full`) detected
|
|
145
|
+
entities — keep your dictionary prose-focused.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
Source-available under the same license as the ChatPanel extension and bridge —
|
|
150
|
+
see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI entry for the ChatPanel Privacy Gateway.
|
|
3
|
+
//
|
|
4
|
+
// chatpanel-gateway start the gateway (foreground)
|
|
5
|
+
// chatpanel-gateway --install register login auto-start + start now
|
|
6
|
+
// chatpanel-gateway --uninstall remove login auto-start
|
|
7
|
+
// chatpanel-gateway --status is auto-start registered?
|
|
8
|
+
// chatpanel-gateway --version print version
|
|
9
|
+
//
|
|
10
|
+
// Config comes from gateway.config.json / env (see src/config.js).
|
|
11
|
+
import { start, VERSION } from '../src/server.js';
|
|
12
|
+
import { installService, uninstallService, serviceStatus } from '../src/service.js';
|
|
13
|
+
|
|
14
|
+
const arg = process.argv[2];
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
switch (arg) {
|
|
18
|
+
case '--version':
|
|
19
|
+
case '-v':
|
|
20
|
+
console.log(VERSION);
|
|
21
|
+
break;
|
|
22
|
+
case '--install':
|
|
23
|
+
installService();
|
|
24
|
+
console.log('ChatPanel Privacy Gateway: installed login auto-start and started it.');
|
|
25
|
+
break;
|
|
26
|
+
case '--uninstall':
|
|
27
|
+
uninstallService();
|
|
28
|
+
console.log('ChatPanel Privacy Gateway: removed login auto-start.');
|
|
29
|
+
break;
|
|
30
|
+
case '--status':
|
|
31
|
+
console.log(serviceStatus() ? 'installed (auto-start registered)' : 'not installed');
|
|
32
|
+
break;
|
|
33
|
+
case undefined:
|
|
34
|
+
start();
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
console.error(`unknown option: ${arg}\nUsage: chatpanel-gateway [--install|--uninstall|--status|--version]`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error(`error: ${e.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"host": "127.0.0.1",
|
|
3
|
+
"port": 4320,
|
|
4
|
+
|
|
5
|
+
"backend": "bridge",
|
|
6
|
+
"bridge": {
|
|
7
|
+
"url": "http://127.0.0.1:4319",
|
|
8
|
+
"agent": "codex",
|
|
9
|
+
"token": ""
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
"upstreams": {
|
|
13
|
+
"openai": { "baseUrl": "https://api.openai.com" },
|
|
14
|
+
"anthropic": { "baseUrl": "https://api.anthropic.com" }
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
"redaction": {
|
|
18
|
+
"tier": "full",
|
|
19
|
+
"redactSystem": true,
|
|
20
|
+
"dictionary": [
|
|
21
|
+
{ "value": "Acme Corp", "type": "ORG" },
|
|
22
|
+
{ "value": "Project Atlas", "alias": "Project Nimbus" }
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
"ner": {
|
|
27
|
+
"autostart": true,
|
|
28
|
+
"port": 9009,
|
|
29
|
+
"enableFullTier": true
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"logRequests": true
|
|
33
|
+
}
|
package/ner/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ChatPanel — local NER helper (spaCy)
|
|
2
|
+
|
|
3
|
+
A ~30-line local service that lets ChatPanel **auto-redact names, organizations,
|
|
4
|
+
and locations** before anything is sent to a chat model. Detection runs entirely
|
|
5
|
+
on your machine; the model only ever sees placeholders like `[[PERSON_1]]`.
|
|
6
|
+
|
|
7
|
+
ChatPanel's redaction contract is simple — any local HTTP service works:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
POST /ner { "text": "..." }
|
|
11
|
+
→ { "entities": [ { "value": "Alex", "type": "PERSON" }, ... ] }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
(spaCy's `{ "ents": [{ "text", "label" }] }` shape is also accepted, as is a
|
|
15
|
+
local OpenAI-compatible LLM — see "Other detectors" below.)
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
Most machines block installing Python packages globally, so use a virtual env:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd helpers/ner-server
|
|
23
|
+
|
|
24
|
+
python3 -m venv .venv
|
|
25
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
26
|
+
pip install -r requirements.txt
|
|
27
|
+
python -m spacy download en_core_web_sm
|
|
28
|
+
|
|
29
|
+
uvicorn server:app --port 9009 # the file is server.py → import path "server:app"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
…or just run the bundled script (does all of the above):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
./run.sh # PORT=9100 ./run.sh to change the port
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Check it's up: `curl http://127.0.0.1:9009/health`
|
|
39
|
+
|
|
40
|
+
## Point ChatPanel at it
|
|
41
|
+
|
|
42
|
+
**Settings → Privacy** (or the 🛡 button in the chat composer):
|
|
43
|
+
|
|
44
|
+
| Field | Value |
|
|
45
|
+
|------|-------|
|
|
46
|
+
| Redaction | **On — + AI detection** |
|
|
47
|
+
| Detector | **Local NER service (spaCy / Presidio)** |
|
|
48
|
+
| Detector URL | `http://127.0.0.1:9009/ner` |
|
|
49
|
+
| Redact types | People / Organizations / Locations / Numbers (your choice) |
|
|
50
|
+
|
|
51
|
+
Now `my name is Alex from Denver` is sent to the model as
|
|
52
|
+
`my name is [[PERSON_1]] from [[LOCATION_1]]`, and the reply is restored to the
|
|
53
|
+
real values in your view.
|
|
54
|
+
|
|
55
|
+
> Turning **Locations** off keeps city names readable (useful for "how far is X
|
|
56
|
+
> from Y" questions) while still redacting people.
|
|
57
|
+
|
|
58
|
+
## Accuracy vs. speed
|
|
59
|
+
|
|
60
|
+
`en_core_web_sm` is small and fast (good default). For fewer misses:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -m spacy download en_core_web_md # or en_core_web_trf (best, heavier)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
then change `MODEL` in `server.py`. Small models can over-tag short acronyms as
|
|
67
|
+
`ORG`; ChatPanel drops noisy short numerics/dates automatically and lets you turn
|
|
68
|
+
off whole categories.
|
|
69
|
+
|
|
70
|
+
## Other detectors
|
|
71
|
+
|
|
72
|
+
ChatPanel doesn't care what's behind the URL, as long as it returns the entity
|
|
73
|
+
shape above. Drop-in alternatives:
|
|
74
|
+
|
|
75
|
+
- **Microsoft Presidio** — `presidio-analyzer` behind a small FastAPI wrapper
|
|
76
|
+
(returns `{ "results": [...] }`, also accepted).
|
|
77
|
+
- **A local LLM** — set the detector to **Local LLM (OpenAI-compatible)** and
|
|
78
|
+
point it at Ollama / LM Studio / llama.cpp (e.g. `http://127.0.0.1:11434`);
|
|
79
|
+
ChatPanel prompts it for strict JSON entities. Slower than spaCy, no extra
|
|
80
|
+
service to run if you already have a local model. See `../local-model.md`.
|
|
81
|
+
|
|
82
|
+
Everything is local and latency-guarded (cached + timed out + fail-open): if the
|
|
83
|
+
detector is slow or down, ChatPanel falls back to deterministic redaction so chat
|
|
84
|
+
never blocks.
|
package/ner/run.sh
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# One-shot: create the venv (if needed), install deps + the spaCy model, and serve.
|
|
3
|
+
# Usage: ./run.sh (defaults to port 9009)
|
|
4
|
+
# PORT=9100 ./run.sh
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
cd "$(dirname "$0")"
|
|
7
|
+
|
|
8
|
+
if [ ! -d .venv ]; then
|
|
9
|
+
echo "→ creating virtual env (.venv)…"
|
|
10
|
+
python3 -m venv .venv
|
|
11
|
+
fi
|
|
12
|
+
# shellcheck disable=SC1091
|
|
13
|
+
source .venv/bin/activate
|
|
14
|
+
|
|
15
|
+
echo "→ installing dependencies…"
|
|
16
|
+
pip install -q --upgrade pip
|
|
17
|
+
pip install -q -r requirements.txt
|
|
18
|
+
python -c "import en_core_web_sm" >/dev/null 2>&1 || python -m spacy download en_core_web_sm
|
|
19
|
+
|
|
20
|
+
echo "→ serving on http://127.0.0.1:${PORT:-9009}/ner (Ctrl-C to stop)"
|
|
21
|
+
exec uvicorn server:app --port "${PORT:-9009}"
|
package/ner/server.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatPanel local NER helper — a tiny on-device entity detector for PII redaction.
|
|
3
|
+
|
|
4
|
+
ChatPanel's Privacy → "AI detection" can point at any LOCAL service that accepts
|
|
5
|
+
POST {"text": "..."}
|
|
6
|
+
and returns
|
|
7
|
+
{"entities": [{"value": "...", "type": "PERSON|ORG|GPE|EMAIL|PHONE|..."}]}
|
|
8
|
+
|
|
9
|
+
This wraps spaCy (people / organizations / locations) AND adds a regex pass for
|
|
10
|
+
the structured identifiers spaCy doesn't emit (emails, phone numbers, SSNs, cards,
|
|
11
|
+
IPs), so the detector is comprehensive on its own. Only the redacted placeholders
|
|
12
|
+
(e.g. [[PERSON_1]]) ever reach the chat model — the raw text never leaves your box.
|
|
13
|
+
|
|
14
|
+
--------------------------------------------------------------------------------
|
|
15
|
+
Setup (most machines block global pip installs, so use a virtual env)
|
|
16
|
+
|
|
17
|
+
python3 -m venv .venv
|
|
18
|
+
source .venv/bin/activate # Windows: .venv\\Scripts\\activate
|
|
19
|
+
pip install -r requirements.txt
|
|
20
|
+
python -m spacy download en_core_web_sm
|
|
21
|
+
|
|
22
|
+
Run (the file is server.py, so the uvicorn import path is "server:app")
|
|
23
|
+
|
|
24
|
+
uvicorn server:app --port 9009
|
|
25
|
+
|
|
26
|
+
Then in ChatPanel → Settings → Privacy:
|
|
27
|
+
Redaction : On — + AI detection
|
|
28
|
+
Detector : Local NER service (spaCy / Presidio)
|
|
29
|
+
URL : http://127.0.0.1:9009/ner
|
|
30
|
+
|
|
31
|
+
Tip: en_core_web_sm is small + fast. For better accuracy use en_core_web_md or
|
|
32
|
+
en_core_web_trf (download the same way, then change the load below).
|
|
33
|
+
--------------------------------------------------------------------------------
|
|
34
|
+
"""
|
|
35
|
+
from fastapi import FastAPI
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
import spacy
|
|
39
|
+
|
|
40
|
+
MODEL = "en_core_web_sm"
|
|
41
|
+
nlp = spacy.load(MODEL)
|
|
42
|
+
|
|
43
|
+
app = FastAPI(title="ChatPanel NER helper")
|
|
44
|
+
|
|
45
|
+
# spaCy's NER emits names / orgs / locations but NOT structured identifiers, so add
|
|
46
|
+
# a regex pass for those. (ChatPanel also catches these on-device, but emitting them
|
|
47
|
+
# here keeps the detector self-contained — what you test is what you get.) Order
|
|
48
|
+
# matters: more specific patterns run first so a card / SSN isn't re-matched as a
|
|
49
|
+
# phone number.
|
|
50
|
+
_PATTERNS = [
|
|
51
|
+
("EMAIL", re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}")),
|
|
52
|
+
("SSN", re.compile(r"\b\d{3}-\d{2}-\d{4}\b")),
|
|
53
|
+
("CREDIT_CARD", re.compile(r"\b(?:\d[ -]?){13,19}\b")),
|
|
54
|
+
("IP", re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")),
|
|
55
|
+
("PHONE", re.compile(r"(?<!\d)(?:\+?\d{1,2}[ .\-]?)?\(?\d{3}\)?[ .\-]?\d{3}[ .\-]?\d{4}(?!\d)")),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _regex_entities(text):
|
|
60
|
+
out, taken = [], []
|
|
61
|
+
for label, rx in _PATTERNS:
|
|
62
|
+
for m in rx.finditer(text):
|
|
63
|
+
start, end = m.start(), m.end()
|
|
64
|
+
if any(start < te and ts < end for ts, te in taken):
|
|
65
|
+
continue # span already claimed by a more specific pattern
|
|
66
|
+
taken.append((start, end))
|
|
67
|
+
out.append({"value": m.group(0).strip(), "type": label})
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.get("/health")
|
|
72
|
+
def health():
|
|
73
|
+
return {"ok": True, "model": MODEL}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.post("/ner")
|
|
77
|
+
def ner(payload: dict):
|
|
78
|
+
"""Return spaCy entities + regex identifiers in ChatPanel's expected shape.
|
|
79
|
+
|
|
80
|
+
ChatPanel maps common labels itself (PER→PERSON, GPE→LOCATION, …) and keeps
|
|
81
|
+
only the categories you enable in the Privacy tab, so it's fine to return all
|
|
82
|
+
of them here.
|
|
83
|
+
"""
|
|
84
|
+
text = (payload or {}).get("text", "") or ""
|
|
85
|
+
doc = nlp(text)
|
|
86
|
+
ents = [{"value": ent.text, "type": ent.label_} for ent in doc.ents]
|
|
87
|
+
ents.extend(_regex_entities(text))
|
|
88
|
+
# De-dup identical value+type (spaCy and a regex can both surface the same span).
|
|
89
|
+
seen, out = set(), []
|
|
90
|
+
for e in ents:
|
|
91
|
+
key = (e["type"], e["value"].lower())
|
|
92
|
+
if e["value"] and key not in seen:
|
|
93
|
+
seen.add(key)
|
|
94
|
+
out.append(e)
|
|
95
|
+
return {"entities": out}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chatpanel/gateway",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"chatpanel-gateway": "bin/chatpanel-gateway.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/server.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/server.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node bin/chatpanel-gateway.js",
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
17
|
+
"build:bin": "bash scripts/build-binaries.sh"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"bin",
|
|
22
|
+
"ner",
|
|
23
|
+
"gateway.config.example.json",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@chatpanel/pii": "^0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://chatpanel.net",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/chatpanel/chatpanel-gateway.git"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"privacy",
|
|
40
|
+
"pii",
|
|
41
|
+
"redaction",
|
|
42
|
+
"pseudonymization",
|
|
43
|
+
"llm",
|
|
44
|
+
"proxy",
|
|
45
|
+
"gateway",
|
|
46
|
+
"openai",
|
|
47
|
+
"anthropic",
|
|
48
|
+
"opencode",
|
|
49
|
+
"codex"
|
|
50
|
+
],
|
|
51
|
+
"author": "ChatPanel (https://chatpanel.net)",
|
|
52
|
+
"license": "SEE LICENSE IN LICENSE"
|
|
53
|
+
}
|