shroud-privacy 2.1.0 → 2.2.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/README.md +29 -32
- package/dist/hooks.js +143 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
| `before_tool_call` | LLM → Tool | Deobfuscate tool parameters + track tool chain depth |
|
|
27
27
|
| `tool_result_persist` | Tool → History | Obfuscate tool results before storing |
|
|
28
28
|
| `message_sending` | Agent → User | Deobfuscate outbound messages (all channels) |
|
|
29
|
+
| `globalThis.__shroudDeobfuscate` | Agent → Channel | Global deobfuscation hook — called by OpenClaw before ANY channel send |
|
|
29
30
|
|
|
30
|
-
> **Privacy guarantee:** Shroud intercepts ALL outbound LLM API calls (Anthropic, OpenAI, Google, any provider) at the `fetch` level and obfuscates PII in every message — including assistant history and Slack `<mailto:>` markup — before it leaves the process. No PII reaches the LLM. On the
|
|
31
|
+
> **Privacy guarantee:** Shroud intercepts ALL outbound LLM API calls (Anthropic, OpenAI, Google, any provider) at the `fetch` level and obfuscates PII in every message — including assistant history and Slack `<mailto:>` markup — before it leaves the process. No PII reaches the LLM. On the channel delivery side, Shroud registers `globalThis.__shroudDeobfuscate` — a single function that OpenClaw calls before sending to ANY channel (Slack, WhatsApp, Signal, web, etc.). One hook, all channels, transparent no-op if Shroud isn't loaded.
|
|
31
32
|
|
|
32
|
-
> **Requires OpenClaw 2026.3.24 or later
|
|
33
|
+
> **Requires OpenClaw 2026.3.24 or later** with the channel delivery patch (see [OpenClaw patch](#openclaw-channel-delivery-patch) below).
|
|
33
34
|
|
|
34
35
|
## Install
|
|
35
36
|
|
|
@@ -103,38 +104,16 @@ Other methods: `reset`, `stats`, `health`, `configure`, `shutdown`.
|
|
|
103
104
|
git clone https://github.com/walterkeating-stack/shroud.git
|
|
104
105
|
cd shroud
|
|
105
106
|
npm install && npm run build
|
|
106
|
-
|
|
107
|
+
openclaw plugins install --path .
|
|
108
|
+
openclaw gateway restart
|
|
107
109
|
```
|
|
108
110
|
|
|
109
111
|
## Updating
|
|
110
112
|
|
|
111
|
-
OpenClaw doesn't have a `plugins update` command yet, so updating requires removing the old install first. A helper script is included:
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
# Update to latest version (preserves your config)
|
|
115
|
-
bash scripts/update-openclaw-plugin.sh
|
|
116
|
-
|
|
117
|
-
# Update to a specific version
|
|
118
|
-
bash scripts/update-openclaw-plugin.sh <version>
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
The script saves your plugin config from `openclaw.json`, removes the old extension, reinstalls from npm, restores your config, and restarts the gateway.
|
|
122
|
-
|
|
123
|
-
### Manual update
|
|
124
|
-
|
|
125
|
-
If you prefer to do it manually:
|
|
126
|
-
|
|
127
113
|
```bash
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# 2. Reinstall (this resets your plugin config to defaults)
|
|
114
|
+
# Remove old plugin, reinstall from npm, restart
|
|
115
|
+
openclaw plugins remove shroud-privacy
|
|
132
116
|
openclaw plugins install shroud-privacy
|
|
133
|
-
|
|
134
|
-
# 3. Re-apply your config in ~/.openclaw/openclaw.json
|
|
135
|
-
# (under plugins.entries."shroud-privacy".config)
|
|
136
|
-
|
|
137
|
-
# 4. Restart
|
|
138
117
|
openclaw gateway restart
|
|
139
118
|
```
|
|
140
119
|
|
|
@@ -261,10 +240,28 @@ Shroud uses runtime prototype patches — **no OpenClaw files are modified**:
|
|
|
261
240
|
|
|
262
241
|
**Inbound (LLM → User):** Patches `EventStream.prototype.push()` to deobfuscate streaming responses in real-time. Fake values are replaced with real values as they stream.
|
|
263
242
|
|
|
264
|
-
**Channel delivery:**
|
|
243
|
+
**Channel delivery:** Shroud registers `globalThis.__shroudDeobfuscate(text)` — a single global function that converts fake values back to real values. OpenClaw calls this once in its generic message delivery function, before sending to any channel. If Shroud isn't loaded, the function doesn't exist — transparent no-op. The `message_sending` hook provides a backup deobfuscation path.
|
|
265
244
|
|
|
266
245
|
All patches are applied once at plugin load and are idempotent — subsequent loads detect and skip them.
|
|
267
246
|
|
|
247
|
+
### OpenClaw channel delivery patch
|
|
248
|
+
|
|
249
|
+
Shroud requires a one-line addition to OpenClaw's message delivery function — the single point where all outbound channel messages pass through. This covers ALL channels (Slack, WhatsApp, Signal, Telegram, web, etc.) with one change:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
// In OpenClaw's generic message delivery function:
|
|
253
|
+
const deob = globalThis.__shroudDeobfuscate;
|
|
254
|
+
if (deob && typeof text === 'string') text = deob(text);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Why this works:**
|
|
258
|
+
- **One change, all channels** — no per-channel hooks needed
|
|
259
|
+
- **Transparent** — if Shroud isn't loaded, `globalThis.__shroudDeobfuscate` is `undefined`, the `if` is false, zero overhead
|
|
260
|
+
- **All releases** — works on any OpenClaw version that sends channel messages through a common delivery path
|
|
261
|
+
- **Last-moment deobfuscation** — fakes are replaced with real values at the latest possible point, just before the HTTP API call
|
|
262
|
+
|
|
263
|
+
The `deploy-local.sh` script includes a post-install verification step that tests the full chain.
|
|
264
|
+
|
|
268
265
|
### Rule hit counters
|
|
269
266
|
|
|
270
267
|
Shroud tracks per-rule match counts for the lifetime of the process. Counters appear in three places:
|
|
@@ -478,7 +475,7 @@ npm run lint # type-check without emitting
|
|
|
478
475
|
|
|
479
476
|
```bash
|
|
480
477
|
npm run build
|
|
481
|
-
|
|
478
|
+
openclaw plugins install --path .
|
|
482
479
|
openclaw gateway restart
|
|
483
480
|
```
|
|
484
481
|
|
|
@@ -491,8 +488,8 @@ openclaw gateway restart
|
|
|
491
488
|
# 2. Update CHANGELOG.md
|
|
492
489
|
# 3. Commit and tag
|
|
493
490
|
git add -A
|
|
494
|
-
git commit -m "
|
|
495
|
-
git tag
|
|
491
|
+
git commit -m "release: vX.Y.Z"
|
|
492
|
+
git tag vX.Y.Z
|
|
496
493
|
git push && git push --tags
|
|
497
494
|
```
|
|
498
495
|
|
package/dist/hooks.js
CHANGED
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
* providers before OpenClaw processes them
|
|
15
15
|
*/
|
|
16
16
|
import { createHash, randomBytes } from "node:crypto";
|
|
17
|
-
import { writeFileSync } from "node:fs";
|
|
17
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { BUILTIN_PATTERNS } from "./detectors/regex.js";
|
|
19
19
|
const STATS_FILE = process.env.SHROUD_STATS_FILE || "/tmp/shroud-stats.json";
|
|
20
20
|
function getSharedObfuscator(fallback) {
|
|
21
21
|
return globalThis.__shroudObfuscator || fallback;
|
|
22
22
|
}
|
|
23
|
+
const MAPPINGS_FILE = process.env.SHROUD_MAPPINGS_FILE || "/tmp/shroud-mappings.json";
|
|
23
24
|
function dumpStatsFile(fallback) {
|
|
24
25
|
try {
|
|
25
26
|
const ob = getSharedObfuscator(fallback);
|
|
@@ -32,6 +33,30 @@ function dumpStatsFile(fallback) {
|
|
|
32
33
|
catch {
|
|
33
34
|
// best-effort
|
|
34
35
|
}
|
|
36
|
+
// Also dump mapping table for child process fetch intercept
|
|
37
|
+
try {
|
|
38
|
+
const ob = getSharedObfuscator(fallback);
|
|
39
|
+
const store = ob._store;
|
|
40
|
+
if (store?.allMappings) {
|
|
41
|
+
const allMap = store.allMappings();
|
|
42
|
+
const mappings = {};
|
|
43
|
+
for (const [real, fake] of allMap) {
|
|
44
|
+
mappings[real] = fake;
|
|
45
|
+
}
|
|
46
|
+
const config = ob.config;
|
|
47
|
+
writeFileSync(MAPPINGS_FILE, JSON.stringify({
|
|
48
|
+
mappings,
|
|
49
|
+
secretKey: config.secretKey,
|
|
50
|
+
persistentSalt: config.persistentSalt,
|
|
51
|
+
}) + "\n");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(MAPPINGS_FILE + ".error", String(e) + "\n");
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}
|
|
35
60
|
}
|
|
36
61
|
// ---------------------------------------------------------------------------
|
|
37
62
|
// Hashing utilities (audit proof hashes)
|
|
@@ -169,6 +194,12 @@ export function registerHooks(api, obfuscator) {
|
|
|
169
194
|
// instance while message_sending fires on another (via the delivery subsystem).
|
|
170
195
|
// Without sharing, the delivery instance has an empty mapping store and
|
|
171
196
|
// cannot deobfuscate CGNAT surrogates in outbound channel messages.
|
|
197
|
+
function stripSlackLinksForHook(text) {
|
|
198
|
+
text = text.replace(/<mailto:[^|>]+\|([^>]*)>/g, "$1");
|
|
199
|
+
text = text.replace(/<https?:\/\/[^|>]+\|([^>]*)>/g, "$1");
|
|
200
|
+
text = text.replace(/<(https?:\/\/[^>]+)>/g, "$1");
|
|
201
|
+
return text;
|
|
202
|
+
}
|
|
172
203
|
if (process.env.NODE_ENV !== "test") {
|
|
173
204
|
const g = globalThis;
|
|
174
205
|
if (g.__shroudObfuscator) {
|
|
@@ -183,6 +214,16 @@ export function registerHooks(api, obfuscator) {
|
|
|
183
214
|
const ob = () => getSharedObfuscator(obfuscator);
|
|
184
215
|
const config = ob().config;
|
|
185
216
|
const auditActive = config.auditEnabled || config.verboseLogging;
|
|
217
|
+
// Write initial config to mappings file so the fetch preload in child
|
|
218
|
+
// processes can create a compatible obfuscator on the first API call.
|
|
219
|
+
try {
|
|
220
|
+
writeFileSync(MAPPINGS_FILE, JSON.stringify({
|
|
221
|
+
mappings: {},
|
|
222
|
+
secretKey: config.secretKey,
|
|
223
|
+
persistentSalt: config.persistentSalt,
|
|
224
|
+
}) + "\n");
|
|
225
|
+
}
|
|
226
|
+
catch { /* best-effort */ }
|
|
186
227
|
// -----------------------------------------------------------------------
|
|
187
228
|
// 1. before_prompt_build (async): obfuscate user prompt
|
|
188
229
|
// -----------------------------------------------------------------------
|
|
@@ -192,17 +233,46 @@ export function registerHooks(api, obfuscator) {
|
|
|
192
233
|
if (ob().toolDepth > 0) {
|
|
193
234
|
ob().resetToolDepth();
|
|
194
235
|
}
|
|
236
|
+
let totalEntities = 0;
|
|
237
|
+
// Obfuscate the system prompt
|
|
195
238
|
const prompt = event?.prompt;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
239
|
+
let obfuscatedPrompt;
|
|
240
|
+
if (typeof prompt === "string" && prompt) {
|
|
241
|
+
const cleaned = stripSlackLinksForHook(prompt);
|
|
242
|
+
const result = ob().obfuscate(cleaned);
|
|
243
|
+
if (result.entities.length > 0 || cleaned !== prompt) {
|
|
244
|
+
obfuscatedPrompt = result.entities.length > 0 ? result.obfuscated : cleaned;
|
|
245
|
+
totalEntities += result.entities.length;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Pre-create mappings for PII in user messages WITHOUT mutating them.
|
|
249
|
+
// This ensures the fetch preload (in child process) has the mappings
|
|
250
|
+
// before the API call. The actual replacement happens in the preload.
|
|
251
|
+
api.logger?.info(`[shroud] event keys: ${Object.keys(event || {}).join(",")}`);
|
|
252
|
+
if (Array.isArray(event?.messages)) {
|
|
253
|
+
for (const msg of event.messages) {
|
|
254
|
+
const texts = [];
|
|
255
|
+
if (typeof msg.content === "string")
|
|
256
|
+
texts.push(msg.content);
|
|
257
|
+
else if (Array.isArray(msg.content)) {
|
|
258
|
+
for (const b of msg.content) {
|
|
259
|
+
if (b?.type === "text" && typeof b.text === "string")
|
|
260
|
+
texts.push(b.text);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const text of texts) {
|
|
264
|
+
const cleaned = stripSlackLinksForHook(text);
|
|
265
|
+
const result = ob().obfuscate(cleaned);
|
|
266
|
+
totalEntities += result.entities.length;
|
|
267
|
+
// Do NOT mutate — just creating mappings in the store
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (totalEntities === 0)
|
|
200
272
|
return;
|
|
201
273
|
dumpStatsFile(obfuscator);
|
|
202
|
-
api.logger?.info(`[shroud] before_prompt_build: obfuscated ${
|
|
203
|
-
return {
|
|
204
|
-
prompt: result.obfuscated,
|
|
205
|
-
};
|
|
274
|
+
api.logger?.info(`[shroud] before_prompt_build: obfuscated ${totalEntities} entities (mappings synced)`);
|
|
275
|
+
return obfuscatedPrompt ? { systemPrompt: obfuscatedPrompt } : undefined;
|
|
206
276
|
});
|
|
207
277
|
// -----------------------------------------------------------------------
|
|
208
278
|
// 2. before_message_write (SYNC): bidirectional privacy filter
|
|
@@ -222,6 +292,30 @@ export function registerHooks(api, obfuscator) {
|
|
|
222
292
|
const role = msg.role ?? "";
|
|
223
293
|
// --- Assistant messages: DEOBFUSCATE (fakes → real values) ---
|
|
224
294
|
if (role === "assistant") {
|
|
295
|
+
// Import mappings created by the fetch preload (child process) so we can
|
|
296
|
+
// deobfuscate fakes that the preload generated independently.
|
|
297
|
+
try {
|
|
298
|
+
const raw = readFileSync(MAPPINGS_FILE, "utf-8");
|
|
299
|
+
const data = JSON.parse(raw.trim());
|
|
300
|
+
const preloadMappings = data?.mappings;
|
|
301
|
+
if (preloadMappings && typeof preloadMappings === "object") {
|
|
302
|
+
const store = ob()._store;
|
|
303
|
+
if (store?.put) {
|
|
304
|
+
for (const [real, fake] of Object.entries(preloadMappings)) {
|
|
305
|
+
if (typeof fake === "string" && store.getFake(real) === undefined) {
|
|
306
|
+
store.put(real, fake, "preload");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// best-effort — file may not exist yet
|
|
314
|
+
}
|
|
315
|
+
const _raw = typeof msg.content === "string" ? msg.content :
|
|
316
|
+
Array.isArray(msg.content) ? msg.content.map((b) => b?.text || "").join("") : "";
|
|
317
|
+
if (_raw.length < 500)
|
|
318
|
+
api.logger?.info(`[shroud][raw-assistant] ${_raw}`);
|
|
225
319
|
if (typeof msg.content === "string") {
|
|
226
320
|
const { text: deobfuscated, replacementCount } = ob().deobfuscateWithStats(msg.content);
|
|
227
321
|
if (deobfuscated === msg.content)
|
|
@@ -368,6 +462,16 @@ export function registerHooks(api, obfuscator) {
|
|
|
368
462
|
api.on("before_tool_call", async (event) => {
|
|
369
463
|
if (!event?.params || typeof event.params !== "object")
|
|
370
464
|
return;
|
|
465
|
+
// Block the message tool for send actions. The gateway auto-delivers
|
|
466
|
+
// responses — using the message tool causes duplicate messages (one
|
|
467
|
+
// deobfuscated via streaming, one with fakes from the tool call).
|
|
468
|
+
if (event.toolName === "message") {
|
|
469
|
+
api.logger?.info(`[shroud] message tool call: action=${event.params?.action}`);
|
|
470
|
+
if (event.params?.action === "send") {
|
|
471
|
+
api.logger?.info("[shroud] blocked message tool send (prevents duplicate delivery)");
|
|
472
|
+
return { block: true, blockReason: "Response is delivered automatically. Do not use the message tool to send replies." };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
371
475
|
// Tool chain depth tracking
|
|
372
476
|
const depth = ob().enterToolCall();
|
|
373
477
|
if (depth > config.maxToolDepth) {
|
|
@@ -517,6 +621,15 @@ export function registerHooks(api, obfuscator) {
|
|
|
517
621
|
// but the final message will be correct.
|
|
518
622
|
const SHROUD_BUF = Symbol("shroudStreamBuf");
|
|
519
623
|
globalThis.__shroudStreamDeobfuscate = (stream, event) => {
|
|
624
|
+
// Streaming deobfuscation is DISABLED. On OpenClaw 2026.3.24+ with
|
|
625
|
+
// streaming: off, before_message_write handles all deobfuscation.
|
|
626
|
+
// The streaming buffer causes text artifacts when fake/real lengths
|
|
627
|
+
// differ — accumulated partial fakes persist in channel delivery
|
|
628
|
+
// alongside the corrected final text, producing duplicate content.
|
|
629
|
+
//
|
|
630
|
+
// The message_end handler below still runs to deobfuscate the final
|
|
631
|
+
// content blocks (partial/message), ensuring the delivered message
|
|
632
|
+
// has real values.
|
|
520
633
|
const isTextDelta = event.type === "text_delta";
|
|
521
634
|
const isMessageUpdateTextDelta = event.type === "message_update" &&
|
|
522
635
|
event.assistantMessageEvent?.type === "text_delta";
|
|
@@ -573,8 +686,6 @@ export function registerHooks(api, obfuscator) {
|
|
|
573
686
|
event.type === "error" || event.type === "agent_end" ||
|
|
574
687
|
(event.type === "message_update" && (event.assistantMessageEvent?.type === "text_end"));
|
|
575
688
|
if (isEnd) {
|
|
576
|
-
// Deobfuscate content blocks in the event's message/partial
|
|
577
|
-
// (corrects any partial fakes left from streaming)
|
|
578
689
|
const targets = [
|
|
579
690
|
event.message, event.partial,
|
|
580
691
|
event.assistantMessageEvent?.partial,
|
|
@@ -585,8 +696,9 @@ export function registerHooks(api, obfuscator) {
|
|
|
585
696
|
for (const block of target.content) {
|
|
586
697
|
if (block?.type === "text" && typeof block.text === "string") {
|
|
587
698
|
const deob = ob().deobfuscate(block.text);
|
|
588
|
-
if (deob !== block.text)
|
|
699
|
+
if (deob !== block.text) {
|
|
589
700
|
block.text = deob;
|
|
701
|
+
}
|
|
590
702
|
}
|
|
591
703
|
}
|
|
592
704
|
}
|
|
@@ -600,7 +712,6 @@ export function registerHooks(api, obfuscator) {
|
|
|
600
712
|
}
|
|
601
713
|
catch { /* best-effort */ }
|
|
602
714
|
}
|
|
603
|
-
// Always dump stats on message_end to capture any counter changes
|
|
604
715
|
dumpStatsFile(obfuscator);
|
|
605
716
|
delete stream[SHROUD_BUF];
|
|
606
717
|
}
|
|
@@ -608,7 +719,24 @@ export function registerHooks(api, obfuscator) {
|
|
|
608
719
|
};
|
|
609
720
|
api.logger?.info("[shroud] Installed global streaming deobfuscation hook");
|
|
610
721
|
// -----------------------------------------------------------------------
|
|
611
|
-
// 7.
|
|
722
|
+
// 7. Global deobfuscation hook for OpenClaw channel delivery.
|
|
723
|
+
// OpenClaw calls this once, in its generic message-delivery function,
|
|
724
|
+
// before sending to ANY channel (Slack, WhatsApp, Signal, web, etc.).
|
|
725
|
+
// Transparent: if Shroud isn't loaded the global doesn't exist — no-op.
|
|
726
|
+
// Works on all past + present releases with a single 3-line patch:
|
|
727
|
+
//
|
|
728
|
+
// const deob = globalThis.__shroudDeobfuscate;
|
|
729
|
+
// if (deob && typeof text === 'string') text = deob(text);
|
|
730
|
+
//
|
|
731
|
+
// -----------------------------------------------------------------------
|
|
732
|
+
globalThis.__shroudDeobfuscate = (text) => {
|
|
733
|
+
if (typeof text !== "string")
|
|
734
|
+
return text;
|
|
735
|
+
return ob().deobfuscate(text);
|
|
736
|
+
};
|
|
737
|
+
api.logger?.info("[shroud] Registered globalThis.__shroudDeobfuscate for channel delivery");
|
|
738
|
+
// -----------------------------------------------------------------------
|
|
739
|
+
// 8. Outbound fetch intercept: obfuscate ALL user message content before
|
|
612
740
|
// it reaches any LLM API. This is the last line of defense — it works
|
|
613
741
|
// regardless of which hooks fire, which OpenClaw version is running,
|
|
614
742
|
// and which LLM provider is used (Anthropic, OpenAI, Google, etc.).
|
|
@@ -630,6 +758,7 @@ export function registerHooks(api, obfuscator) {
|
|
|
630
758
|
"/v1/models/", // Google Vertex AI
|
|
631
759
|
];
|
|
632
760
|
const originalFetch = globalThis.fetch;
|
|
761
|
+
api.logger?.info(`[shroud][fetch-guard] hasFetch=${!!originalFetch} patched=${!!globalThis.__shroudFetchPatched}`);
|
|
633
762
|
if (originalFetch && !(globalThis.__shroudFetchPatched)) {
|
|
634
763
|
globalThis.__shroudFetchPatched = true;
|
|
635
764
|
globalThis.fetch = async function shroudFetchInterceptor(input, init) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shroud-privacy",
|
|
3
3
|
"name": "Shroud",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
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.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Privacy obfuscation for AI agents — detects PII and replaces with deterministic fake values before anything reaches the LLM. Works with OpenClaw (plugin) or any agent (APP protocol).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|