shroud-privacy 2.0.12 → 2.0.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 +31 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -1
- package/scripts/shroud-stats.mjs +125 -0
package/README.md
CHANGED
|
@@ -164,6 +164,37 @@ Disable or tune individual detection rules by name. Rule names match the built-i
|
|
|
164
164
|
|
|
165
165
|
Rules not listed keep their defaults. Overrides apply to both direct regex detection and code-aware detection.
|
|
166
166
|
|
|
167
|
+
### Conversational tools
|
|
168
|
+
|
|
169
|
+
Shroud registers tools that the LLM can call during conversations:
|
|
170
|
+
|
|
171
|
+
| Tool | What it does |
|
|
172
|
+
|------|-------------|
|
|
173
|
+
| `shroud-stats` | Show all detection rules with status, confidence, hit counts, store size, and config summary |
|
|
174
|
+
| `shroud_status` | Quick stats: entity counts, session info, audit status (JSON) |
|
|
175
|
+
| `shroud_reset` | Clear all mappings and start a fresh privacy session |
|
|
176
|
+
|
|
177
|
+
You can also run the stats CLI directly:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
shroud-stats # live rule table from running gateway
|
|
181
|
+
shroud-stats --json # machine-readable JSON output
|
|
182
|
+
shroud-stats --test "Contact john@acme.com" # test detection
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The CLI reads live stats from `/tmp/shroud-stats.json` (override with `SHROUD_STATS_FILE` env var). The stats file is updated by the running gateway on every obfuscation event.
|
|
186
|
+
|
|
187
|
+
### Auto-patching on first install
|
|
188
|
+
|
|
189
|
+
On first load, Shroud automatically patches pi-ai's `EventStream.push()` to enable streaming deobfuscation across all LLM providers and delivery channels. The patch:
|
|
190
|
+
|
|
191
|
+
1. Backs up the original file (`.shroud-backup`)
|
|
192
|
+
2. Injects a 4-line hook that calls `globalThis.__shroudStreamDeobfuscate`
|
|
193
|
+
3. Clears the Node.js V8 compile cache
|
|
194
|
+
4. Triggers a gateway restart via SIGUSR1
|
|
195
|
+
|
|
196
|
+
On subsequent loads, the patch is detected and skipped. To revert: restore the `.shroud-backup` file and restart.
|
|
197
|
+
|
|
167
198
|
### Rule hit counters
|
|
168
199
|
|
|
169
200
|
Shroud tracks per-rule match counts for the lifetime of the process. Counters appear in three places:
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shroud-privacy",
|
|
3
3
|
"name": "Shroud",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.13",
|
|
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,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shroud-privacy",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"description": "Privacy obfuscation plugin for OpenClaw — detects sensitive data and replaces with deterministic fake values",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist/",
|
|
10
|
+
"scripts/shroud-stats.mjs",
|
|
10
11
|
"openclaw.plugin.json",
|
|
11
12
|
"LICENSE",
|
|
12
13
|
"NOTICE"
|
|
13
14
|
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"shroud-stats": "scripts/shroud-stats.mjs"
|
|
17
|
+
},
|
|
14
18
|
"scripts": {
|
|
15
19
|
"build": "tsc",
|
|
16
20
|
"test": "vitest run",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shroud Stats CLI — show active rules, hit counts, store size.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/shroud-stats.mjs # live stats from running gateway
|
|
7
|
+
* node scripts/shroud-stats.mjs --test "some text" # obfuscate text then show hits
|
|
8
|
+
*
|
|
9
|
+
* Reads live stats from /tmp/shroud-stats.json (written by the Shroud plugin).
|
|
10
|
+
* Falls back to a fresh instance if no stats file exists.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { dirname, resolve } from "node:path";
|
|
15
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const distDir = resolve(__dirname, "..", "dist");
|
|
19
|
+
|
|
20
|
+
const { resolveConfig } = await import(resolve(distDir, "config.js"));
|
|
21
|
+
const { Obfuscator } = await import(resolve(distDir, "obfuscator.js"));
|
|
22
|
+
const { BUILTIN_PATTERNS } = await import(resolve(distDir, "detectors", "regex.js"));
|
|
23
|
+
|
|
24
|
+
// Load config from OpenClaw config file if available
|
|
25
|
+
let pluginConfig = {};
|
|
26
|
+
try {
|
|
27
|
+
const configPath = resolve(process.env.HOME || "~", ".openclaw", "openclaw.json");
|
|
28
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
29
|
+
const entry = raw?.plugins?.entries?.["shroud-privacy"];
|
|
30
|
+
if (entry?.config) pluginConfig = entry.config;
|
|
31
|
+
} catch {
|
|
32
|
+
// skip
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const config = resolveConfig(pluginConfig);
|
|
36
|
+
const overrides = config.detectorOverrides;
|
|
37
|
+
|
|
38
|
+
// Try to read live stats from the bridge stats file
|
|
39
|
+
const STATS_FILE = process.env.SHROUD_STATS_FILE || "/tmp/shroud-stats.json";
|
|
40
|
+
let liveStats = null;
|
|
41
|
+
let source = "fresh instance";
|
|
42
|
+
|
|
43
|
+
if (existsSync(STATS_FILE) && !process.argv.includes("--test")) {
|
|
44
|
+
try {
|
|
45
|
+
liveStats = JSON.parse(readFileSync(STATS_FILE, "utf-8"));
|
|
46
|
+
source = `live (pid ${liveStats.pid}, updated ${liveStats.updatedAt})`;
|
|
47
|
+
} catch {
|
|
48
|
+
// fall through to fresh instance
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If --test flag or no live stats, use a fresh obfuscator
|
|
53
|
+
let ruleHits;
|
|
54
|
+
let storeMappings;
|
|
55
|
+
|
|
56
|
+
if (liveStats && !process.argv.includes("--test")) {
|
|
57
|
+
ruleHits = liveStats.ruleHits || {};
|
|
58
|
+
storeMappings = liveStats.storeMappings || 0;
|
|
59
|
+
} else {
|
|
60
|
+
const obf = new Obfuscator(config);
|
|
61
|
+
const testIdx = process.argv.indexOf("--test");
|
|
62
|
+
if (testIdx !== -1) {
|
|
63
|
+
const text = process.argv.slice(testIdx + 1).join(" ");
|
|
64
|
+
if (text) {
|
|
65
|
+
obf.obfuscate(text);
|
|
66
|
+
source = `test input (${text.length} chars)`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const stats = obf.getStats();
|
|
70
|
+
ruleHits = stats.ruleHits;
|
|
71
|
+
storeMappings = stats.storeMappings;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build rule table
|
|
75
|
+
const rules = BUILTIN_PATTERNS.map((p) => {
|
|
76
|
+
const ov = overrides[p.name];
|
|
77
|
+
const enabled = ov?.enabled !== false;
|
|
78
|
+
const confidence = ov?.confidence ?? p.confidence;
|
|
79
|
+
const hits = ruleHits[`regex:${p.name}`] ?? 0;
|
|
80
|
+
return { name: p.name, category: p.category, enabled, confidence, hits };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
rules.sort((a, b) => b.hits - a.hits);
|
|
84
|
+
|
|
85
|
+
// JSON output mode
|
|
86
|
+
if (process.argv.includes("--json")) {
|
|
87
|
+
const output = {
|
|
88
|
+
source,
|
|
89
|
+
storeMappings,
|
|
90
|
+
auditEnabled: config.auditEnabled || config.verboseLogging || false,
|
|
91
|
+
overrideCount: Object.keys(overrides).length,
|
|
92
|
+
rules,
|
|
93
|
+
};
|
|
94
|
+
if (liveStats) {
|
|
95
|
+
output.pid = liveStats.pid;
|
|
96
|
+
output.updatedAt = liveStats.updatedAt;
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Format table
|
|
103
|
+
const maxName = Math.max(...rules.map((r) => r.name.length), 4);
|
|
104
|
+
const maxCat = Math.max(...rules.map((r) => r.category.length), 8);
|
|
105
|
+
|
|
106
|
+
const header = `${"Rule".padEnd(maxName)} ${"Category".padEnd(maxCat)} Status Conf Hits`;
|
|
107
|
+
const sep = "─".repeat(header.length + 20);
|
|
108
|
+
|
|
109
|
+
console.log(`Shroud Rule Hits (${source})`);
|
|
110
|
+
console.log(sep);
|
|
111
|
+
console.log(header);
|
|
112
|
+
console.log(sep);
|
|
113
|
+
|
|
114
|
+
for (const r of rules) {
|
|
115
|
+
const status = r.enabled ? "active" : "DISABLED";
|
|
116
|
+
const bar = r.hits > 0 ? " " + "█".repeat(Math.min(Math.ceil(Math.log2(r.hits + 1)), 16)) : "";
|
|
117
|
+
console.log(
|
|
118
|
+
`${r.name.padEnd(maxName)} ${r.category.padEnd(maxCat)} ${status.padEnd(8)} ${r.confidence.toFixed(2).padStart(4)} ${String(r.hits).padStart(5)}${bar}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(sep);
|
|
123
|
+
console.log(`Store: ${storeMappings} active mappings`);
|
|
124
|
+
console.log(`Audit: ${config.auditEnabled || config.verboseLogging ? "enabled" : "disabled"}`);
|
|
125
|
+
console.log(`Config: detectorOverrides has ${Object.keys(overrides).length} override(s)`);
|