shroud-privacy 2.2.16 → 2.2.18
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 +9 -0
- package/dist/detectors/context.js +8 -0
- package/dist/detectors/regex.js +11 -0
- package/dist/hooks.js +14 -3
- package/dist/obfuscator.js +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -80,6 +80,15 @@ Shroud does not guarantee compliance — regex-based detection has limitations (
|
|
|
80
80
|
|
|
81
81
|
> **Requires OpenClaw 2026.3.22 or later.**
|
|
82
82
|
|
|
83
|
+
### OpenClaw support policy
|
|
84
|
+
|
|
85
|
+
- **Formal minimum supported version:** `2026.3.24` (from `openclaw.plugin.json` `minOpenClawVersion`).
|
|
86
|
+
- **Release validation matrix (this release):**
|
|
87
|
+
- **Baseline:** `2026.3.28` (includes WhatsApp E2E path)
|
|
88
|
+
- **Latest-at-release:** `2026.4.5` (Slack E2E path)
|
|
89
|
+
- **Latest caveat:** on OpenClaw builds where WhatsApp provisioning via `channels add` is unsupported, latest-focused compat runs skip WhatsApp E2E and validate Slack E2E.
|
|
90
|
+
- **Source of truth for current matrix:** `docs/ci-current-state.md` and `CHANGELOG.md`.
|
|
91
|
+
|
|
83
92
|
---
|
|
84
93
|
|
|
85
94
|
## Install
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 10. Confidence decay by frequency (common words lose confidence)
|
|
10
10
|
*/
|
|
11
11
|
import { Category } from "../types.js";
|
|
12
|
+
import { isDocExample } from "./regex.js";
|
|
12
13
|
/**
|
|
13
14
|
* Single-pass multi-string scanner using a combined regex.
|
|
14
15
|
* Replaces per-string indexOf loops with one regex alternation pass — O(M)
|
|
@@ -28,6 +29,10 @@ function scanMultiplePatterns(text, values, covered, category, confidence, detec
|
|
|
28
29
|
const val = m[0];
|
|
29
30
|
const key = `${pos}:${pos + val.length}`;
|
|
30
31
|
if (!covered.has(key)) {
|
|
32
|
+
if (isDocExample(val, category)) {
|
|
33
|
+
covered.add(key);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
31
36
|
covered.add(key);
|
|
32
37
|
results.push({
|
|
33
38
|
value: val,
|
|
@@ -330,6 +335,9 @@ export class ContextDetector {
|
|
|
330
335
|
e.category === Category.HOSTNAME)) {
|
|
331
336
|
// Only learn values that look like identifiers (not too short, not common words)
|
|
332
337
|
if (e.value.length >= 3 && !COMMON_WORDS.has(e.value.toLowerCase())) {
|
|
338
|
+
if (isDocExample(e.value, e.category)) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
333
341
|
this._learnedEntities.set(e.value, e.category);
|
|
334
342
|
}
|
|
335
343
|
}
|
package/dist/detectors/regex.js
CHANGED
|
@@ -172,6 +172,17 @@ export function isDocExample(value, category) {
|
|
|
172
172
|
case Category.HOSTNAME:
|
|
173
173
|
if (DOC_HOSTNAMES.has(value) || DOC_HOSTNAMES.has(value.toUpperCase()))
|
|
174
174
|
return true;
|
|
175
|
+
{
|
|
176
|
+
const lower = value.toLowerCase();
|
|
177
|
+
for (const d of DOC_DOMAINS) {
|
|
178
|
+
if (lower === d || lower.endsWith(`.${d}`))
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
for (const d of PUBLIC_DOMAINS) {
|
|
182
|
+
if (lower === d || lower.endsWith(`.${d}`))
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
175
186
|
for (const pfx of DOC_HOSTNAME_PREFIXES) {
|
|
176
187
|
if (value.toUpperCase().startsWith(pfx))
|
|
177
188
|
return true;
|
package/dist/hooks.js
CHANGED
|
@@ -179,7 +179,7 @@ export function registerHooks(api, obfuscator) {
|
|
|
179
179
|
// cannot deobfuscate CGNAT surrogates in outbound channel messages.
|
|
180
180
|
function stripSlackLinksForHook(text) {
|
|
181
181
|
text = text.replace(/<mailto:[^|>]+\|([^>]*)>/g, "$1");
|
|
182
|
-
text = text.replace(/<https?:\/\/[^|>]
|
|
182
|
+
text = text.replace(/<(https?:\/\/[^|>]+)\|[^>]*>/g, "$1");
|
|
183
183
|
text = text.replace(/<(https?:\/\/[^>]+)>/g, "$1");
|
|
184
184
|
return text;
|
|
185
185
|
}
|
|
@@ -225,12 +225,23 @@ export function registerHooks(api, obfuscator) {
|
|
|
225
225
|
// All hook closures must use the shared obfuscator, not the local parameter.
|
|
226
226
|
// OpenClaw loads the plugin multiple times; only one instance has the mappings.
|
|
227
227
|
const ob = () => getSharedObfuscator(obfuscator);
|
|
228
|
+
const sessionScope = globalThis;
|
|
228
229
|
const config = ob().config;
|
|
229
230
|
const auditActive = config.auditEnabled || config.verboseLogging;
|
|
230
231
|
// -----------------------------------------------------------------------
|
|
231
232
|
// 1. before_prompt_build (async): obfuscate user prompt
|
|
232
233
|
// -----------------------------------------------------------------------
|
|
233
|
-
api.on("before_prompt_build", async (event) => {
|
|
234
|
+
api.on("before_prompt_build", async (event, ctx) => {
|
|
235
|
+
// Scope mapping and learned-entity state to the OpenClaw session key.
|
|
236
|
+
// The obfuscator instance is shared globally across plugin instances, so
|
|
237
|
+
// without this reset independent sessions can contaminate each other.
|
|
238
|
+
if (typeof ctx?.sessionKey === "string" && ctx.sessionKey) {
|
|
239
|
+
const prevSessionKey = sessionScope.__shroudLastSessionKey;
|
|
240
|
+
if (typeof prevSessionKey === "string" && prevSessionKey !== ctx.sessionKey) {
|
|
241
|
+
ob().reset();
|
|
242
|
+
}
|
|
243
|
+
sessionScope.__shroudLastSessionKey = ctx.sessionKey;
|
|
244
|
+
}
|
|
234
245
|
// Reset tool depth at the start of each turn — tool calls from the
|
|
235
246
|
// previous turn are complete, so the counter should not carry over.
|
|
236
247
|
if (ob().toolDepth > 0) {
|
|
@@ -880,7 +891,7 @@ export function registerHooks(api, obfuscator) {
|
|
|
880
891
|
// leaking real PII to the LLM.
|
|
881
892
|
function stripSlackLinks(text) {
|
|
882
893
|
text = text.replace(/<mailto:[^|>]+\|([^>]*)>/g, "$1");
|
|
883
|
-
text = text.replace(/<https?:\/\/[^|>]
|
|
894
|
+
text = text.replace(/<(https?:\/\/[^|>]+)\|[^>]*>/g, "$1");
|
|
884
895
|
text = text.replace(/<(https?:\/\/[^>]+)>/g, "$1");
|
|
885
896
|
return text;
|
|
886
897
|
}
|
package/dist/obfuscator.js
CHANGED
|
@@ -101,8 +101,8 @@ function compressIPv6(addr) {
|
|
|
101
101
|
function stripSlackLinks(text) {
|
|
102
102
|
// <mailto:X|DISPLAY> → DISPLAY (email links)
|
|
103
103
|
text = text.replace(/<mailto:[^|>]+\|([^>]*)>/g, "$1");
|
|
104
|
-
// <URL|DISPLAY> →
|
|
105
|
-
text = text.replace(/<https?:\/\/[^|>]
|
|
104
|
+
// <URL|DISPLAY> → URL (preserve real URL for passthrough checks)
|
|
105
|
+
text = text.replace(/<(https?:\/\/[^|>]+)\|[^>]*>/g, "$1");
|
|
106
106
|
// <URL> → URL (bare URL links, no display text)
|
|
107
107
|
text = text.replace(/<(https?:\/\/[^>]+)>/g, "$1");
|
|
108
108
|
return text;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shroud-privacy",
|
|
3
3
|
"name": "Shroud",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.17",
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shroud-privacy",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.18",
|
|
4
4
|
"description": "Privacy and infrastructure protection for AI agents — detects sensitive data (PII, network topology, credentials, OT/SCADA) and replaces with deterministic fakes before anything reaches the LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"test:unit": "vitest run",
|
|
24
24
|
"test:integration": "node tests/harness/run.mjs",
|
|
25
25
|
"test:docker": "bash compat/run-e2e.sh latest",
|
|
26
|
-
"test:docker:legacy": "bash compat/run-compat.sh latest",
|
|
27
26
|
"test:all": "npm run test:unit && npm run test:integration && npm run test:docker",
|
|
28
27
|
"test:watch": "vitest",
|
|
29
28
|
"lint": "tsc --noEmit",
|