shroud-privacy 2.2.7 → 2.2.9
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 +10 -0
- package/dist/detectors/regex.js +61 -1
- package/dist/hooks.js +41 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -256,6 +256,16 @@ Shroud distinguishes between internal and external URLs:
|
|
|
256
256
|
| `https://secret.local/admin` | NXDOMAIN | Obfuscate |
|
|
257
257
|
| `https://github.com/org/repo` | (PUBLIC_DOMAINS list) | Pass through |
|
|
258
258
|
|
|
259
|
+
### LLM agent guidance
|
|
260
|
+
|
|
261
|
+
Because Shroud replaces URLs before they reach the LLM, the LLM may see unfamiliar or fake-looking domains in the conversation context. Tool calls (fetch, read, etc.) are deobfuscated automatically before execution, so they work correctly even when the LLM sees a fake URL.
|
|
262
|
+
|
|
263
|
+
**If you are building an agent that uses Shroud, add the following to your agent's system prompt or instruction files:**
|
|
264
|
+
|
|
265
|
+
> Shroud privacy is active. URLs and domains in the conversation may appear different from what the user sent — internal URLs are replaced with fake domains to protect infrastructure. If a URL looks unfamiliar or doesn't resolve, it has likely been obfuscated. The tool call pipeline deobfuscates automatically. Do NOT tell the user a URL is invalid just because you see an unfamiliar domain. If a fetch or read tool succeeded with the URL, trust the result.
|
|
266
|
+
|
|
267
|
+
This prevents the LLM from questioning obfuscated URLs or telling the user their link is broken.
|
|
268
|
+
|
|
259
269
|
---
|
|
260
270
|
|
|
261
271
|
## Redaction levels
|
package/dist/detectors/regex.js
CHANGED
|
@@ -42,6 +42,24 @@ const PUBLIC_DOMAINS = new Set([
|
|
|
42
42
|
"w3.org",
|
|
43
43
|
"archive.org",
|
|
44
44
|
]);
|
|
45
|
+
/**
|
|
46
|
+
* Operational path prefixes — local system/workspace paths that agents need
|
|
47
|
+
* to function. These are NOT sensitive infrastructure to hide from the LLM.
|
|
48
|
+
*/
|
|
49
|
+
const OPERATIONAL_PATH_PREFIXES = [
|
|
50
|
+
"/home/", // user home directories (workspace, scripts, media)
|
|
51
|
+
"/tmp/", // temp files
|
|
52
|
+
"/proc/", // procfs
|
|
53
|
+
"/sys/", // sysfs
|
|
54
|
+
"/dev/", // devices
|
|
55
|
+
"/run/", // runtime data
|
|
56
|
+
"/snap/", // snap packages
|
|
57
|
+
"/root/", // root home
|
|
58
|
+
"/nix/", // nix store
|
|
59
|
+
// NOTE: /etc/, /usr/, /var/, /bin/, /sbin/, /lib/, /opt/ are NOT included —
|
|
60
|
+
// they may contain infrastructure config paths (nginx, systemd units, etc.)
|
|
61
|
+
// that should be obfuscated in network/OT contexts.
|
|
62
|
+
];
|
|
45
63
|
const DOC_HOSTNAMES = new Set([
|
|
46
64
|
"localhost", "HOSTNAME", "EXAMPLE", "CHANGEME",
|
|
47
65
|
"YOUR_HOST", "YOURHOST", "hostname", "example",
|
|
@@ -113,6 +131,44 @@ export function isDocExample(value, category) {
|
|
|
113
131
|
case Category.BGP_ASN:
|
|
114
132
|
// Private ASNs are real infra identifiers — don't skip them
|
|
115
133
|
return false;
|
|
134
|
+
case Category.FILE_PATH: {
|
|
135
|
+
if (value.startsWith("/")) {
|
|
136
|
+
const pathLower = value.toLowerCase();
|
|
137
|
+
// Skip operational/system paths — these are local workspace paths the
|
|
138
|
+
// agent needs to function, not sensitive infrastructure to hide from the LLM.
|
|
139
|
+
// Sensitive paths are things like /opt/network-configs/router.cfg on internal
|
|
140
|
+
// servers — those won't match these prefixes.
|
|
141
|
+
for (const pfx of OPERATIONAL_PATH_PREFIXES) {
|
|
142
|
+
if (pathLower.startsWith(pfx))
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
// Skip paths that are clearly URL path components from public domains.
|
|
146
|
+
// e.g., /www.npmjs.com/package/shroud-privacy, /github.com/org/repo
|
|
147
|
+
for (const d of PUBLIC_DOMAINS) {
|
|
148
|
+
if (pathLower.startsWith(`/${d}/`) || pathLower.startsWith(`/${d}`)
|
|
149
|
+
|| pathLower.startsWith(`/www.${d}/`) || pathLower.startsWith(`/www.${d}`)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const d of DOC_DOMAINS) {
|
|
154
|
+
if (pathLower.startsWith(`/${d}/`) || pathLower.startsWith(`/${d}`)
|
|
155
|
+
|| pathLower.startsWith(`/www.${d}/`) || pathLower.startsWith(`/www.${d}`)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// DNS cache check — if the first path segment is a public domain
|
|
160
|
+
const dnsCache = globalThis.__shroudDnsCache;
|
|
161
|
+
if (dnsCache) {
|
|
162
|
+
const firstSeg = value.slice(1).split("/")[0];
|
|
163
|
+
if (firstSeg && firstSeg.includes(".")) {
|
|
164
|
+
const isPublic = dnsCache.isPublic("https://" + firstSeg + "/");
|
|
165
|
+
if (isPublic === true)
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
116
172
|
case Category.HOSTNAME:
|
|
117
173
|
if (DOC_HOSTNAMES.has(value) || DOC_HOSTNAMES.has(value.toUpperCase()))
|
|
118
174
|
return true;
|
|
@@ -584,7 +640,11 @@ export const BUILTIN_PATTERNS = [
|
|
|
584
640
|
},
|
|
585
641
|
{
|
|
586
642
|
name: "gps_coordinate",
|
|
587
|
-
|
|
643
|
+
// Require realistic lat/lon ranges: lat [-90,90], lon [-180,180].
|
|
644
|
+
// Must be comma-separated (not just whitespace — that matches too many
|
|
645
|
+
// false positives in financial data, ML weights, research metrics).
|
|
646
|
+
// Require 4-8 decimal places (GPS precision).
|
|
647
|
+
pattern: /(?<!\w)-?(?:[0-8]?\d(?:\.\d{4,8})|90(?:\.0{4,8}))\s*,\s*-?(?:1[0-7]\d(?:\.\d{4,8})|0?\d{1,2}(?:\.\d{4,8})|180(?:\.0{4,8}))(?!\w)/g,
|
|
588
648
|
category: Category.GPS_COORDINATE,
|
|
589
649
|
confidence: 0.85,
|
|
590
650
|
},
|
package/dist/hooks.js
CHANGED
|
@@ -194,7 +194,32 @@ export function registerHooks(api, obfuscator) {
|
|
|
194
194
|
}
|
|
195
195
|
// DNS cache for public URL detection — shared across plugin instances
|
|
196
196
|
if (!g.__shroudDnsCache) {
|
|
197
|
-
|
|
197
|
+
const cache = new DnsCache();
|
|
198
|
+
g.__shroudDnsCache = cache;
|
|
199
|
+
// Pre-warm with well-known public domains so first-turn URLs pass through
|
|
200
|
+
// without waiting for async DNS resolution. These domains are guaranteed
|
|
201
|
+
// public — no lookup needed.
|
|
202
|
+
const publicDomains = [
|
|
203
|
+
"youtube.com", "youtu.be", "m.youtube.com",
|
|
204
|
+
"google.com", "google.co.uk", "google.de", "google.fr",
|
|
205
|
+
"github.com", "gitlab.com", "bitbucket.org",
|
|
206
|
+
"stackoverflow.com", "stackexchange.com",
|
|
207
|
+
"wikipedia.org", "wikimedia.org",
|
|
208
|
+
"twitter.com", "x.com",
|
|
209
|
+
"reddit.com",
|
|
210
|
+
"linkedin.com",
|
|
211
|
+
"medium.com",
|
|
212
|
+
"npmjs.com", "www.npmjs.com", "pypi.org", "crates.io",
|
|
213
|
+
"docker.com", "hub.docker.com",
|
|
214
|
+
"microsoft.com", "apple.com",
|
|
215
|
+
"mozilla.org",
|
|
216
|
+
"w3.org",
|
|
217
|
+
"archive.org",
|
|
218
|
+
];
|
|
219
|
+
for (const d of publicDomains) {
|
|
220
|
+
cache.seed(d, "0.0.0.1", true); // address doesn't matter, isPublic=true
|
|
221
|
+
cache.seed("www." + d, "0.0.0.1", true);
|
|
222
|
+
}
|
|
198
223
|
}
|
|
199
224
|
}
|
|
200
225
|
// All hook closures must use the shared obfuscator, not the local parameter.
|
|
@@ -215,12 +240,24 @@ export function registerHooks(api, obfuscator) {
|
|
|
215
240
|
// Extract all URLs from the prompt and messages, resolve their FQDNs
|
|
216
241
|
// to determine public vs private. This runs BEFORE obfuscation so
|
|
217
242
|
// the sync pipeline's isDocExample() can check the cache.
|
|
243
|
+
//
|
|
244
|
+
// Slack wraps URLs as <https://url|display> or <https://url>.
|
|
245
|
+
// We must strip this markup BEFORE extracting URLs, otherwise the
|
|
246
|
+
// regex won't match and the DNS cache won't warm for Slack messages.
|
|
218
247
|
const dnsCache = globalThis.__shroudDnsCache;
|
|
219
248
|
if (dnsCache) {
|
|
220
249
|
const urlRe = /https?:\/\/[^\s<>"')\]]+[^\s<>"')\].,;:!?]/g;
|
|
221
250
|
const allUrls = [];
|
|
251
|
+
// Strip Slack link markup so URL regex can match cleanly
|
|
252
|
+
function stripSlackForDns(text) {
|
|
253
|
+
text = text.replace(/<mailto:[^|>]+\|([^>]*)>/g, "$1");
|
|
254
|
+
text = text.replace(/<(https?:\/\/[^|>]+)\|[^>]*>/g, "$1");
|
|
255
|
+
text = text.replace(/<(https?:\/\/[^>]+)>/g, "$1");
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
222
258
|
if (typeof event?.prompt === "string") {
|
|
223
|
-
|
|
259
|
+
const cleaned = stripSlackForDns(event.prompt);
|
|
260
|
+
for (const m of cleaned.matchAll(urlRe))
|
|
224
261
|
allUrls.push(m[0]);
|
|
225
262
|
}
|
|
226
263
|
if (Array.isArray(event?.messages)) {
|
|
@@ -235,7 +272,8 @@ export function registerHooks(api, obfuscator) {
|
|
|
235
272
|
}
|
|
236
273
|
}
|
|
237
274
|
for (const text of texts) {
|
|
238
|
-
|
|
275
|
+
const cleaned = stripSlackForDns(text);
|
|
276
|
+
for (const m of cleaned.matchAll(urlRe))
|
|
239
277
|
allUrls.push(m[0]);
|
|
240
278
|
}
|
|
241
279
|
}
|
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.9",
|
|
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.9",
|
|
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",
|