shroud-privacy 2.2.11 → 2.2.13

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.
Files changed (40) hide show
  1. package/README.md +19 -10
  2. package/dist/hooks.js +246 -14
  3. package/openclaw.plugin.json +1 -1
  4. package/package.json +3 -2
  5. package/dist/agent-session.d.ts +0 -259
  6. package/dist/agent-session.js +0 -693
  7. package/dist/compliance.d.ts +0 -44
  8. package/dist/compliance.js +0 -76
  9. package/dist/dashboard.d.ts +0 -42
  10. package/dist/dashboard.js +0 -1558
  11. package/dist/detectors/injection-multilingual.d.ts +0 -27
  12. package/dist/detectors/injection-multilingual.js +0 -399
  13. package/dist/detectors/injection-signatures.d.ts +0 -26
  14. package/dist/detectors/injection-signatures.js +0 -508
  15. package/dist/detectors/injection.d.ts +0 -56
  16. package/dist/detectors/injection.js +0 -269
  17. package/dist/detectors/tool-guard.d.ts +0 -27
  18. package/dist/detectors/tool-guard.js +0 -418
  19. package/dist/event-grader.d.ts +0 -97
  20. package/dist/event-grader.js +0 -214
  21. package/dist/exposure.d.ts +0 -29
  22. package/dist/exposure.js +0 -72
  23. package/dist/policy.d.ts +0 -99
  24. package/dist/policy.js +0 -212
  25. package/dist/profiler-analysis.d.ts +0 -35
  26. package/dist/profiler-analysis.js +0 -230
  27. package/dist/profiler-store.d.ts +0 -33
  28. package/dist/profiler-store.js +0 -118
  29. package/dist/profiler-types.d.ts +0 -128
  30. package/dist/profiler-types.js +0 -16
  31. package/dist/profiler.d.ts +0 -81
  32. package/dist/profiler.js +0 -392
  33. package/dist/security-event.d.ts +0 -70
  34. package/dist/security-event.js +0 -80
  35. package/dist/siem.d.ts +0 -49
  36. package/dist/siem.js +0 -113
  37. package/dist/signature-loader.d.ts +0 -113
  38. package/dist/signature-loader.js +0 -255
  39. package/dist/store-file.d.ts +0 -26
  40. package/dist/store-file.js +0 -79
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  <a href="CHANGELOG.md">Changelog</a>
18
18
  </p>
19
19
 
20
- > Apache 2.0 &middot; Zero runtime dependencies &middot; Works with [OpenClaw](https://openclaw.ai) or any agent via [APP](#agent-privacy-protocol-app)
20
+ > Apache 2.0 &middot; Zero runtime dependencies &middot; Anthropic + OpenAI + Google supported &middot; Prompt-caching friendly &middot; Works with [OpenClaw](https://openclaw.ai) or any agent via [APP](#agent-privacy-protocol-app)
21
21
 
22
22
  ---
23
23
 
@@ -61,6 +61,7 @@ Shroud does not guarantee compliance — regex-based detection has limitations (
61
61
  3. **Passes through public URLs** — external URLs (arxiv.org, docs.stripe.com, etc.) are not obfuscated. Shroud resolves FQDNs via DNS: public IPs pass through, RFC 1918 / NXDOMAIN / internal IPs are obfuscated. Well-known platforms (GitHub, YouTube, Wikipedia, etc.) are always passed through.
62
62
  4. **Deobfuscates** LLM responses and tool parameters so the user sees real values and tools receive real arguments.
63
63
  5. **Audit logs** every event with counts, categories, char deltas, and optional proof hashes — never logging raw sensitive values.
64
+ 6. **Preserves prompt caching.** Obfuscation is deterministic — same input + same key = same output every turn. The system prompt prefix stays identical across turns, so provider-side prompt caching (Anthropic, OpenAI, Bedrock) works normally. No cache-busting, no extra token costs.
64
65
 
65
66
  ### Hook lifecycle
66
67
 
@@ -355,10 +356,12 @@ APP is an open protocol for adding privacy and infrastructure protection to any
355
356
  | (any language) | | (app-server.mjs)|
356
357
  +-------------------+ +------------------+
357
358
  | |
358
- | 1. obfuscate(user_input) | detects entities,
359
- | 2. send to LLM | returns fakes
360
- | 3. deobfuscate(llm_response) | restores reals
361
- | 4. show to user |
359
+ | 1. identify(agent, version) | registers agent
360
+ | 2. obfuscate(user_input) | detects entities,
361
+ | 3. send to LLM | returns fakes
362
+ | 4. tool_call(tool, args) | scans tool call
363
+ | 5. deobfuscate(llm_response) | restores reals
364
+ | 6. show to user |
362
365
  ```
363
366
 
364
367
  ### Protocol specification
@@ -371,15 +374,21 @@ APP is an open protocol for adding privacy and infrastructure protection to any
371
374
 
372
375
  | Method | Params | Returns | Description |
373
376
  |--------|--------|---------|-------------|
374
- | `obfuscate` | `{text}` | `{text, entityCount, categories, modified, audit}` | Replace real values with fakes |
375
- | `deobfuscate` | `{text}` | `{text, replacementCount, modified, audit}` | Restore fakes to real values |
377
+ | `identify` | `{agent, version, channel?}` | `{ok, agent, buildId}` | Identify the agent (required before obfuscate/deobfuscate) |
378
+ | `obfuscate` | `{text, partition?}` | `{text, entityCount, categories, modified, audit}` | Replace real values with fakes |
379
+ | `deobfuscate` | `{text, partition?}` | `{text, replacementCount, modified, audit}` | Restore fakes to real values |
380
+ | `tool_call` | `{tool, args?}` | `{allowed, blocked, tool, events?}` | Report a tool call for security scanning |
381
+ | `tool_result` | `{tool, result}` | `{text, replacementCount}` | Obfuscate tool result before storing |
376
382
  | `reset` | `{}` | `{ok, summary}` | Clear all mappings |
377
383
  | `stats` | `{}` | `{storeMappings, ruleHits, ...}` | Engine statistics |
378
384
  | `health` | `{}` | `{uptime, requests, avgLatencyMs}` | Liveness check |
379
385
  | `configure` | `{config}` | `{ok}` | Hot-reload configuration |
380
386
  | `batch` | `{operations: [{direction, text}]}` | `{results: [...]}` | Batch obfuscate/deobfuscate |
387
+ | `setPartition` | `{partition}` | `{ok}` | Switch mapping namespace (multi-tenant) |
381
388
  | `shutdown` | `{}` | `{ok}` | Graceful shutdown (flushes stats) |
382
389
 
390
+ Agents should call `identify` first to register themselves. `tool_call` and `tool_result` are optional — they enable per-tool privacy scanning and mapping isolation for tool arguments.
391
+
383
392
  ### Python client
384
393
 
385
394
  ```python
@@ -408,16 +417,16 @@ client.stop()
408
417
  npm install
409
418
  npm run build # compile TypeScript
410
419
  npm run lint # type-check without emitting
411
- npm test # unit + harness (1,229 tests, no Docker)
420
+ npm test # unit + harness (1,238 tests, no Docker)
412
421
  npm run test:docker # Docker E2E — real OpenClaw, all channels (192 tests)
413
- npm run test:all # everything (1,421 tests)
422
+ npm run test:all # everything (1,430 tests)
414
423
  ```
415
424
 
416
425
  ### Test layers
417
426
 
418
427
  | Layer | Command | Tests | What it covers |
419
428
  |-------|---------|-------|---------------|
420
- | Unit | `npm run test:unit` | 870 | Obfuscator, detectors, generators, store, config |
429
+ | Unit | `npm run test:unit` | 879 | Obfuscator, detectors, generators, store, config |
421
430
  | APP Harness | `npm run test:integration` | 359 | 48 scenario files via mock LLM, no OpenClaw |
422
431
  | Docker E2E | `npm run test:docker` | 192 | Real OpenClaw gateway, Slack/WhatsApp/Cron/TUI channels, 153 regression scenarios |
423
432
  | Sandbox E2E | `run-compat.sh <ver> --sandbox` | +8 | Docker-in-Docker, sandboxed agent exec, tool call deobfuscation |
package/dist/hooks.js CHANGED
@@ -299,25 +299,56 @@ export function registerHooks(api, obfuscator) {
299
299
  totalEntities += result.entities.length;
300
300
  }
301
301
  }
302
- // Pre-create mappings for PII in user messages WITHOUT mutating them.
303
- // This seeds the mapping store so the fetch intercept's response
304
- // deobfuscation can replace fakes with the correct real values.
302
+ // Obfuscate ALL messages in-place seeds the mapping store AND mutates
303
+ // the message array so PII is replaced before OpenClaw builds the request.
304
+ // This is critical when the LLM SDK (e.g. OpenAI v6) captures fetch at
305
+ // construction time, bypassing Shroud's globalThis.fetch intercept.
306
+ // The fetch intercept is still the primary path for SDKs that use
307
+ // globalThis.fetch (Anthropic) — double-obfuscation is safe because
308
+ // already-obfuscated text has no detectable PII entities.
305
309
  if (Array.isArray(event?.messages)) {
306
310
  for (const msg of event.messages) {
307
- const texts = [];
308
- if (typeof msg.content === "string")
309
- texts.push(msg.content);
311
+ // String content (Anthropic/OpenAI)
312
+ if (typeof msg.content === "string") {
313
+ const cleaned = stripSlackLinksForHook(msg.content);
314
+ const result = ob().obfuscate(cleaned);
315
+ totalEntities += result.entities.length;
316
+ if (result.entities.length > 0 || cleaned !== msg.content) {
317
+ msg.content = result.entities.length > 0 ? result.obfuscated : cleaned;
318
+ }
319
+ }
320
+ // Array content blocks
310
321
  else if (Array.isArray(msg.content)) {
311
322
  for (const b of msg.content) {
312
- if (b?.type === "text" && typeof b.text === "string")
313
- texts.push(b.text);
323
+ if (b?.type === "text" && typeof b.text === "string") {
324
+ const cleaned = stripSlackLinksForHook(b.text);
325
+ const result = ob().obfuscate(cleaned);
326
+ totalEntities += result.entities.length;
327
+ if (result.entities.length > 0 || cleaned !== b.text) {
328
+ b.text = result.entities.length > 0 ? result.obfuscated : cleaned;
329
+ }
330
+ }
331
+ // tool_result blocks with string content
332
+ if (typeof b?.content === "string") {
333
+ const cleaned = stripSlackLinksForHook(b.content);
334
+ const result = ob().obfuscate(cleaned);
335
+ totalEntities += result.entities.length;
336
+ if (result.entities.length > 0 || cleaned !== b.content) {
337
+ b.content = result.entities.length > 0 ? result.obfuscated : cleaned;
338
+ }
339
+ }
314
340
  }
315
341
  }
316
- for (const text of texts) {
317
- const cleaned = stripSlackLinksForHook(text);
318
- const result = ob().obfuscate(cleaned);
319
- totalEntities += result.entities.length;
320
- // Do NOT mutate — just creating mappings in the store
342
+ // OpenAI tool_calls in assistant messages
343
+ if (Array.isArray(msg.tool_calls)) {
344
+ for (const tc of msg.tool_calls) {
345
+ if (typeof tc.function?.arguments === "string") {
346
+ const result = ob().obfuscate(tc.function.arguments);
347
+ totalEntities += result.entities.length;
348
+ if (result.entities.length > 0)
349
+ tc.function.arguments = result.obfuscated;
350
+ }
351
+ }
321
352
  }
322
353
  }
323
354
  }
@@ -740,6 +771,8 @@ export function registerHooks(api, obfuscator) {
740
771
  const originalFetch = globalThis.fetch;
741
772
  if (originalFetch && !(globalThis.__shroudFetchPatched)) {
742
773
  globalThis.__shroudFetchPatched = true;
774
+ // Store original for comparison — SDK clients may have captured it
775
+ const _prePatchFetch = globalThis.fetch;
743
776
  globalThis.fetch = async function shroudFetchInterceptor(input, init) {
744
777
  // Only intercept POST requests with a body
745
778
  if (init?.method?.toUpperCase() !== "POST" || !init?.body) {
@@ -922,6 +955,25 @@ export function registerHooks(api, obfuscator) {
922
955
  modified = true;
923
956
  }
924
957
  }
958
+ // OpenAI: tool_calls in assistant messages (multi-turn re-obfuscation)
959
+ if (Array.isArray(msg.tool_calls)) {
960
+ for (const tc of msg.tool_calls) {
961
+ if (typeof tc.function?.arguments === "string") {
962
+ const r = obfuscateText(tc.function.arguments);
963
+ if (r.modified) {
964
+ tc.function.arguments = r.text;
965
+ modified = true;
966
+ }
967
+ }
968
+ if (typeof tc.function?.name === "string") {
969
+ const r = obfuscateText(tc.function.name);
970
+ if (r.modified) {
971
+ tc.function.name = r.text;
972
+ modified = true;
973
+ }
974
+ }
975
+ }
976
+ }
925
977
  }
926
978
  if (modified) {
927
979
  const newBody = JSON.stringify(body);
@@ -968,6 +1020,9 @@ export function registerHooks(api, obfuscator) {
968
1020
  // OpenAI per-choice state
969
1021
  const choiceAccum = new Map();
970
1022
  const choiceBuffer = new Map();
1023
+ // OpenAI tool_calls per-choice state: Map<choiceIdx, Map<toolCallIdx, argString>>
1024
+ const toolCallAccum = new Map();
1025
+ const toolCallBuffer = new Map();
971
1026
  let sseRemainder = "";
972
1027
  const transform = new TransformStream({
973
1028
  transform(chunk, controller) {
@@ -1038,7 +1093,7 @@ export function registerHooks(api, obfuscator) {
1038
1093
  controller.enqueue(new TextEncoder().encode(part + "\n\n"));
1039
1094
  continue;
1040
1095
  }
1041
- // OpenAI delta.content: buffer until finish_reason
1096
+ // OpenAI delta.content / delta.tool_calls: buffer until finish_reason
1042
1097
  if (Array.isArray(json.choices)) {
1043
1098
  let buffered = false;
1044
1099
  for (const choice of json.choices) {
@@ -1050,8 +1105,25 @@ export function registerHooks(api, obfuscator) {
1050
1105
  choiceBuffer.get(idx).push(part);
1051
1106
  buffered = true;
1052
1107
  }
1108
+ // Buffer tool_calls argument fragments
1109
+ if (Array.isArray(choice.delta?.tool_calls)) {
1110
+ for (const tc of choice.delta.tool_calls) {
1111
+ const tcIdx = tc.index ?? 0;
1112
+ if (!toolCallAccum.has(idx))
1113
+ toolCallAccum.set(idx, new Map());
1114
+ const tcMap = toolCallAccum.get(idx);
1115
+ if (typeof tc.function?.arguments === "string") {
1116
+ tcMap.set(tcIdx, (tcMap.get(tcIdx) || "") + tc.function.arguments);
1117
+ }
1118
+ }
1119
+ if (!toolCallBuffer.has(idx))
1120
+ toolCallBuffer.set(idx, []);
1121
+ toolCallBuffer.get(idx).push(part);
1122
+ buffered = true;
1123
+ }
1053
1124
  // finish_reason signals block complete — flush
1054
1125
  if (choice.finish_reason) {
1126
+ // Flush text content
1055
1127
  const accumulated = choiceAccum.get(idx);
1056
1128
  const buf = choiceBuffer.get(idx);
1057
1129
  if (accumulated && buf && buf.length > 0) {
@@ -1082,6 +1154,57 @@ export function registerHooks(api, obfuscator) {
1082
1154
  choiceAccum.delete(idx);
1083
1155
  choiceBuffer.delete(idx);
1084
1156
  }
1157
+ // Flush tool_calls — deobfuscate accumulated arguments
1158
+ const tcMap = toolCallAccum.get(idx);
1159
+ const tcBuf = toolCallBuffer.get(idx);
1160
+ if (tcMap && tcMap.size > 0 && tcBuf && tcBuf.length > 0) {
1161
+ // Deobfuscate each tool call's accumulated arguments
1162
+ const deobArgs = new Map();
1163
+ for (const [tcIdx, args] of tcMap) {
1164
+ const { text: deobArg, replacementCount: tcRc } = ob().deobfuscateWithStats(args);
1165
+ deobArgs.set(tcIdx, deobArg);
1166
+ // tcRc tracked via deobfuscateWithStats
1167
+ }
1168
+ // Track per-tool-call whether we've emitted the deobbed args
1169
+ const tcEmitted = new Set();
1170
+ for (const eventStr of tcBuf) {
1171
+ const dLine = eventStr.split("\n").find((l) => l.startsWith("data: "));
1172
+ if (dLine) {
1173
+ try {
1174
+ const dJson = JSON.parse(dLine.slice(6));
1175
+ if (Array.isArray(dJson.choices)) {
1176
+ for (const c of dJson.choices) {
1177
+ if (Array.isArray(c.delta?.tool_calls)) {
1178
+ for (const tc of c.delta.tool_calls) {
1179
+ const tcIdx = tc.index ?? 0;
1180
+ if (typeof tc.function?.arguments === "string") {
1181
+ const deob = deobArgs.get(tcIdx);
1182
+ if (deob !== undefined) {
1183
+ if (!tcEmitted.has(tcIdx)) {
1184
+ tc.function.arguments = deob;
1185
+ tcEmitted.add(tcIdx);
1186
+ }
1187
+ else {
1188
+ tc.function.arguments = "";
1189
+ }
1190
+ }
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1195
+ const nonDataLines = eventStr.split("\n").filter((l) => !l.startsWith("data: ")).join("\n");
1196
+ const rebuilt = (nonDataLines ? nonDataLines + "\n" : "") + "data: " + JSON.stringify(dJson);
1197
+ controller.enqueue(new TextEncoder().encode(rebuilt + "\n\n"));
1198
+ continue;
1199
+ }
1200
+ }
1201
+ catch { }
1202
+ }
1203
+ controller.enqueue(new TextEncoder().encode(eventStr + "\n\n"));
1204
+ }
1205
+ toolCallAccum.delete(idx);
1206
+ toolCallBuffer.delete(idx);
1207
+ }
1085
1208
  buffered = false;
1086
1209
  }
1087
1210
  }
@@ -1156,6 +1279,54 @@ export function registerHooks(api, obfuscator) {
1156
1279
  controller.enqueue(new TextEncoder().encode(eventStr + "\n\n"));
1157
1280
  }
1158
1281
  }
1282
+ // Flush any remaining tool_calls buffers
1283
+ for (const [idx, tcBuf] of toolCallBuffer) {
1284
+ const tcMap = toolCallAccum.get(idx);
1285
+ if (tcMap && tcMap.size > 0 && tcBuf.length > 0) {
1286
+ const deobArgs = new Map();
1287
+ for (const [tcIdx, args] of tcMap) {
1288
+ const { text: deobArg, replacementCount: tcFlushRc } = ob().deobfuscateWithStats(args);
1289
+ deobArgs.set(tcIdx, deobArg);
1290
+ // tcFlushRc tracked via deobfuscateWithStats
1291
+ }
1292
+ const tcEmitted = new Set();
1293
+ for (const eventStr of tcBuf) {
1294
+ const dLine = eventStr.split("\n").find((l) => l.startsWith("data: "));
1295
+ if (dLine) {
1296
+ try {
1297
+ const dJson = JSON.parse(dLine.slice(6));
1298
+ if (Array.isArray(dJson.choices)) {
1299
+ for (const c of dJson.choices) {
1300
+ if (Array.isArray(c.delta?.tool_calls)) {
1301
+ for (const tc of c.delta.tool_calls) {
1302
+ const tcIdx = tc.index ?? 0;
1303
+ if (typeof tc.function?.arguments === "string") {
1304
+ const deob = deobArgs.get(tcIdx);
1305
+ if (deob !== undefined) {
1306
+ if (!tcEmitted.has(tcIdx)) {
1307
+ tc.function.arguments = deob;
1308
+ tcEmitted.add(tcIdx);
1309
+ }
1310
+ else {
1311
+ tc.function.arguments = "";
1312
+ }
1313
+ }
1314
+ }
1315
+ }
1316
+ }
1317
+ }
1318
+ const nonDataLines = eventStr.split("\n").filter((l) => !l.startsWith("data: ")).join("\n");
1319
+ const rebuilt = (nonDataLines ? nonDataLines + "\n" : "") + "data: " + JSON.stringify(dJson);
1320
+ controller.enqueue(new TextEncoder().encode(rebuilt + "\n\n"));
1321
+ continue;
1322
+ }
1323
+ }
1324
+ catch { }
1325
+ }
1326
+ controller.enqueue(new TextEncoder().encode(eventStr + "\n\n"));
1327
+ }
1328
+ }
1329
+ }
1159
1330
  if (sseRemainder.trim()) {
1160
1331
  controller.enqueue(new TextEncoder().encode(sseRemainder));
1161
1332
  }
@@ -1185,6 +1356,16 @@ export function registerHooks(api, obfuscator) {
1185
1356
  if (typeof choice.message?.content === "string") {
1186
1357
  choice.message.content = ob().deobfuscate(choice.message.content);
1187
1358
  }
1359
+ // OpenAI: deobfuscate tool_calls arguments
1360
+ if (Array.isArray(choice.message?.tool_calls)) {
1361
+ for (const tc of choice.message.tool_calls) {
1362
+ if (typeof tc.function?.arguments === "string") {
1363
+ const { text: _jdt3, replacementCount: _jdrc3 } = ob().deobfuscateWithStats(tc.function.arguments);
1364
+ tc.function.arguments = _jdt3;
1365
+ // _jdrc3 tracked via deobfuscateWithStats
1366
+ }
1367
+ }
1368
+ }
1188
1369
  }
1189
1370
  }
1190
1371
  return new Response(JSON.stringify(json), {
@@ -1203,5 +1384,56 @@ export function registerHooks(api, obfuscator) {
1203
1384
  }
1204
1385
  return response;
1205
1386
  }
1387
+ // ── Rebind SDK clients that captured the pre-patch fetch reference ──
1388
+ // The OpenAI SDK (and others) capture `globalThis.fetch` at construction
1389
+ // time via `this.fetch = options.fetch ?? getDefaultFetch()`. If the SDK
1390
+ // was constructed before Shroud patched fetch, it holds the original
1391
+ // unpatched reference and all LLM requests bypass the intercept.
1392
+ // Walk all reachable objects looking for SDK client instances that still
1393
+ // hold the old reference and rebind them to the patched interceptor.
1394
+ try {
1395
+ const patchedFetch = globalThis.fetch;
1396
+ const visited = new WeakSet();
1397
+ function rebindSdkClients(obj, depth) {
1398
+ if (!obj || typeof obj !== "object" || depth > 4 || visited.has(obj))
1399
+ return;
1400
+ visited.add(obj);
1401
+ // OpenAI SDK: client.fetch === old unpatched fetch
1402
+ if (obj.fetch === _prePatchFetch && obj.fetch !== patchedFetch) {
1403
+ obj.fetch = patchedFetch;
1404
+ api.logger?.info("[shroud] rebound SDK client fetch to patched interceptor");
1405
+ }
1406
+ // Check known extension paths
1407
+ try {
1408
+ for (const key of Object.keys(obj)) {
1409
+ if (key.startsWith("_") || key === "constructor")
1410
+ continue;
1411
+ try {
1412
+ rebindSdkClients(obj[key], depth + 1);
1413
+ }
1414
+ catch { }
1415
+ }
1416
+ }
1417
+ catch { }
1418
+ }
1419
+ // Scan globalThis for SDK client instances
1420
+ rebindSdkClients(globalThis.__ocClients, 0);
1421
+ rebindSdkClients(globalThis.__openaiClient, 0);
1422
+ // Scan all loaded modules for exported clients
1423
+ if (typeof require !== "undefined" && require.cache) {
1424
+ for (const modId of Object.keys(require.cache)) {
1425
+ if (modId.includes("openai") || modId.includes("lossless")) {
1426
+ try {
1427
+ const mod = require.cache[modId]?.exports;
1428
+ rebindSdkClients(mod, 0);
1429
+ }
1430
+ catch { }
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ catch {
1436
+ // Non-fatal — fetch intercept still works for globalThis.fetch callers
1437
+ }
1206
1438
  }
1207
1439
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "shroud-privacy",
3
3
  "name": "Shroud",
4
- "version": "2.2.11",
4
+ "version": "2.2.12",
5
5
  "description": "Privacy obfuscation with deterministic fake values and deobfuscation — PII never reaches the LLM, tool calls still work",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shroud-privacy",
3
- "version": "2.2.11",
3
+ "version": "2.2.13",
4
4
  "description": "Privacy and infrastructure protection for AI agents — detects sensitive data (PII, network topology, credentials, OT/SCADA) and replaces with deterministic fakes before anything reaches the LLM.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,8 @@
22
22
  "test": "npm run test:unit && npm run test:integration",
23
23
  "test:unit": "vitest run",
24
24
  "test:integration": "node tests/harness/run.mjs",
25
- "test:docker": "bash compat/run-compat.sh latest",
25
+ "test:docker": "bash compat/run-e2e.sh latest",
26
+ "test:docker:legacy": "bash compat/run-compat.sh latest",
26
27
  "test:all": "npm run test:unit && npm run test:integration && npm run test:docker",
27
28
  "test:watch": "vitest",
28
29
  "lint": "tsc --noEmit",