shroud-privacy 2.5.5 → 2.5.7

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
@@ -13,12 +13,15 @@
13
13
  <a href="#install">Install</a> &middot;
14
14
  <a href="#why-shroud">Why Shroud</a> &middot;
15
15
  <a href="#configure">Configure</a> &middot;
16
+ <a href="docs/integrations.md">Integrations</a> &middot;
16
17
  <a href="#agent-privacy-protocol-app">APP Protocol</a> &middot;
17
18
  <a href="CHANGELOG.md">Changelog</a>
18
19
  </p>
19
20
 
20
21
  > Apache 2.0 &middot; Zero runtime dependencies &middot; Anthropic + OpenAI + Google supported &middot; Prompt-caching friendly &middot; Works with [OpenClaw](https://openclaw.ai), [Hermes Agent](https://github.com/nousresearch/hermes-agent), or any agent via [APP](#agent-privacy-protocol-app)
21
22
 
23
+ **Detailed integration reference:** [`docs/integrations.md`](docs/integrations.md)
24
+
22
25
  ---
23
26
 
24
27
  ## Why Shroud
@@ -76,7 +79,7 @@ Shroud does not guarantee compliance — regex-based detection has limitations (
76
79
  | `globalThis.__shroudStreamDeobfuscate` | LLM → Agent | Streaming event deobfuscation hook |
77
80
  | `globalThis.__shroudDeobfuscate` | Agent → Channel | Global deobfuscation hook — called by OpenClaw before ANY channel send |
78
81
 
79
- > **How it works:** Shroud intercepts ALL outbound LLM API calls (Anthropic, OpenAI, Google, any provider) at the `fetch` level and obfuscates detected entities in every message — including assistant history and Slack `<mailto:>` markup — before it leaves the process. On the response side, SSE streaming is deobfuscated per content block with buffered flushing. Every delivery path (Slack, WhatsApp, TUI, Telegram, Discord, Signal, web) gets real text automatically. Zero host patches required.
82
+ > **How it works:** Shroud intercepts ALL outbound LLM API calls (Anthropic, OpenAI, Google, any provider) at the `fetch` level and obfuscates detected entities in every message — including assistant history, Slack `<mailto:>` markup, and OpenAI Responses / Codex `input_text` blocks — before it leaves the process. On the response side, SSE streaming is deobfuscated per content block with buffered flushing, and OpenAI Responses `output_text` blocks are treated the same as plain `text` blocks. Every delivery path (Slack, WhatsApp, TUI, Telegram, Discord, Signal, web) gets real text automatically. Zero host patches required.
80
83
 
81
84
  > **Requires OpenClaw 2026.3.24 or later.**
82
85
 
@@ -138,7 +141,20 @@ Add to your project's `.mcp.json` or `~/.claude/.mcp.json`:
138
141
  }
139
142
  ```
140
143
 
141
- That's it — the MCP server auto-starts the privacy engine. Claude gains six tools: `shroud_obfuscate`, `shroud_deobfuscate`, `shroud_status`, `shroud_scan_tool`, `shroud_configure`, and `shroud_reset`.
144
+ That's it — the MCP server auto-starts a dedicated APP daemon on `/tmp/shroud-claude-mcp.sock` and writes Claude MCP session state under `OPENCLAW_STATE_DIR` (or `~/.openclaw`) as `shroud-claude-mcp-sessions.json`. Claude gains six tools: `shroud_obfuscate`, `shroud_deobfuscate`, `shroud_status`, `shroud_scan_tool`, `shroud_configure`, and `shroud_reset`.
145
+
146
+ If you want automatic tool-boundary obfuscation/deobfuscation instead of explicit MCP tools, use the shipped Claude hooks bridge in `clients/claude-code/shroud-bridge.mjs` together with `clients/claude-code/hooks.json`.
147
+
148
+ ### Codex
149
+
150
+ ```bash
151
+ npm install shroud-privacy
152
+ codex mcp add shroud -- node node_modules/shroud-privacy/clients/codex/shroud-mcp.mjs
153
+ ```
154
+
155
+ Codex uses the same six MCP tools as Claude, but on a separate APP daemon and socket: `/tmp/shroud-codex-mcp.sock`. Its APP session file defaults to `shroud-codex-mcp-sessions.json` under `OPENCLAW_STATE_DIR` or `~/.openclaw`.
156
+
157
+ The Codex MCP wrapper also auto-starts `clients/codex/shroud-bridge.mjs` on the host. That bridge watches Codex's local `~/.codex/history.jsonl` and writes `shroud-codex-cli-sessions.json` into the shared OpenClaw state dir, so Codex call/session counters stay current even when Codex is not actively invoking Shroud MCP tools. Set `SHROUD_CODEX_BRIDGE=0` only if you explicitly want to disable that behavior.
142
158
 
143
159
  ### Any agent (via APP)
144
160
 
@@ -165,6 +181,8 @@ with ShroudClient() as shroud:
165
181
 
166
182
  Copy `clients/python/shroud_client.py` into your project, or import it directly from the npm install path. Requires Node.js on the PATH.
167
183
 
184
+ For direct APP clients such as NCG, call `identify` first if you want per-agent session counters, then `obfuscate` / `deobfuscate`, and optionally wrap tool execution with `tool_call` / `tool_result` for telemetry.
185
+
168
186
  **Any language:**
169
187
 
170
188
  Spawn the APP server and talk JSON-RPC over stdin/stdout:
@@ -202,6 +220,15 @@ openclaw plugins install --path .
202
220
  openclaw gateway restart
203
221
  ```
204
222
 
223
+ For a local Docker-backed OpenClaw install, use the repo deploy script instead. It builds the checkout, runs the key regression tests, syncs the packaged plugin into `~/.openclaw/extensions/shroud-privacy`, clears the Node compile cache, and recreates `openclaw-primary-gateway` when `~/.openclaw/compose/docker-compose.primary.yml` is present:
224
+
225
+ ```bash
226
+ git clone https://github.com/wkeything/shroud.git
227
+ cd shroud
228
+ npm install
229
+ ./deploy-local.sh
230
+ ```
231
+
205
232
  ## Updating
206
233
 
207
234
  ```bash
package/app-server.mjs CHANGED
@@ -13,6 +13,8 @@
13
13
  * SHROUD_PLUGIN_CONFIG JSON config for the engine
14
14
  * SHROUD_STORE_FILE Persistent store file path
15
15
  * SHROUD_STATS_FILE Stats dump file path
16
+ * SHROUD_APP_EVENTS_FILE JSONL file for APP-side event export
17
+ * SHROUD_APP_SESSIONS_FILE JSON file for APP-side agent session state
16
18
  */
17
19
 
18
20
  import { createHash, randomBytes } from "node:crypto";
@@ -62,6 +64,8 @@ let obfuscator = new Obfuscator(config);
62
64
 
63
65
  const STATS_FILE = process.env.SHROUD_STATS_FILE || "/tmp/shroud-stats.json";
64
66
  const STORE_FILE = process.env.SHROUD_STORE_FILE || "";
67
+ const APP_EVENTS_FILE = process.env.SHROUD_APP_EVENTS_FILE || "/tmp/shroud-app-events.jsonl";
68
+ const APP_SESSIONS_FILE = process.env.SHROUD_APP_SESSIONS_FILE || "/tmp/shroud-app-sessions.json";
65
69
 
66
70
  // ---------------------------------------------------------------------------
67
71
  // Audit chain
@@ -117,6 +121,20 @@ function resolvePartition(params) {
117
121
  const startTime = Date.now();
118
122
  let requestCount = 0;
119
123
  let totalProcessingMs = 0;
124
+ const toolSequence = [];
125
+ const privacy = {
126
+ obfuscationCalls: 0,
127
+ deobfuscationCalls: 0,
128
+ entitiesObfuscated: 0,
129
+ replacementsDeobfuscated: 0,
130
+ categoryCounts: {},
131
+ };
132
+
133
+ let agentIdentified = false;
134
+ let agentLabel = null;
135
+ let agentBuildId = null;
136
+ let agentVersion = null;
137
+ let agentChannel = null;
120
138
 
121
139
  // ---------------------------------------------------------------------------
122
140
  // Stats dump helper
@@ -131,6 +149,36 @@ function dumpStats() {
131
149
  } catch { /* best-effort */ }
132
150
  }
133
151
 
152
+ function dumpSessionFile() {
153
+ if (!agentIdentified) return;
154
+ try {
155
+ const session = {
156
+ agentLabel,
157
+ agentBuildId,
158
+ agentVersion,
159
+ channel: agentChannel,
160
+ source: "app-server",
161
+ pid: process.pid,
162
+ requestCount,
163
+ uptimeMs: Date.now() - startTime,
164
+ securityEvents: 0,
165
+ storeSize: getObfuscator().getStats().storeMappings ?? 0,
166
+ classification: {
167
+ role: "APP Agent",
168
+ confidencePct: 100,
169
+ confidence: "high",
170
+ colour: "#06b6d4",
171
+ signals: ["app-server"],
172
+ },
173
+ toolSequence: toolSequence.slice(-20),
174
+ privacy,
175
+ eventsFile: APP_EVENTS_FILE,
176
+ updatedAt: new Date().toISOString(),
177
+ };
178
+ writeFileSync(APP_SESSIONS_FILE, JSON.stringify(session, null, 2) + "\n");
179
+ } catch { /* best-effort */ }
180
+ }
181
+
134
182
  // ---------------------------------------------------------------------------
135
183
  // JSON-RPC helpers
136
184
  // ---------------------------------------------------------------------------
@@ -154,6 +202,37 @@ function jsonResult(id, result) {
154
202
  // APP method handlers
155
203
  // ---------------------------------------------------------------------------
156
204
 
205
+ function handleIdentify(id, params) {
206
+ if (!params || typeof params.agent !== "string" || !params.agent.trim()) {
207
+ return jsonError(id, ERR_BAD_PARAMS, 'Missing required param: agent (string)');
208
+ }
209
+ if (typeof params.version !== "string" || !params.version.trim()) {
210
+ return jsonError(id, ERR_BAD_PARAMS, 'Missing required param: version (string)');
211
+ }
212
+
213
+ agentLabel = params.agent.trim();
214
+ agentVersion = params.version.trim();
215
+ agentChannel = typeof params.channel === "string" && params.channel.trim() ? params.channel.trim() : "app";
216
+ agentBuildId = createHash("sha256")
217
+ .update(agentLabel + ":" + agentVersion)
218
+ .digest("hex")
219
+ .slice(0, 16);
220
+ agentIdentified = true;
221
+
222
+ process.stderr.write(
223
+ `[app-server] Agent identified: ${agentLabel} v${agentVersion} (${agentChannel}) buildId=${agentBuildId}\n`
224
+ );
225
+
226
+ dumpSessionFile();
227
+
228
+ return jsonResult(id, {
229
+ ok: true,
230
+ agent: agentLabel,
231
+ buildId: agentBuildId,
232
+ security: false,
233
+ });
234
+ }
235
+
157
236
  function handleObfuscate(id, params) {
158
237
  if (!params || typeof params.text !== "string") {
159
238
  return jsonError(id, ERR_BAD_PARAMS, "Missing required param: text");
@@ -194,6 +273,12 @@ function handleObfuscate(id, params) {
194
273
  audit.fakesSample = Object.values(out.mappingsUsed || {}).slice(0, maxFakes);
195
274
  result.audit = audit;
196
275
 
276
+ privacy.obfuscationCalls++;
277
+ privacy.entitiesObfuscated += out.entities.length;
278
+ for (const [cat, count] of Object.entries(categories)) {
279
+ privacy.categoryCounts[cat] = (privacy.categoryCounts[cat] || 0) + count;
280
+ }
281
+
197
282
  dumpStats();
198
283
  return jsonResult(id, result);
199
284
  }
@@ -232,6 +317,11 @@ function handleDeobfuscate(id, params) {
232
317
  };
233
318
  result.audit = audit;
234
319
 
320
+ if ((deobResult.replacementCount || 0) > 0) {
321
+ privacy.deobfuscationCalls++;
322
+ privacy.replacementsDeobfuscated += deobResult.replacementCount || 0;
323
+ }
324
+
235
325
  dumpStats();
236
326
  return jsonResult(id, result);
237
327
  }
@@ -382,8 +472,53 @@ function handleConfigure(id, params) {
382
472
  return jsonResult(id, { ok: true, appliedKeys });
383
473
  }
384
474
 
475
+ function handleSecurity(id) {
476
+ return jsonResult(id, {
477
+ enabled: false,
478
+ mode: "off",
479
+ events: 0,
480
+ byThreatClass: {},
481
+ agent: agentIdentified ? {
482
+ label: agentLabel,
483
+ buildId: agentBuildId,
484
+ version: agentVersion,
485
+ channel: agentChannel,
486
+ requestCount,
487
+ } : null,
488
+ recentEvents: [],
489
+ });
490
+ }
491
+
492
+ function handleToolCall(id, params) {
493
+ if (!params || typeof params.tool !== "string" || !params.tool.trim()) {
494
+ return jsonError(id, ERR_BAD_PARAMS, "Missing required param: tool (string)");
495
+ }
496
+
497
+ toolSequence.push(params.tool.trim());
498
+
499
+ return jsonResult(id, {
500
+ blocked: false,
501
+ reason: null,
502
+ sequenceLength: toolSequence.length,
503
+ events: [],
504
+ securityEnabled: false,
505
+ });
506
+ }
507
+
508
+ function handleToolResult(id, params) {
509
+ if (!params || typeof params.tool !== "string" || !params.tool.trim()) {
510
+ return jsonError(id, ERR_BAD_PARAMS, "Missing required param: tool (string)");
511
+ }
512
+
513
+ return jsonResult(id, {
514
+ ok: true,
515
+ recorded: true,
516
+ });
517
+ }
518
+
385
519
  function handleShutdown(id) {
386
520
  dumpStats();
521
+ dumpSessionFile();
387
522
  const response = jsonResult(id, { ok: true, flushed: true });
388
523
  process.stdout.write(response + "\n");
389
524
  process.exit(0);
@@ -414,12 +549,16 @@ function handleSetPartition(id, params) {
414
549
  // ---------------------------------------------------------------------------
415
550
 
416
551
  const METHODS = {
552
+ identify: handleIdentify,
417
553
  obfuscate: handleObfuscate,
418
554
  deobfuscate: handleDeobfuscate,
419
555
  batch: handleBatch,
420
556
  reset: handleReset,
421
557
  stats: handleStats,
422
558
  health: handleHealth,
559
+ security: handleSecurity,
560
+ tool_call: handleToolCall,
561
+ tool_result: handleToolResult,
423
562
  configure: handleConfigure,
424
563
  shutdown: handleShutdown,
425
564
  setPartition: handleSetPartition,
@@ -462,6 +601,7 @@ function dispatch(line, writeFn) {
462
601
  // shutdown writes its own response and exits
463
602
  if (method !== "shutdown") {
464
603
  write(response + "\n");
604
+ dumpSessionFile();
465
605
  }
466
606
  } catch (e) {
467
607
  write(
@@ -509,15 +649,20 @@ const handshake = {
509
649
  engine: "shroud",
510
650
  version: engineVersion,
511
651
  capabilities: [
652
+ "identify",
512
653
  "obfuscate",
513
654
  "deobfuscate",
514
655
  "batch",
515
656
  "stats",
516
657
  "health",
517
658
  "configure",
659
+ "security",
660
+ "tool_call",
661
+ "tool_result",
518
662
  "audit",
519
663
  "partitions",
520
664
  ],
665
+ security: null,
521
666
  };
522
667
 
523
668
  process.stderr.write(`[app-server] Starting APP server v${engineVersion}\n`);
@@ -573,6 +718,7 @@ if (SOCKET_PATH) {
573
718
  socketServer.close();
574
719
  clearInterval(heartbeatInterval);
575
720
  dumpStats();
721
+ dumpSessionFile();
576
722
  process.exit(0);
577
723
  });
578
724
 
@@ -583,6 +729,7 @@ if (SOCKET_PATH) {
583
729
  socketServer.close();
584
730
  clearInterval(heartbeatInterval);
585
731
  dumpStats();
732
+ dumpSessionFile();
586
733
  process.exit(0);
587
734
  });
588
735
  }
@@ -598,6 +745,7 @@ if (SOCKET_PATH) {
598
745
  rl.on("close", () => {
599
746
  clearInterval(heartbeatInterval);
600
747
  dumpStats();
748
+ dumpSessionFile();
601
749
  process.exit(0);
602
750
  });
603
751
  }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://docs.anthropic.com/schemas/claude-code-hooks.json",
3
+ "_comment": "Add this 'hooks' block to your .claude/settings.json or .claude/settings.local.json",
4
+ "hooks": {
5
+ "PreToolUse": [
6
+ {
7
+ "matcher": "Read|Bash|Grep|Glob|Write|Edit|NotebookEdit|WebFetch|WebSearch",
8
+ "hooks": [
9
+ {
10
+ "type": "http",
11
+ "url": "http://127.0.0.1:17380/pre-tool-use",
12
+ "timeout": 5000,
13
+ "statusMessage": "Shroud: scanning tool call"
14
+ }
15
+ ]
16
+ }
17
+ ],
18
+ "PostToolUse": [
19
+ {
20
+ "matcher": "Read|Bash|Grep|Glob|WebFetch|WebSearch",
21
+ "hooks": [
22
+ {
23
+ "type": "http",
24
+ "url": "http://127.0.0.1:17380/post-tool-use",
25
+ "timeout": 5000,
26
+ "statusMessage": "Shroud: obfuscating output"
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ }
32
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Copy this into your project's .mcp.json or ~/.claude/.mcp.json",
3
+ "mcpServers": {
4
+ "shroud": {
5
+ "command": "node",
6
+ "args": ["node_modules/shroud-privacy/clients/claude-code/shroud-mcp.mjs"]
7
+ }
8
+ }
9
+ }