shroud-privacy 2.0.12 → 2.0.14

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 CHANGED
@@ -164,11 +164,47 @@ 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 from the terminal:
178
+
179
+ ```bash
180
+ node ~/.openclaw/extensions/shroud-privacy/scripts/shroud-stats.mjs # live rule table
181
+ node ~/.openclaw/extensions/shroud-privacy/scripts/shroud-stats.mjs --json # JSON output
182
+ node ~/.openclaw/extensions/shroud-privacy/scripts/shroud-stats.mjs --test "Contact john@acme.com"
183
+ ```
184
+
185
+ Tip: create an alias for convenience:
186
+ ```bash
187
+ alias shroud-stats="node ~/.openclaw/extensions/shroud-privacy/scripts/shroud-stats.mjs"
188
+ ```
189
+
190
+ 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.
191
+
192
+ ### Auto-patching on first install
193
+
194
+ On first load, Shroud automatically patches pi-ai's `EventStream.push()` to enable streaming deobfuscation across all LLM providers and delivery channels. The patch:
195
+
196
+ 1. Backs up the original file (`.shroud-backup`)
197
+ 2. Injects a 4-line hook that calls `globalThis.__shroudStreamDeobfuscate`
198
+ 3. Clears the Node.js V8 compile cache
199
+ 4. Triggers a gateway restart via SIGUSR1
200
+
201
+ On subsequent loads, the patch is detected and skipped. To revert: restore the `.shroud-backup` file and restart.
202
+
167
203
  ### Rule hit counters
168
204
 
169
205
  Shroud tracks per-rule match counts for the lifetime of the process. Counters appear in three places:
170
206
 
171
- - **`shroud-stats` CLI** — run `node scripts/shroud-stats.mjs` to see all rules with status, confidence, and hit counts. Shows live cumulative stats from the running OpenClaw gateway via `/tmp/shroud-stats.json`. Use `--test "text with PII"` to test detection against sample input.
207
+ - **`shroud-stats` CLI** — see [Conversational tools](#conversational-tools) above for usage. Shows all rules with status, confidence, and hit counts from the running gateway.
172
208
  - **Audit log lines** — `byRule=regex:email:3,regex:ipv4:2,...` alongside the existing `byCat` field.
173
209
  - **`getStats()`** — the `ruleHits` object in the stats response, useful for programmatic access.
174
210
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "shroud-privacy",
3
3
  "name": "Shroud",
4
- "version": "2.0.12",
4
+ "version": "2.0.14",
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.12",
3
+ "version": "2.0.14",
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)`);