@chatpanel/pii 0.2.1 → 0.2.3

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/index.js CHANGED
@@ -14,3 +14,4 @@ export * from './pii-redact.js';
14
14
  export * from './pii-detect.js';
15
15
  export * from './pipeline.js';
16
16
  export * from './tool-rank.js';
17
+ export * from './tool-harness.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/pii",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "The canonical ChatPanel privacy engine — reversible PII redaction + pseudonymization with local entity detection. Pure, dependency-free ESM shared by the ChatPanel extension, gateway, and bridge.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -9,7 +9,8 @@
9
9
  "./pii-redact.js": "./pii-redact.js",
10
10
  "./pii-detect.js": "./pii-detect.js",
11
11
  "./pipeline.js": "./pipeline.js",
12
- "./tool-rank.js": "./tool-rank.js"
12
+ "./tool-rank.js": "./tool-rank.js",
13
+ "./tool-harness.js": "./tool-harness.js"
13
14
  },
14
15
  "files": [
15
16
  "index.js",
@@ -17,6 +18,7 @@
17
18
  "pii-detect.js",
18
19
  "pipeline.js",
19
20
  "tool-rank.js",
21
+ "tool-harness.js",
20
22
  "LICENSE",
21
23
  "README.md"
22
24
  ],
package/pii-redact.js CHANGED
@@ -20,6 +20,14 @@
20
20
 
21
21
  const TOKEN_RE = /\[\[([A-Z][A-Z0-9]*)_(\d+)\]\]/g;
22
22
 
23
+ // Bracket-TOLERANT match of the same token. Smaller models routinely drop or mangle
24
+ // the [[ ]] when echoing a placeholder into tool-call JSON — e.g. they emit "ORG_1"
25
+ // or "[ORG_1]" instead of "[[ORG_1]]" — which the strict TOKEN_RE misses, leaving
26
+ // the tool to search the literal "ORG_1" (and get nothing). We match 0–2 brackets
27
+ // on each side and reconstruct the canonical token to look up; only ACTUAL vault
28
+ // tokens are swapped, so a coincidental "ABC_1" that isn't ours is left untouched.
29
+ const TOLERANT_TOKEN_RE = /\[{0,2}([A-Z][A-Z0-9]*_\d+)\]{0,2}/g;
30
+
23
31
  // A vault is the per-conversation mapping between placeholders and originals. Keep
24
32
  // one per conversation so PERSON_1 means the same entity across turns.
25
33
  export function createVault() {
@@ -168,7 +176,10 @@ export function redactText(text, vault, {
168
176
  // Swap placeholders back to their originals. Unknown tokens are left untouched.
169
177
  export function restoreText(text, vault) {
170
178
  if (text == null || !vault) return text;
171
- return String(text).replace(TOKEN_RE, (m) => (vault.byToken.has(m) ? vault.byToken.get(m) : m));
179
+ return String(text).replace(TOLERANT_TOKEN_RE, (m, inner) => {
180
+ const canonical = `[[${inner}]]`;
181
+ return vault.byToken.has(canonical) ? vault.byToken.get(canonical) : m;
182
+ });
172
183
  }
173
184
 
174
185
  // Restore for LOCAL use only — e.g. tool-call args that hit on-device history /
@@ -0,0 +1,79 @@
1
+ // THE tool harness — one interception layer shared by every orchestrator
2
+ // (ChatPanel API + agent, gateway API + relay) so tool handling can't drift. It
3
+ // owns the boundaries around a model turn:
4
+ //
5
+ // ⓪ selectTools — inject + narrow the tools the model is offered (MCP-auto).
6
+ // ② toTool — what a tool RECEIVES: real values (so on-device / remote
7
+ // lookups work), or the redacted token for remote MCP tools
8
+ // when the user chose "redact remote".
9
+ // ③ toModelResult— what the MODEL sees back: the tool result re-redacted so it
10
+ // stays blinded.
11
+ // ④ toUser — the final reply: reversible tokens restored (pseudonyms stay).
12
+ //
13
+ // PRIVACY IS OPTIONAL. With no `vault` (redaction off), ②③④ are pass-throughs —
14
+ // no latency, no placeholder confusion — but ⓪ selectTools STILL narrows, because
15
+ // not every turn is privacy-sensitive yet every turn benefits from fewer tools.
16
+ //
17
+ // Self-contained on the SYNCED engine files (pii-redact.js, tool-rank.js), so the
18
+ // extension (browser ESM) and the gateway (npm) run the exact same code. The caller
19
+ // passes the already-gated `redactOpts` ({tier, entities, dictionary}) it computed
20
+ // from cfg+isPro — keeping tier/dictionary Pro-gating out of the harness.
21
+
22
+ import { restoreText, restoreWithAliases, redactText } from './pii-redact.js';
23
+ import { narrowSpecs } from './tool-rank.js';
24
+
25
+ // MCP / remote tools are server-prefixed (mcp_server__tool). Local tools
26
+ // (history/meeting/page, or a client's core bash/read) are not — they always get
27
+ // real values and are never narrowed away.
28
+ export const isRemoteToolName = (name) => /^mcp[_-]/i.test(String(name || ''));
29
+
30
+ // Deep restore of a tool-call argument value, undoing reversible tokens AND
31
+ // pseudonyms (tools run locally / on real data; only the model stays blinded).
32
+ export function restoreToolArgs(value, vault) {
33
+ if (!vault) return value;
34
+ if (typeof value === 'string') return restoreWithAliases(value, vault);
35
+ if (Array.isArray(value)) return value.map((v) => restoreToolArgs(v, vault));
36
+ if (value && typeof value === 'object') {
37
+ const out = {};
38
+ for (const k of Object.keys(value)) out[k] = restoreToolArgs(value[k], vault);
39
+ return out;
40
+ }
41
+ return value;
42
+ }
43
+
44
+ export function makeToolHarness({ vault = null, toolData = 'real', redactOpts = null, redactResults = true } = {}) {
45
+ const on = !!vault; // privacy enabled for this turn?
46
+ const redactRemote = toolData === 'redactRemote';
47
+ return {
48
+ enabled: on,
49
+ isRemoteTool: isRemoteToolName,
50
+
51
+ // ⓪ Always-on tool selection (privacy-independent). `available` is any spec
52
+ // list; `opts` forwards { cap, keep, name, description } to the shared ranker.
53
+ selectTools(available, query, opts = {}) {
54
+ return narrowSpecs(available, query, opts);
55
+ },
56
+
57
+ // ② What the tool receives.
58
+ toTool(name, args) {
59
+ if (!on) return args; // privacy off → already real
60
+ if (redactRemote && isRemoteToolName(name)) return args; // keep PII off remote MCP
61
+ return restoreToolArgs(args, vault); // real values for the tool
62
+ },
63
+
64
+ // ③ What the model sees back (re-redacted). Handles a string or a { text } shape.
65
+ toModelResult(name, raw) {
66
+ if (!on || !redactResults || !redactOpts) return raw;
67
+ if (typeof raw === 'string') return redactText(raw, vault, redactOpts);
68
+ if (raw && typeof raw === 'object' && typeof raw.text === 'string') {
69
+ return { ...raw, text: redactText(raw.text, vault, redactOpts) };
70
+ }
71
+ return raw;
72
+ },
73
+
74
+ // ④ The final reply the user sees.
75
+ toUser(text) {
76
+ return on ? restoreText(text, vault) : text;
77
+ },
78
+ };
79
+ }