@chatpanel/pii 0.2.14 → 0.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.
- package/package.json +2 -2
- package/pii-redact.js +44 -0
- package/tool-harness.js +23 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/pii",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The canonical ChatPanel privacy engine
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The canonical ChatPanel privacy engine \u2014 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",
|
|
7
7
|
"exports": {
|
package/pii-redact.js
CHANGED
|
@@ -345,3 +345,47 @@ export function hasToken(text) {
|
|
|
345
345
|
TOKEN_RE.lastIndex = 0;
|
|
346
346
|
return TOKEN_RE.test(String(text || ''));
|
|
347
347
|
}
|
|
348
|
+
|
|
349
|
+
// ── What was redacted, for the user's own eyes ──────────────────────────────────────────
|
|
350
|
+
//
|
|
351
|
+
// The privacy promise is invisible unless you can SEE it: which entity types were caught,
|
|
352
|
+
// how many of each, and (on request) the actual before → after pairs. All of that already
|
|
353
|
+
// exists in the vault — this just summarises it, so every client renders the same shape
|
|
354
|
+
// instead of each one re-deriving it.
|
|
355
|
+
//
|
|
356
|
+
// PRIVACY OF THE SUMMARY ITSELF: `types` carries counts only, never values, so it is safe
|
|
357
|
+
// to render, log or persist. Real values live behind `pairs`, which a caller must ask for
|
|
358
|
+
// explicitly (`includeValues: true`) — they are the user's own data, shown on their own
|
|
359
|
+
// device, and must never be written anywhere durable.
|
|
360
|
+
|
|
361
|
+
/** Entity type + count for everything this vault redacted, most-frequent first. */
|
|
362
|
+
export function redactionSummary(vault, { includeValues = false, maxPairs = 200 } = {}) {
|
|
363
|
+
const byType = new Map();
|
|
364
|
+
const pairs = [];
|
|
365
|
+
for (const [token, value] of vault?.byToken || new Map()) {
|
|
366
|
+
const m = /^\[\[([A-Z][A-Z0-9]*)_(\d+)\]\]$/.exec(token);
|
|
367
|
+
const type = m ? m[1] : 'OTHER';
|
|
368
|
+
byType.set(type, (byType.get(type) || 0) + 1);
|
|
369
|
+
if (includeValues && pairs.length < maxPairs) pairs.push({ token, value, type });
|
|
370
|
+
}
|
|
371
|
+
const types = [...byType.entries()]
|
|
372
|
+
.map(([type, count]) => ({ type, count }))
|
|
373
|
+
.sort((a, b) => b.count - a.count || a.type.localeCompare(b.type));
|
|
374
|
+
return {
|
|
375
|
+
total: types.reduce((n, t) => n + t.count, 0),
|
|
376
|
+
types,
|
|
377
|
+
...(includeValues ? { pairs } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Merge several vault summaries (e.g. every conversation) into one. Counts only. */
|
|
382
|
+
export function mergeRedactionSummaries(summaries) {
|
|
383
|
+
const byType = new Map();
|
|
384
|
+
for (const s of summaries || []) {
|
|
385
|
+
for (const t of s?.types || []) byType.set(t.type, (byType.get(t.type) || 0) + t.count);
|
|
386
|
+
}
|
|
387
|
+
const types = [...byType.entries()]
|
|
388
|
+
.map(([type, count]) => ({ type, count }))
|
|
389
|
+
.sort((a, b) => b.count - a.count || a.type.localeCompare(b.type));
|
|
390
|
+
return { total: types.reduce((n, t) => n + t.count, 0), types };
|
|
391
|
+
}
|
package/tool-harness.js
CHANGED
|
@@ -77,6 +77,16 @@ export function placeholderToolNote({ toolData = 'real' } = {}) {
|
|
|
77
77
|
return intro + remote + rules;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// Tools whose results come from the PUBLIC web rather than from the user's own machine or
|
|
81
|
+
// accounts. Deliberately a short, explicit allowlist rather than a heuristic: being wrong in
|
|
82
|
+
// the "public" direction would send real PII to a model, so a tool earns its place here only
|
|
83
|
+
// when its output is public by construction. Page tools are NOT here — the user's open tab
|
|
84
|
+
// may be an internal app.
|
|
85
|
+
const PUBLIC_SOURCE_TOOLS = new Set(['web_search', 'web_fetch', 'fetch_url']);
|
|
86
|
+
export function isPublicSourceTool(name) {
|
|
87
|
+
return PUBLIC_SOURCE_TOOLS.has(String(name || '').toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
export function makeToolHarness({ vault = null, toolData = 'real', redactOpts = null, redactResults = true, remoteTools = null } = {}) {
|
|
81
91
|
const on = !!vault; // privacy enabled for this turn?
|
|
82
92
|
const redactRemote = toolData === 'redactRemote';
|
|
@@ -109,6 +119,19 @@ export function makeToolHarness({ vault = null, toolData = 'real', redactOpts =
|
|
|
109
119
|
// via a nested field the old string/{text}-only path skipped.
|
|
110
120
|
toModelResult(name, raw) {
|
|
111
121
|
if (!on || !redactResults || !redactOpts) return raw;
|
|
122
|
+
// PUBLIC RESULTS ARE NOT THE USER'S DATA.
|
|
123
|
+
//
|
|
124
|
+
// Redaction exists to stop the user's information LEAVING the device. Text coming back
|
|
125
|
+
// from a public web search never left it — the model's provider could fetch the same
|
|
126
|
+
// page itself — so rewriting it buys no privacy and actively corrupts facts: a
|
|
127
|
+
// dictionary pseudonym (Suresh → John) renamed a public actor inside search results,
|
|
128
|
+
// and the answer came back about "Mysore Seshaiah John Babu Naidu". The detectors
|
|
129
|
+
// (emails, phones, keys) also fire on unrelated strangers' details in fetched pages.
|
|
130
|
+
//
|
|
131
|
+
// So public-source results pass through intact. Everything local or private — history,
|
|
132
|
+
// meetings, notes, the user's own page, any MCP server — is redacted exactly as before,
|
|
133
|
+
// which is where a leak could actually happen.
|
|
134
|
+
if (isPublicSourceTool(name)) return raw;
|
|
112
135
|
return redactResultShape(raw, vault, redactOpts);
|
|
113
136
|
},
|
|
114
137
|
|