shroud-privacy 2.0.3 → 2.0.5
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 +8 -11
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +134 -243
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,11 +16,10 @@ Privacy obfuscation plugin for [OpenClaw](https://openclaw.ai). Detects sensitiv
|
|
|
16
16
|
| Hook | Direction | What happens |
|
|
17
17
|
|------|-----------|-------------|
|
|
18
18
|
| `before_prompt_build` | User → LLM | Obfuscate user prompt, prepend privacy context |
|
|
19
|
-
| `
|
|
20
|
-
| `transformResponse` | LLM → User | Deobfuscate LLM output (auto-reply, WhatsApp, etc.) |
|
|
19
|
+
| `before_message_write` | Any → History | Obfuscate every message written to the session transcript |
|
|
21
20
|
| `before_tool_call` | LLM → Tool | Deobfuscate tool parameters + track tool chain depth |
|
|
22
21
|
| `tool_result_persist` | Tool → History | Obfuscate tool results before storing |
|
|
23
|
-
| `message_sending` | Agent → User | Deobfuscate outbound messages (
|
|
22
|
+
| `message_sending` | Agent → User | Deobfuscate outbound messages (WhatsApp, auto-reply, etc.) |
|
|
24
23
|
|
|
25
24
|
## Install
|
|
26
25
|
|
|
@@ -232,13 +231,13 @@ tail -f ~/.openclaw/logs/openclaw.log \
|
|
|
232
231
|
You should see:
|
|
233
232
|
|
|
234
233
|
```
|
|
235
|
-
[shroud][audit] OBFUSCATE req=dc5f9199cfb0d835 | entities=4 |
|
|
234
|
+
[shroud][audit] OBFUSCATE req=dc5f9199cfb0d835 | entities=4 | chars=1200->1218 (delta=+18) | modified=YES | byCat=email:1,ip_address:2,hostname:1 | byRule=regex:email:1,regex:ipv4:2,regex:hostname:1
|
|
236
235
|
```
|
|
237
236
|
|
|
238
237
|
With proof hashes enabled:
|
|
239
238
|
|
|
240
239
|
```
|
|
241
|
-
[shroud][audit] OBFUSCATE req=a3f1bc9e02d4e7f1 | entities=4 |
|
|
240
|
+
[shroud][audit] OBFUSCATE req=a3f1bc9e02d4e7f1 | entities=4 | chars=1200->1218 (delta=+18) | modified=YES | byCat=email:1,ip_address:2,hostname:1 | byRule=regex:email:1,regex:ipv4:2,regex:hostname:1 | proof_in=8a3c1f0e2b4d proof_out=f7d2a1c9e084 | fakes=[jsmith@corp.net|100.64.0.12|SW-LAB-01]
|
|
242
241
|
```
|
|
243
242
|
|
|
244
243
|
### Audit field reference
|
|
@@ -247,16 +246,14 @@ With proof hashes enabled:
|
|
|
247
246
|
|-------|---------|
|
|
248
247
|
| `req` | Random request ID (hex) — correlates obfuscate ↔ deobfuscate |
|
|
249
248
|
| `entities` | Total entities detected and replaced |
|
|
250
|
-
| `touched` | Messages with replacements / total messages |
|
|
251
|
-
| `blocks` | Content blocks with replacements |
|
|
252
249
|
| `chars` | Input → output character count |
|
|
253
250
|
| `delta` | Character count change (fakes may be longer/shorter) |
|
|
254
251
|
| `modified` | `YES` if text was changed, `NO` if pass-through |
|
|
255
252
|
| `byCat` | Entity counts by category |
|
|
256
253
|
| `byRule` | Entity counts by detector rule |
|
|
257
|
-
| `proof_in` | Truncated salted SHA-256 of input text |
|
|
258
|
-
| `proof_out` | Truncated salted SHA-256 of output text |
|
|
259
|
-
| `fakes` | Sample of fake replacement values (never real values) |
|
|
254
|
+
| `proof_in` | Truncated salted SHA-256 of input text (opt-in) |
|
|
255
|
+
| `proof_out` | Truncated salted SHA-256 of output text (opt-in) |
|
|
256
|
+
| `fakes` | Sample of fake replacement values (opt-in, never real values) |
|
|
260
257
|
|
|
261
258
|
### Note on log duplication
|
|
262
259
|
|
|
@@ -266,7 +263,7 @@ OpenClaw logs each plugin message twice (once under the plugin subsystem logger,
|
|
|
266
263
|
|
|
267
264
|
```bash
|
|
268
265
|
npm install
|
|
269
|
-
npm test # run vitest (
|
|
266
|
+
npm test # run vitest (203 tests)
|
|
270
267
|
npm run build # compile TypeScript
|
|
271
268
|
npm run lint # type-check without emitting
|
|
272
269
|
```
|
package/dist/hooks.d.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* OpenClaw lifecycle hooks for the Shroud privacy plugin.
|
|
3
3
|
*
|
|
4
4
|
* Registers 5 hooks:
|
|
5
|
-
* 1. before_prompt_build
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. before_tool_call
|
|
8
|
-
* 4. tool_result_persist
|
|
9
|
-
* 5. message_sending
|
|
5
|
+
* 1. before_prompt_build (async) -- obfuscate user prompt via prependContext
|
|
6
|
+
* 2. before_message_write (SYNC) -- obfuscate every message written to the session transcript
|
|
7
|
+
* 3. before_tool_call (async) -- deobfuscate tool params (+ depth tracking)
|
|
8
|
+
* 4. tool_result_persist (SYNC) -- obfuscate tool result message
|
|
9
|
+
* 5. message_sending (async) -- deobfuscate outbound message content
|
|
10
10
|
*/
|
|
11
11
|
import { Obfuscator } from "./obfuscator.js";
|
|
12
12
|
export interface PluginApi {
|
package/dist/hooks.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* OpenClaw lifecycle hooks for the Shroud privacy plugin.
|
|
3
3
|
*
|
|
4
4
|
* Registers 5 hooks:
|
|
5
|
-
* 1. before_prompt_build
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. before_tool_call
|
|
8
|
-
* 4. tool_result_persist
|
|
9
|
-
* 5. message_sending
|
|
5
|
+
* 1. before_prompt_build (async) -- obfuscate user prompt via prependContext
|
|
6
|
+
* 2. before_message_write (SYNC) -- obfuscate every message written to the session transcript
|
|
7
|
+
* 3. before_tool_call (async) -- deobfuscate tool params (+ depth tracking)
|
|
8
|
+
* 4. tool_result_persist (SYNC) -- obfuscate tool result message
|
|
9
|
+
* 5. message_sending (async) -- deobfuscate outbound message content
|
|
10
10
|
*/
|
|
11
11
|
import { createHash, randomBytes } from "node:crypto";
|
|
12
12
|
import { writeFileSync } from "node:fs";
|
|
@@ -37,205 +37,77 @@ function truncateHash(hash, n) {
|
|
|
37
37
|
return hash.slice(0, n);
|
|
38
38
|
}
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
|
-
//
|
|
40
|
+
// Audit log emitter — per-message (used by before_message_write)
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (typeof value === "string")
|
|
49
|
-
return fn(value);
|
|
50
|
-
if (Array.isArray(value)) {
|
|
51
|
-
return value.map((item) => {
|
|
52
|
-
if (typeof item === "object" && item !== null && "text" in item && typeof item.text === "string") {
|
|
53
|
-
return { ...item, text: fn(item.text) };
|
|
54
|
-
}
|
|
55
|
-
return walkStrings(item, fn);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
if (typeof value === "object" && value !== null) {
|
|
59
|
-
const out = {};
|
|
60
|
-
for (const [k, v] of Object.entries(value)) {
|
|
61
|
-
if (typeof v === "string") {
|
|
62
|
-
out[k] = fn(v);
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
out[k] = v; // don't recurse arbitrarily into unknown objects
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return out;
|
|
42
|
+
function emitObfuscationAudit(logger, config, requestId, result, inputText, outputText) {
|
|
43
|
+
const byCat = {};
|
|
44
|
+
const byRule = {};
|
|
45
|
+
for (const e of result.entities) {
|
|
46
|
+
byCat[e.category] = (byCat[e.category] || 0) + 1;
|
|
47
|
+
byRule[e.detector] = (byRule[e.detector] || 0) + 1;
|
|
69
48
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (!msg || typeof msg !== "object")
|
|
78
|
-
return msg;
|
|
79
|
-
if (typeof msg.content === "string") {
|
|
80
|
-
const result = obfuscator.obfuscate(msg.content);
|
|
81
|
-
if (result.entities.length === 0)
|
|
82
|
-
return msg;
|
|
83
|
-
return { ...msg, content: result.obfuscated };
|
|
84
|
-
}
|
|
85
|
-
if (Array.isArray(msg.content)) {
|
|
86
|
-
const newContent = msg.content.map((block) => {
|
|
87
|
-
if (block && typeof block === "object" && typeof block.text === "string") {
|
|
88
|
-
const result = obfuscator.obfuscate(block.text);
|
|
89
|
-
if (result.entities.length === 0)
|
|
90
|
-
return block;
|
|
91
|
-
return { ...block, text: result.obfuscated };
|
|
92
|
-
}
|
|
93
|
-
return block;
|
|
94
|
-
});
|
|
95
|
-
return { ...msg, content: newContent };
|
|
96
|
-
}
|
|
97
|
-
return msg;
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
// obfuscateMessagesWithStats (audit-aware)
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
function obfuscateMessagesWithStats(messages, obfuscator, config) {
|
|
104
|
-
const stats = {
|
|
105
|
-
totalEntities: 0,
|
|
106
|
-
byCategory: {},
|
|
107
|
-
byRule: {},
|
|
108
|
-
messagesTouched: 0,
|
|
109
|
-
blocksTouched: 0,
|
|
110
|
-
inputChars: 0,
|
|
111
|
-
outputChars: 0,
|
|
112
|
-
fakesSample: [],
|
|
113
|
-
};
|
|
114
|
-
const maxFakes = config.auditMaxFakesSample;
|
|
115
|
-
const obfuscatedMessages = messages.map((msg) => {
|
|
116
|
-
if (!msg || typeof msg !== "object")
|
|
117
|
-
return msg;
|
|
118
|
-
if (typeof msg.content === "string") {
|
|
119
|
-
const result = obfuscator.obfuscate(msg.content);
|
|
120
|
-
stats.inputChars += msg.content.length;
|
|
121
|
-
stats.outputChars += result.obfuscated.length;
|
|
122
|
-
if (result.entities.length > 0) {
|
|
123
|
-
stats.messagesTouched++;
|
|
124
|
-
stats.blocksTouched++;
|
|
125
|
-
accumulateStats(stats, result, maxFakes);
|
|
126
|
-
}
|
|
127
|
-
if (result.entities.length === 0)
|
|
128
|
-
return msg;
|
|
129
|
-
return { ...msg, content: result.obfuscated };
|
|
130
|
-
}
|
|
131
|
-
if (Array.isArray(msg.content)) {
|
|
132
|
-
let msgTouched = false;
|
|
133
|
-
const newContent = msg.content.map((block) => {
|
|
134
|
-
if (block && typeof block === "object" && typeof block.text === "string") {
|
|
135
|
-
const result = obfuscator.obfuscate(block.text);
|
|
136
|
-
stats.inputChars += block.text.length;
|
|
137
|
-
stats.outputChars += result.obfuscated.length;
|
|
138
|
-
if (result.entities.length > 0) {
|
|
139
|
-
msgTouched = true;
|
|
140
|
-
stats.blocksTouched++;
|
|
141
|
-
accumulateStats(stats, result, maxFakes);
|
|
142
|
-
}
|
|
143
|
-
if (result.entities.length === 0)
|
|
144
|
-
return block;
|
|
145
|
-
return { ...block, text: result.obfuscated };
|
|
146
|
-
}
|
|
147
|
-
return block;
|
|
148
|
-
});
|
|
149
|
-
if (msgTouched)
|
|
150
|
-
stats.messagesTouched++;
|
|
151
|
-
return { ...msg, content: newContent };
|
|
152
|
-
}
|
|
153
|
-
return msg;
|
|
154
|
-
});
|
|
155
|
-
return { messages: obfuscatedMessages, stats };
|
|
156
|
-
}
|
|
157
|
-
function accumulateStats(stats, result, maxFakes) {
|
|
158
|
-
for (const entity of result.entities) {
|
|
159
|
-
stats.totalEntities++;
|
|
160
|
-
stats.byCategory[entity.category] = (stats.byCategory[entity.category] || 0) + 1;
|
|
161
|
-
stats.byRule[entity.detector] = (stats.byRule[entity.detector] || 0) + 1;
|
|
49
|
+
const modified = result.entities.length > 0;
|
|
50
|
+
const charDelta = outputText.length - inputText.length;
|
|
51
|
+
let proofIn = "";
|
|
52
|
+
let proofOut = "";
|
|
53
|
+
if (config.auditIncludeProofHashes) {
|
|
54
|
+
proofIn = truncateHash(safeHash(inputText, config.auditHashSalt), config.auditHashTruncate);
|
|
55
|
+
proofOut = truncateHash(safeHash(outputText, config.auditHashSalt), config.auditHashTruncate);
|
|
162
56
|
}
|
|
163
|
-
|
|
164
|
-
if (
|
|
57
|
+
const fakesSample = [];
|
|
58
|
+
if (config.auditMaxFakesSample > 0) {
|
|
165
59
|
for (const fake of Object.values(result.mappingsUsed)) {
|
|
166
|
-
if (
|
|
60
|
+
if (fakesSample.length >= config.auditMaxFakesSample)
|
|
167
61
|
break;
|
|
168
|
-
|
|
62
|
+
fakesSample.push(fake);
|
|
169
63
|
}
|
|
170
64
|
}
|
|
171
|
-
}
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Audit log emitters
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
function emitAuditLog(logger, config, requestId, stats, totalMessages, concatenatedOriginal, concatenatedObfuscated) {
|
|
176
|
-
const byCatStr = Object.entries(stats.byCategory)
|
|
177
|
-
.map(([k, v]) => `${k}:${v}`)
|
|
178
|
-
.join(",");
|
|
179
|
-
const byRuleStr = Object.entries(stats.byRule)
|
|
180
|
-
.map(([k, v]) => `${k}:${v}`)
|
|
181
|
-
.join(",");
|
|
182
|
-
const modified = stats.inputChars !== stats.outputChars || stats.totalEntities > 0;
|
|
183
|
-
const charDelta = stats.outputChars - stats.inputChars;
|
|
184
|
-
// Always compute proof hashes when proofs enabled
|
|
185
|
-
let proofHashIn = "";
|
|
186
|
-
let proofHashOut = "";
|
|
187
|
-
if (config.auditIncludeProofHashes) {
|
|
188
|
-
proofHashIn = truncateHash(safeHash(concatenatedOriginal, config.auditHashSalt), config.auditHashTruncate);
|
|
189
|
-
proofHashOut = truncateHash(safeHash(concatenatedObfuscated, config.auditHashSalt), config.auditHashTruncate);
|
|
190
|
-
}
|
|
191
65
|
if (config.auditLogFormat === "json") {
|
|
192
66
|
const obj = {
|
|
193
|
-
event: "shroud.audit.
|
|
67
|
+
event: "shroud.audit.obfuscate",
|
|
194
68
|
req: requestId,
|
|
195
69
|
ts: new Date().toISOString(),
|
|
196
70
|
modified,
|
|
197
|
-
totalEntities:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
inputChars: stats.inputChars,
|
|
201
|
-
outputChars: stats.outputChars,
|
|
71
|
+
totalEntities: result.entities.length,
|
|
72
|
+
inputChars: inputText.length,
|
|
73
|
+
outputChars: outputText.length,
|
|
202
74
|
charDelta,
|
|
203
|
-
byCategory:
|
|
204
|
-
byRule
|
|
75
|
+
byCategory: byCat,
|
|
76
|
+
byRule,
|
|
205
77
|
};
|
|
206
78
|
if (config.auditIncludeProofHashes) {
|
|
207
|
-
obj.proofIn =
|
|
208
|
-
obj.proofOut =
|
|
79
|
+
obj.proofIn = proofIn;
|
|
80
|
+
obj.proofOut = proofOut;
|
|
209
81
|
}
|
|
210
|
-
if (
|
|
211
|
-
obj.fakesSample =
|
|
82
|
+
if (fakesSample.length > 0) {
|
|
83
|
+
obj.fakesSample = fakesSample;
|
|
212
84
|
}
|
|
213
85
|
logger?.info(JSON.stringify(obj));
|
|
214
86
|
}
|
|
215
87
|
else {
|
|
88
|
+
const byCatStr = Object.entries(byCat).map(([k, v]) => `${k}:${v}`).join(",");
|
|
89
|
+
const byRuleStr = Object.entries(byRule).map(([k, v]) => `${k}:${v}`).join(",");
|
|
216
90
|
const parts = [
|
|
217
91
|
`[shroud][audit] OBFUSCATE req=${requestId}`,
|
|
218
|
-
`entities=${
|
|
219
|
-
`
|
|
220
|
-
`blocks=${stats.blocksTouched}`,
|
|
221
|
-
`chars=${stats.inputChars}->${stats.outputChars} (delta=${charDelta >= 0 ? "+" : ""}${charDelta})`,
|
|
92
|
+
`entities=${result.entities.length}`,
|
|
93
|
+
`chars=${inputText.length}->${outputText.length} (delta=${charDelta >= 0 ? "+" : ""}${charDelta})`,
|
|
222
94
|
`modified=${modified ? "YES" : "NO"}`,
|
|
223
95
|
`byCat=${byCatStr || "none"}`,
|
|
224
96
|
`byRule=${byRuleStr || "none"}`,
|
|
225
97
|
];
|
|
226
98
|
if (config.auditIncludeProofHashes) {
|
|
227
|
-
parts.push(`proof_in=${
|
|
99
|
+
parts.push(`proof_in=${proofIn} proof_out=${proofOut}`);
|
|
228
100
|
}
|
|
229
|
-
if (
|
|
230
|
-
parts.push(`fakes=[${
|
|
101
|
+
if (fakesSample.length > 0) {
|
|
102
|
+
parts.push(`fakes=[${fakesSample.join("|")}]`);
|
|
231
103
|
}
|
|
232
104
|
logger?.info(parts.join(" | "));
|
|
233
105
|
}
|
|
234
106
|
}
|
|
235
|
-
function
|
|
107
|
+
function emitDeobfuscationAudit(logger, config, requestId, replacementCount) {
|
|
236
108
|
if (config.auditLogFormat === "json") {
|
|
237
109
|
logger?.info(JSON.stringify({
|
|
238
|
-
event: "shroud.audit.
|
|
110
|
+
event: "shroud.audit.deobfuscate",
|
|
239
111
|
req: requestId,
|
|
240
112
|
ts: new Date().toISOString(),
|
|
241
113
|
modified: replacementCount > 0,
|
|
@@ -247,6 +119,39 @@ function emitDeobfuscationAuditLog(logger, config, requestId, replacementCount)
|
|
|
247
119
|
}
|
|
248
120
|
}
|
|
249
121
|
// ---------------------------------------------------------------------------
|
|
122
|
+
// String walking helper
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
/**
|
|
125
|
+
* Deep-walk an unknown message structure and obfuscate/deobfuscate all
|
|
126
|
+
* string leaves. OpenClaw message payloads can be a plain string, an
|
|
127
|
+
* array of content blocks, or a nested object — this handles all three.
|
|
128
|
+
*/
|
|
129
|
+
function walkStrings(value, fn) {
|
|
130
|
+
if (typeof value === "string")
|
|
131
|
+
return fn(value);
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.map((item) => {
|
|
134
|
+
if (typeof item === "object" && item !== null && "text" in item && typeof item.text === "string") {
|
|
135
|
+
return { ...item, text: fn(item.text) };
|
|
136
|
+
}
|
|
137
|
+
return walkStrings(item, fn);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === "object" && value !== null) {
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const [k, v] of Object.entries(value)) {
|
|
143
|
+
if (typeof v === "string") {
|
|
144
|
+
out[k] = fn(v);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
out[k] = v; // don't recurse arbitrarily into unknown objects
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
250
155
|
// Hook registration
|
|
251
156
|
// ---------------------------------------------------------------------------
|
|
252
157
|
export function registerHooks(api, obfuscator) {
|
|
@@ -256,6 +161,11 @@ export function registerHooks(api, obfuscator) {
|
|
|
256
161
|
// 1. before_prompt_build (async): obfuscate user prompt
|
|
257
162
|
// -----------------------------------------------------------------------
|
|
258
163
|
api.on("before_prompt_build", async (event) => {
|
|
164
|
+
// Reset tool depth at the start of each turn — tool calls from the
|
|
165
|
+
// previous turn are complete, so the counter should not carry over.
|
|
166
|
+
if (obfuscator.toolDepth > 0) {
|
|
167
|
+
obfuscator.resetToolDepth();
|
|
168
|
+
}
|
|
259
169
|
const prompt = event?.prompt;
|
|
260
170
|
if (typeof prompt !== "string" || !prompt)
|
|
261
171
|
return;
|
|
@@ -276,87 +186,57 @@ export function registerHooks(api, obfuscator) {
|
|
|
276
186
|
};
|
|
277
187
|
});
|
|
278
188
|
// -----------------------------------------------------------------------
|
|
279
|
-
// 2.
|
|
189
|
+
// 2. before_message_write (SYNC): obfuscate every message written to session
|
|
190
|
+
// This ensures the LLM always sees obfuscated history, even on platforms
|
|
191
|
+
// that don't support before_llm_send.
|
|
280
192
|
// -----------------------------------------------------------------------
|
|
281
|
-
api.on("
|
|
282
|
-
if (!
|
|
193
|
+
api.on("before_message_write", (event) => {
|
|
194
|
+
if (!event?.message || typeof event.message !== "object")
|
|
283
195
|
return;
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
if (
|
|
287
|
-
obfuscator.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const { messages: msgs, stats } = obfuscateMessagesWithStats(event.messages, obfuscator, config);
|
|
295
|
-
obfuscatedMessages = msgs;
|
|
296
|
-
// Build concatenated texts for proof hashes
|
|
297
|
-
let concatenatedOriginal = "";
|
|
298
|
-
let concatenatedObfuscated = "";
|
|
299
|
-
if (config.auditIncludeProofHashes) {
|
|
300
|
-
for (let i = 0; i < event.messages.length; i++) {
|
|
301
|
-
const orig = event.messages[i];
|
|
302
|
-
const obf = obfuscatedMessages[i];
|
|
303
|
-
if (typeof orig?.content === "string") {
|
|
304
|
-
concatenatedOriginal += orig.content;
|
|
305
|
-
concatenatedObfuscated += (obf?.content ?? "");
|
|
306
|
-
}
|
|
307
|
-
else if (Array.isArray(orig?.content)) {
|
|
308
|
-
for (let j = 0; j < orig.content.length; j++) {
|
|
309
|
-
const origBlock = orig.content[j];
|
|
310
|
-
const obfBlock = obf?.content?.[j];
|
|
311
|
-
if (typeof origBlock?.text === "string") {
|
|
312
|
-
concatenatedOriginal += origBlock.text;
|
|
313
|
-
concatenatedObfuscated += (obfBlock?.text ?? "");
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
196
|
+
const msg = event.message;
|
|
197
|
+
// Obfuscate string content
|
|
198
|
+
if (typeof msg.content === "string") {
|
|
199
|
+
const result = obfuscator.obfuscate(msg.content);
|
|
200
|
+
if (result.entities.length === 0)
|
|
201
|
+
return;
|
|
202
|
+
dumpStatsFile(obfuscator);
|
|
203
|
+
if (auditActive) {
|
|
204
|
+
try {
|
|
205
|
+
emitObfuscationAudit(api.logger, config, randomBytes(8).toString("hex"), result, msg.content, result.obfuscated);
|
|
317
206
|
}
|
|
207
|
+
catch { /* best-effort */ }
|
|
318
208
|
}
|
|
319
|
-
|
|
320
|
-
emitAuditLog(api.logger, config, requestId, stats, totalMessages, concatenatedOriginal, concatenatedObfuscated);
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
323
|
-
// Logging is best-effort
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
obfuscatedMessages = obfuscateMessages(event.messages, obfuscator);
|
|
209
|
+
return { message: { ...msg, content: result.obfuscated } };
|
|
328
210
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
emitDeobfuscationAuditLog(api.logger, config, capturedReqId, replacementCount);
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// best-effort
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
dumpStatsFile(obfuscator);
|
|
347
|
-
return deobfuscated;
|
|
211
|
+
// Obfuscate array-of-blocks content
|
|
212
|
+
if (Array.isArray(msg.content)) {
|
|
213
|
+
let changed = false;
|
|
214
|
+
const allResults = [];
|
|
215
|
+
const newContent = msg.content.map((block) => {
|
|
216
|
+
if (block && typeof block === "object" && typeof block.text === "string") {
|
|
217
|
+
const result = obfuscator.obfuscate(block.text);
|
|
218
|
+
if (result.entities.length > 0) {
|
|
219
|
+
changed = true;
|
|
220
|
+
allResults.push(result);
|
|
221
|
+
return { ...block, text: result.obfuscated };
|
|
348
222
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
223
|
+
}
|
|
224
|
+
return block;
|
|
225
|
+
});
|
|
226
|
+
if (!changed)
|
|
227
|
+
return;
|
|
228
|
+
dumpStatsFile(obfuscator);
|
|
229
|
+
if (auditActive) {
|
|
230
|
+
for (const result of allResults) {
|
|
231
|
+
try {
|
|
232
|
+
const origText = Object.keys(result.mappingsUsed).join(" ");
|
|
233
|
+
emitObfuscationAudit(api.logger, config, randomBytes(8).toString("hex"), result, origText, result.obfuscated);
|
|
353
234
|
}
|
|
235
|
+
catch { /* best-effort */ }
|
|
354
236
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
},
|
|
359
|
-
};
|
|
237
|
+
}
|
|
238
|
+
return { message: { ...msg, content: newContent } };
|
|
239
|
+
}
|
|
360
240
|
});
|
|
361
241
|
// -----------------------------------------------------------------------
|
|
362
242
|
// 3. before_tool_call (async): deobfuscate tool params + track depth
|
|
@@ -405,6 +285,17 @@ export function registerHooks(api, obfuscator) {
|
|
|
405
285
|
api.on("message_sending", async (event) => {
|
|
406
286
|
if (typeof event?.content !== "string")
|
|
407
287
|
return;
|
|
288
|
+
if (auditActive) {
|
|
289
|
+
const { text: deobfuscated, replacementCount } = obfuscator.deobfuscateWithStats(event.content);
|
|
290
|
+
if (deobfuscated === event.content)
|
|
291
|
+
return;
|
|
292
|
+
try {
|
|
293
|
+
emitDeobfuscationAudit(api.logger, config, randomBytes(8).toString("hex"), replacementCount);
|
|
294
|
+
}
|
|
295
|
+
catch { /* best-effort */ }
|
|
296
|
+
dumpStatsFile(obfuscator);
|
|
297
|
+
return { content: deobfuscated };
|
|
298
|
+
}
|
|
408
299
|
const deobfuscated = obfuscator.deobfuscate(event.content);
|
|
409
300
|
if (deobfuscated === event.content)
|
|
410
301
|
return;
|
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.5",
|
|
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