@chatpanel/pii 0.7.0 → 0.7.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/pii",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
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",
package/pii-detect.js CHANGED
@@ -244,6 +244,24 @@ const hostOf = (u) => { try { return new URL(String(u)).host; } catch { return '
244
244
  export async function detectEntities(text, cfg, { signal, fetchImpl = globalThis.fetch, strict = false, structured = NO_STRUCTURE, onEgress = null } = {}) {
245
245
  const det = cfg?.detection;
246
246
  if (!det || !det.backend || det.backend === 'off' || !det.url || typeof fetchImpl !== 'function') return [];
247
+ // AN IN-PROCESS DETECTOR SENDS NOTHING ANYWHERE, so the network guard below must not
248
+ // judge it by a URL it never dials.
249
+ //
250
+ // This is not a hypothetical. A host that runs the model in its own process passes a
251
+ // sentinel URL and a fetchImpl that ignores it entirely — and the sentinel failed the
252
+ // http(s) scheme check, threw, and was swallowed by the fail-open path. The result was a
253
+ // detector that reported itself ready, answered its own health route correctly, and
254
+ // contributed NOTHING to a single redaction: names, organisations and places went to the
255
+ // model in full while the UI said full tier.
256
+ //
257
+ // The opt-out is deliberately narrow. It requires the caller to have supplied its OWN
258
+ // fetch, so a `transport: 'in-process'` line in a config file cannot turn the SSRF guard
259
+ // off for a real network address — without an injected transport there is no in-process
260
+ // anything, and the flag is refused rather than honoured.
261
+ const inProcess = det.transport === 'in-process';
262
+ if (inProcess && fetchImpl === globalThis.fetch) {
263
+ throw new Error("detection.transport 'in-process' needs an injected fetch; refusing to treat a network call as in-process");
264
+ }
247
265
  const capped = String(text || '').slice(0, det.maxChars || 8000);
248
266
  if (capped.trim().length < 8) return [];
249
267
  const key = cacheKey(capped, det);
@@ -255,12 +273,12 @@ export async function detectEntities(text, cfg, { signal, fetchImpl = globalThis
255
273
  // only, never cloud metadata. Loopback/LAN allowed — a local NER server / Ollama
256
274
  // is the normal case. A blocked URL fails open (deterministic-only), or surfaces
257
275
  // to the Test button in strict mode.
258
- assertEndpointUrl(det.url);
276
+ if (!inProcess) assertEndpointUrl(det.url);
259
277
  const t0 = Date.now();
260
278
  try {
261
279
  ents = await withTimeout(run(capped, det, signal, fetchImpl, structured), det.timeoutMs || 1500, signal);
262
- report(onEgress, det, capped, t0, ents.length, null);
263
- } catch (e) { report(onEgress, det, capped, t0, 0, e); throw e; }
280
+ if (!inProcess) report(onEgress, det, capped, t0, ents.length, null);
281
+ } catch (e) { if (!inProcess) report(onEgress, det, capped, t0, 0, e); throw e; }
264
282
  } catch (e) {
265
283
  if (strict) throw e; // surface errors to the Test button
266
284
  ents = []; // otherwise fail open — deterministic redaction still applies
package/tool-harness.js CHANGED
@@ -46,7 +46,15 @@ export function restoreToolArgs(value, vault) {
46
46
  // [[LOCATION_1]] token as redacted and REFUSE to use it for a lookup ("I can't see
47
47
  // your real city") — the opposite of what we want. Weak models call the tool blindly
48
48
  // and it works (the harness restores the real value), so the note levels them up.
49
- export function placeholderToolNote({ toolData = 'real' } = {}) {
49
+ //
50
+ // `ownTools`: the model is a relayed CLI agent that brings tools of ITS OWN (Codex's web
51
+ // search, a shell, files). Those run on the far side of the harness and receive the
52
+ // placeholder LITERALLY — a search for "[[ORG_1]] stock price" finds nothing, and the agent
53
+ // reports that "the lookup did not resolve the company". Seen exactly so, on the desktop:
54
+ // Codex chose its own web search over ChatPanel's `find` on one turn and answered from
55
+ // nothing. The extra sentence says which tools restore and which do not, so the choice is
56
+ // no longer a coin toss.
57
+ export function placeholderToolNote({ toolData = 'real', ownTools = false } = {}) {
50
58
  const intro =
51
59
  'PRIVACY PLACEHOLDERS: some values in this conversation are tokens like [[PERSON_1]], '
52
60
  + '[[LOCATION_1]], [[ORG_1]] that stand in for the user\'s real private data. ';
@@ -74,7 +82,14 @@ export function placeholderToolNote({ toolData = 'real' } = {}) {
74
82
  + 'Do NOT ask the user to re-type the value and do NOT refuse on privacy grounds — the lookup '
75
83
  + 'will work. The real values are restored in your final answer automatically, so write your '
76
84
  + 'answer using the placeholders too.';
77
- return intro + remote + rules;
85
+ const own = ownTools
86
+ ? ' ONLY THE TOOLS LISTED IN THIS CONVERSATION restore placeholders. Any tool you bring '
87
+ + 'yourself — your own web search, shell, file or code tools — receives the placeholder '
88
+ + 'text literally and will find nothing. So for ANY lookup that involves a placeholder, '
89
+ + 'call the listed tool (for example `find` with action `web_search` or `history_search`) '
90
+ + 'rather than your own; use your own tools only for things that involve no placeholder.'
91
+ : '';
92
+ return intro + remote + rules + own;
78
93
  }
79
94
 
80
95
  // Tools whose results come from the PUBLIC web rather than from the user's own machine or