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 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:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "shroud-privacy",
3
3
  "name": "Shroud",
4
- "version": "2.0.12",
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.12",
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)`);