shroud-privacy 2.2.12 → 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.
- package/README.md +23 -15
- package/dist/hooks.js +246 -14
- package/package.json +3 -2
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 · Zero runtime dependencies · Works with [OpenClaw](https://openclaw.ai) or any agent via [APP](#agent-privacy-protocol-app)
|
|
20
|
+
> Apache 2.0 · Zero runtime dependencies · Anthropic + OpenAI + Google supported · Prompt-caching friendly · 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
|
|
|
@@ -88,10 +89,9 @@ Shroud does not guarantee compliance — regex-based detection has limitations (
|
|
|
88
89
|
```bash
|
|
89
90
|
openclaw --version # ensure 2026.3.22+
|
|
90
91
|
openclaw plugins install shroud-privacy
|
|
91
|
-
openclaw gateway restart
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Configure in `~/.openclaw/openclaw.json` under `plugins.entries."shroud-privacy".config`. No OpenClaw file modifications needed — Shroud uses runtime interception only.
|
|
95
95
|
|
|
96
96
|
### Any agent (via APP)
|
|
97
97
|
|
|
@@ -167,13 +167,13 @@ openclaw gateway restart
|
|
|
167
167
|
|
|
168
168
|
## Configure
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
Edit `~/.openclaw/openclaw.json` under `plugins.entries."shroud-privacy".config`:
|
|
171
171
|
|
|
172
172
|
```jsonc
|
|
173
173
|
"shroud-privacy": {
|
|
174
|
-
"enabled": true,
|
|
174
|
+
"enabled": true,
|
|
175
175
|
"config": {
|
|
176
|
-
"auditEnabled": true
|
|
176
|
+
"auditEnabled": true // audit log on — see what Shroud is doing
|
|
177
177
|
// "minConfidence": 0.0 // catch everything (default)
|
|
178
178
|
// "secretKey": "" // auto-generated if empty
|
|
179
179
|
// "persistentSalt": "" // set for cross-session consistency
|
|
@@ -356,10 +356,12 @@ APP is an open protocol for adding privacy and infrastructure protection to any
|
|
|
356
356
|
| (any language) | | (app-server.mjs)|
|
|
357
357
|
+-------------------+ +------------------+
|
|
358
358
|
| |
|
|
359
|
-
| 1.
|
|
360
|
-
| 2.
|
|
361
|
-
| 3.
|
|
362
|
-
| 4.
|
|
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 |
|
|
363
365
|
```
|
|
364
366
|
|
|
365
367
|
### Protocol specification
|
|
@@ -372,15 +374,21 @@ APP is an open protocol for adding privacy and infrastructure protection to any
|
|
|
372
374
|
|
|
373
375
|
| Method | Params | Returns | Description |
|
|
374
376
|
|--------|--------|---------|-------------|
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
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 |
|
|
377
382
|
| `reset` | `{}` | `{ok, summary}` | Clear all mappings |
|
|
378
383
|
| `stats` | `{}` | `{storeMappings, ruleHits, ...}` | Engine statistics |
|
|
379
384
|
| `health` | `{}` | `{uptime, requests, avgLatencyMs}` | Liveness check |
|
|
380
385
|
| `configure` | `{config}` | `{ok}` | Hot-reload configuration |
|
|
381
386
|
| `batch` | `{operations: [{direction, text}]}` | `{results: [...]}` | Batch obfuscate/deobfuscate |
|
|
387
|
+
| `setPartition` | `{partition}` | `{ok}` | Switch mapping namespace (multi-tenant) |
|
|
382
388
|
| `shutdown` | `{}` | `{ok}` | Graceful shutdown (flushes stats) |
|
|
383
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
|
+
|
|
384
392
|
### Python client
|
|
385
393
|
|
|
386
394
|
```python
|
|
@@ -409,16 +417,16 @@ client.stop()
|
|
|
409
417
|
npm install
|
|
410
418
|
npm run build # compile TypeScript
|
|
411
419
|
npm run lint # type-check without emitting
|
|
412
|
-
npm test # unit + harness (1,
|
|
420
|
+
npm test # unit + harness (1,238 tests, no Docker)
|
|
413
421
|
npm run test:docker # Docker E2E — real OpenClaw, all channels (192 tests)
|
|
414
|
-
npm run test:all # everything (1,
|
|
422
|
+
npm run test:all # everything (1,430 tests)
|
|
415
423
|
```
|
|
416
424
|
|
|
417
425
|
### Test layers
|
|
418
426
|
|
|
419
427
|
| Layer | Command | Tests | What it covers |
|
|
420
428
|
|-------|---------|-------|---------------|
|
|
421
|
-
| Unit | `npm run test:unit` |
|
|
429
|
+
| Unit | `npm run test:unit` | 879 | Obfuscator, detectors, generators, store, config |
|
|
422
430
|
| APP Harness | `npm run test:integration` | 359 | 48 scenario files via mock LLM, no OpenClaw |
|
|
423
431
|
| Docker E2E | `npm run test:docker` | 192 | Real OpenClaw gateway, Slack/WhatsApp/Cron/TUI channels, 153 regression scenarios |
|
|
424
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
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
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
|
-
|
|
308
|
-
if (typeof msg.content === "string")
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shroud-privacy",
|
|
3
|
-
"version": "2.2.
|
|
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-
|
|
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",
|