shroud-privacy 2.0.6 → 2.0.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 +1 -1
- package/dist/hooks.d.ts +10 -2
- package/dist/hooks.js +85 -2
- package/dist/obfuscator.js +19 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -264,7 +264,7 @@ OpenClaw logs each plugin message twice (once under the plugin subsystem logger,
|
|
|
264
264
|
|
|
265
265
|
```bash
|
|
266
266
|
npm install
|
|
267
|
-
npm test # run vitest (
|
|
267
|
+
npm test # run vitest (215 tests)
|
|
268
268
|
npm run build # compile TypeScript
|
|
269
269
|
npm run lint # type-check without emitting
|
|
270
270
|
```
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenClaw lifecycle hooks for the Shroud privacy plugin.
|
|
3
3
|
*
|
|
4
|
-
* Registers 6 hooks (version-adaptive
|
|
4
|
+
* Registers 6 hooks + 1 transport interceptor (version-adaptive):
|
|
5
5
|
* 1. before_prompt_build (async) -- obfuscate user prompt via prependContext
|
|
6
6
|
* 2. before_message_write (SYNC) -- obfuscate every message written to the session transcript
|
|
7
|
-
* 3. before_llm_send (async) -- obfuscate LLM messages + install transformResponse
|
|
7
|
+
* 3. before_llm_send (async) -- obfuscate LLM messages + install transformResponse (>=2026.3.14)
|
|
8
8
|
* 4. before_tool_call (async) -- deobfuscate tool params (+ depth tracking)
|
|
9
9
|
* 5. tool_result_persist (SYNC) -- obfuscate tool result message
|
|
10
10
|
* 6. message_sending (async) -- deobfuscate outbound message content
|
|
11
|
+
* 7. Transport interceptor -- wraps Slack WebClient.apiCall for universal deobfuscation
|
|
12
|
+
*
|
|
13
|
+
* The transport interceptor is the universal fallback: it deobfuscates Slack
|
|
14
|
+
* messages at the API-call level, independent of which OpenClaw hooks fire.
|
|
15
|
+
* On >=2026.3.14, transformResponse handles deobfuscation first (for ALL
|
|
16
|
+
* channels); the transport interceptor is a no-op since the text is already
|
|
17
|
+
* deobfuscated. On older versions where message_sending doesn't fire for
|
|
18
|
+
* Slack, the interceptor catches it.
|
|
11
19
|
*/
|
|
12
20
|
import { Obfuscator } from "./obfuscator.js";
|
|
13
21
|
export interface PluginApi {
|
package/dist/hooks.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenClaw lifecycle hooks for the Shroud privacy plugin.
|
|
3
3
|
*
|
|
4
|
-
* Registers 6 hooks (version-adaptive
|
|
4
|
+
* Registers 6 hooks + 1 transport interceptor (version-adaptive):
|
|
5
5
|
* 1. before_prompt_build (async) -- obfuscate user prompt via prependContext
|
|
6
6
|
* 2. before_message_write (SYNC) -- obfuscate every message written to the session transcript
|
|
7
|
-
* 3. before_llm_send (async) -- obfuscate LLM messages + install transformResponse
|
|
7
|
+
* 3. before_llm_send (async) -- obfuscate LLM messages + install transformResponse (>=2026.3.14)
|
|
8
8
|
* 4. before_tool_call (async) -- deobfuscate tool params (+ depth tracking)
|
|
9
9
|
* 5. tool_result_persist (SYNC) -- obfuscate tool result message
|
|
10
10
|
* 6. message_sending (async) -- deobfuscate outbound message content
|
|
11
|
+
* 7. Transport interceptor -- wraps Slack WebClient.apiCall for universal deobfuscation
|
|
12
|
+
*
|
|
13
|
+
* The transport interceptor is the universal fallback: it deobfuscates Slack
|
|
14
|
+
* messages at the API-call level, independent of which OpenClaw hooks fire.
|
|
15
|
+
* On >=2026.3.14, transformResponse handles deobfuscation first (for ALL
|
|
16
|
+
* channels); the transport interceptor is a no-op since the text is already
|
|
17
|
+
* deobfuscated. On older versions where message_sending doesn't fire for
|
|
18
|
+
* Slack, the interceptor catches it.
|
|
11
19
|
*/
|
|
12
20
|
import { createHash, randomBytes } from "node:crypto";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
13
22
|
import { writeFileSync } from "node:fs";
|
|
14
23
|
import { BUILTIN_PATTERNS } from "./detectors/regex.js";
|
|
15
24
|
const STATS_FILE = process.env.SHROUD_STATS_FILE || "/tmp/shroud-stats.json";
|
|
@@ -153,6 +162,74 @@ function walkStrings(value, fn) {
|
|
|
153
162
|
return value;
|
|
154
163
|
}
|
|
155
164
|
// ---------------------------------------------------------------------------
|
|
165
|
+
// Transport-level deobfuscation interceptor (fallback for all versions)
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
/**
|
|
168
|
+
* Wraps the Slack WebClient.prototype.apiCall to deobfuscate outbound message
|
|
169
|
+
* text before it hits the Slack API. This is the universal fallback that works
|
|
170
|
+
* on ANY OpenClaw version — it doesn't depend on hook dispatch at all.
|
|
171
|
+
*
|
|
172
|
+
* The @slack/web-api package is CJS and already loaded by OpenClaw, so it lives
|
|
173
|
+
* in require.cache. We find it there, wrap the prototype once, done.
|
|
174
|
+
*/
|
|
175
|
+
function installTransportInterceptor(obfuscator, logger) {
|
|
176
|
+
try {
|
|
177
|
+
const esmRequire = createRequire(import.meta.url);
|
|
178
|
+
const cache = esmRequire.cache;
|
|
179
|
+
if (!cache)
|
|
180
|
+
return;
|
|
181
|
+
for (const key of Object.keys(cache)) {
|
|
182
|
+
if (!key.includes("@slack/web-api"))
|
|
183
|
+
continue;
|
|
184
|
+
const mod = cache[key];
|
|
185
|
+
const WebClient = mod?.exports?.WebClient;
|
|
186
|
+
if (typeof WebClient !== "function")
|
|
187
|
+
continue;
|
|
188
|
+
const proto = WebClient.prototype;
|
|
189
|
+
if (typeof proto.apiCall !== "function")
|
|
190
|
+
continue;
|
|
191
|
+
if (proto.__shroudPatched)
|
|
192
|
+
return; // already wrapped
|
|
193
|
+
const origApiCall = proto.apiCall;
|
|
194
|
+
proto.apiCall = async function shroudApiCall(method, options) {
|
|
195
|
+
if ((method === "chat.postMessage" || method === "chat.update") &&
|
|
196
|
+
options) {
|
|
197
|
+
// Deobfuscate the plain-text fallback
|
|
198
|
+
if (typeof options.text === "string") {
|
|
199
|
+
const original = options.text;
|
|
200
|
+
const deobfuscated = obfuscator.deobfuscate(original);
|
|
201
|
+
if (deobfuscated !== original) {
|
|
202
|
+
options = { ...options, text: deobfuscated };
|
|
203
|
+
logger?.info(`[shroud][transport] deobfuscated Slack ${method}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Deobfuscate blocks (rich text) — walk all text elements
|
|
207
|
+
if (Array.isArray(options.blocks)) {
|
|
208
|
+
const json = JSON.stringify(options.blocks);
|
|
209
|
+
const deobJson = obfuscator.deobfuscate(json);
|
|
210
|
+
if (deobJson !== json) {
|
|
211
|
+
try {
|
|
212
|
+
options = { ...options, blocks: JSON.parse(deobJson) };
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// If JSON parse fails, leave blocks unchanged
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return origApiCall.call(this, method, options);
|
|
221
|
+
};
|
|
222
|
+
proto.__shroudPatched = true;
|
|
223
|
+
logger?.info("[shroud] Installed Slack transport interceptor (universal deobfuscation fallback)");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
logger?.info("[shroud] Slack WebClient not found in require.cache — transport interceptor not installed (hooks-only mode)");
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
logger?.warn(`[shroud] Failed to install transport interceptor: ${String(err)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
156
233
|
// Hook registration
|
|
157
234
|
// ---------------------------------------------------------------------------
|
|
158
235
|
export function registerHooks(api, obfuscator) {
|
|
@@ -397,4 +474,10 @@ export function registerHooks(api, obfuscator) {
|
|
|
397
474
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
398
475
|
},
|
|
399
476
|
});
|
|
477
|
+
// -----------------------------------------------------------------------
|
|
478
|
+
// 7. Transport interceptor: universal deobfuscation fallback
|
|
479
|
+
// Wraps Slack WebClient.apiCall so outbound messages are deobfuscated
|
|
480
|
+
// regardless of which OpenClaw hooks fire (or don't).
|
|
481
|
+
// -----------------------------------------------------------------------
|
|
482
|
+
installTransportInterceptor(obfuscator, api.logger);
|
|
400
483
|
}
|
package/dist/obfuscator.js
CHANGED
|
@@ -78,6 +78,21 @@ function compressIPv6(addr) {
|
|
|
78
78
|
}
|
|
79
79
|
return groups.join(":");
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Strip Slack/chat mrkdwn link formatting to recover plain text.
|
|
83
|
+
* Slack wraps emails as `<mailto:X|DISPLAY>` and URLs as `<URL|DISPLAY>`,
|
|
84
|
+
* which splits entities across tag boundaries and breaks regex detection.
|
|
85
|
+
* This converts display-text links back to their visible form.
|
|
86
|
+
*/
|
|
87
|
+
function stripSlackLinks(text) {
|
|
88
|
+
// <mailto:X|DISPLAY> → DISPLAY (email links)
|
|
89
|
+
text = text.replace(/<mailto:[^|>]+\|([^>]*)>/g, "$1");
|
|
90
|
+
// <URL|DISPLAY> → DISPLAY (URL links with display text)
|
|
91
|
+
text = text.replace(/<https?:\/\/[^|>]+\|([^>]*)>/g, "$1");
|
|
92
|
+
// <URL> → URL (bare URL links, no display text)
|
|
93
|
+
text = text.replace(/<(https?:\/\/[^>]+)>/g, "$1");
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
81
96
|
/**
|
|
82
97
|
* Build a single combined regex from an array of literal strings.
|
|
83
98
|
* Strings are escaped and joined with alternation (|), sorted longest-first
|
|
@@ -197,6 +212,10 @@ export class Obfuscator {
|
|
|
197
212
|
*/
|
|
198
213
|
obfuscate(text, context) {
|
|
199
214
|
const startTime = Date.now();
|
|
215
|
+
// 0. Strip Slack/chat mrkdwn link formatting so detection sees clean text.
|
|
216
|
+
// Slack wraps emails as <mailto:X|DISPLAY> and URLs as <URL|DISPLAY>,
|
|
217
|
+
// which splits entities across tag boundaries and breaks regex matching.
|
|
218
|
+
text = stripSlackLinks(text);
|
|
200
219
|
// 1. Learn subnet context from CIDR notation and masks in text
|
|
201
220
|
this._subnetMapper.learnSubnetsFromText(text);
|
|
202
221
|
// 2. Detect all entities from all detectors
|
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.7",
|
|
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