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 +29 -2
- package/app-server.mjs +148 -0
- package/clients/claude-code/hooks.json +32 -0
- package/clients/claude-code/mcp.json +9 -0
- package/clients/claude-code/shroud-bridge.mjs +406 -0
- package/clients/claude-code/shroud-mcp.mjs +548 -0
- package/clients/claude-code/socket-client.mjs +119 -0
- package/clients/codex/bridge-env.mjs +21 -0
- package/clients/codex/shroud-bridge.mjs +220 -0
- package/clients/codex/shroud-mcp.mjs +65 -0
- package/clients/shared/agent-config.mjs +35 -0
- package/dist/codex-telemetry.d.ts +16 -0
- package/dist/codex-telemetry.js +69 -0
- package/dist/hooks.js +5 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
<a href="#install">Install</a> ·
|
|
14
14
|
<a href="#why-shroud">Why Shroud</a> ·
|
|
15
15
|
<a href="#configure">Configure</a> ·
|
|
16
|
+
<a href="docs/integrations.md">Integrations</a> ·
|
|
16
17
|
<a href="#agent-privacy-protocol-app">APP Protocol</a> ·
|
|
17
18
|
<a href="CHANGELOG.md">Changelog</a>
|
|
18
19
|
</p>
|
|
19
20
|
|
|
20
21
|
> Apache 2.0 · Zero runtime dependencies · Anthropic + OpenAI + Google supported · Prompt-caching friendly · 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
|
|
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
|
|
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
|
+
}
|