shroud-privacy 2.4.0 → 2.5.1
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 +70 -4
- package/dist/config-manager.js +13 -0
- package/dist/config.js +25 -0
- package/dist/field-scope.d.ts +25 -0
- package/dist/field-scope.js +92 -0
- package/dist/hooks.js +59 -7
- package/dist/obfuscator.d.ts +1 -1
- package/dist/obfuscator.js +7 -1
- package/dist/types.d.ts +28 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<a href="CHANGELOG.md">Changelog</a>
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
|
-
> Apache 2.0 · Zero runtime dependencies · Anthropic + OpenAI + Google supported · Prompt-caching friendly · Works with [OpenClaw](https://openclaw.ai) or any agent via [APP](#agent-privacy-protocol-app)
|
|
20
|
+
> 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
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -32,7 +32,7 @@ Frontier LLMs are transformative for infrastructure operations — network troub
|
|
|
32
32
|
- **Customer PII** — emails, phone numbers, national IDs, credit cards, physical addresses
|
|
33
33
|
- **Internal URLs** — wiki pages, Jira tickets, admin portals, API endpoints
|
|
34
34
|
|
|
35
|
-
Shroud sits between your agent and the LLM. It detects all of the above (
|
|
35
|
+
Shroud sits between your agent and the LLM. It detects all of the above (130+ entity types), replaces each with a deterministic format-preserving fake, and reverses the mapping on the way back. The LLM reasons over realistic-looking data. Your real infrastructure stays private.
|
|
36
36
|
|
|
37
37
|
### Who needs this
|
|
38
38
|
|
|
@@ -56,7 +56,7 @@ Shroud does not guarantee compliance — regex-based detection has limitations (
|
|
|
56
56
|
|
|
57
57
|
## What it does
|
|
58
58
|
|
|
59
|
-
1. **Detects**
|
|
59
|
+
1. **Detects** 130+ entity types: emails, IPs, phones, API keys, hostnames, SNMP communities, BGP ASNs, credit cards, SSNs, file paths, URLs, person/org/location names, VLANs, route-maps, ACLs, OSPF IDs, IBANs, JWTs, PEM certs, GPS coordinates, ICS/SCADA identifiers, dates of birth, medical record numbers (MRN/NPI/DEA), bank accounts (routing/sort code/SWIFT), tax IDs (EIN/UTR), passport numbers, driver's licenses, court case/docket/patent numbers, cryptocurrency addresses (Ethereum/Bitcoin), AWS ARNs, vendor-specific secrets (Cisco, Juniper, Palo Alto, Check Point, Fortinet, F5, Arista), and custom regex patterns.
|
|
60
60
|
2. **Replaces** each value with a deterministic fake (same input + key = same fake every time). Fakes are format-preserving: IPv4 stays in CGNAT range (`100.64.0.0/10`), IPv6 uses ULA range (`fd00::/8`), emails keep `@domain` structure, credit cards pass Luhn, etc.
|
|
61
61
|
3. **Passes through public URLs** — external URLs (arxiv.org, docs.stripe.com, etc.) are not obfuscated. Shroud resolves FQDNs via DNS: public IPs pass through, RFC 1918 / NXDOMAIN / internal IPs are obfuscated. Well-known platforms (GitHub, YouTube, Wikipedia, etc.) are always passed through.
|
|
62
62
|
4. **Deobfuscates** LLM responses and tool parameters so the user sees real values and tools receive real arguments.
|
|
@@ -102,6 +102,44 @@ openclaw plugins install shroud-privacy
|
|
|
102
102
|
|
|
103
103
|
Configure in `~/.openclaw/openclaw.json` under `plugins.entries."shroud-privacy".config`. No OpenClaw file modifications needed — Shroud uses runtime interception only.
|
|
104
104
|
|
|
105
|
+
### Hermes Agent
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
hermes plugins install wkeything/shroud
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
That's it. The plugin auto-builds on first session start (requires Node.js). All LLM traffic is obfuscated transparently — no Hermes configuration changes needed.
|
|
112
|
+
|
|
113
|
+
Per-tool field scoping is enabled by default, reducing false positives on structural fields (IDs, hashes, timestamps). Works with all Hermes-supported providers (OpenRouter, Anthropic, OpenAI, z.ai, local models).
|
|
114
|
+
|
|
115
|
+
**Config-as-code** is supported — edit `~/.shroud/shroud.config.json` to customize detection rules, field scoping, and confidence thresholds. Changes hot-reload within 2 seconds, no restart needed. The config file is shared with OpenClaw — edits apply to both platforms.
|
|
116
|
+
|
|
117
|
+
Verify after a conversation:
|
|
118
|
+
```bash
|
|
119
|
+
cat ~/.hermes/shroud-stats.json | python3 -m json.tool
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Claude Code
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm install shroud-privacy
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Add to your project's `.mcp.json` or `~/.claude/.mcp.json`:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"mcpServers": {
|
|
133
|
+
"shroud": {
|
|
134
|
+
"command": "node",
|
|
135
|
+
"args": ["node_modules/shroud-privacy/clients/claude-code/shroud-mcp.mjs"]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
That's it — the MCP server auto-starts the privacy engine. Claude gains six tools: `shroud_obfuscate`, `shroud_deobfuscate`, `shroud_status`, `shroud_scan_tool`, `shroud_configure`, and `shroud_reset`.
|
|
142
|
+
|
|
105
143
|
### Any agent (via APP)
|
|
106
144
|
|
|
107
145
|
The **Agent Privacy Protocol** (APP) lets any AI agent add privacy and infrastructure protection — no OpenClaw required. Shroud ships with an APP server and a Python client.
|
|
@@ -230,9 +268,37 @@ Out of the box, Shroud:
|
|
|
230
268
|
| `redactionLevel` | `"full"` \| `"masked"` \| `"stats"` | `"full"` | Output mode: fake values, partial masking, or category placeholders |
|
|
231
269
|
| `dryRun` | boolean | `false` | Detect entities but don't replace (testing mode) |
|
|
232
270
|
| `maxStoreMappings` | number | `0` | Max mapping store size with LRU eviction (0 = unlimited) |
|
|
271
|
+
| `fieldScoping` | object | — | Per-tool field scoping and per-agent category exemptions (see below) |
|
|
233
272
|
|
|
234
273
|
> **Env var overrides:** `SHROUD_SECRET_KEY` and `SHROUD_PERSISTENT_SALT` override their respective config keys (priority: env var > plugin config > default).
|
|
235
274
|
|
|
275
|
+
### Per-tool field scoping
|
|
276
|
+
|
|
277
|
+
By default Shroud scans every string field in every message. This catches everything but produces false positives — file paths agents need, config values, UUIDs matching credit card patterns.
|
|
278
|
+
|
|
279
|
+
Field scoping narrows what gets scanned. Add a `fieldScoping` block to `shroud.config.json`:
|
|
280
|
+
|
|
281
|
+
```jsonc
|
|
282
|
+
{
|
|
283
|
+
"fieldScoping": {
|
|
284
|
+
"toolFields": {
|
|
285
|
+
"Read": { "scanFields": ["content", "text"] },
|
|
286
|
+
"Bash": { "scanFields": ["output", "stdout", "stderr"] },
|
|
287
|
+
"gmail_*": { "scanFields": ["subject", "body", "snippet", "from", "to"] },
|
|
288
|
+
"github_*": { "scanFields": ["title", "body", "description", "comment"] }
|
|
289
|
+
},
|
|
290
|
+
"neverScanFields": ["id", "created_at", "updated_at", "sha", "hash", "ref", "type", "status"],
|
|
291
|
+
"defaultScanFields": []
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**`toolFields`** maps tool name patterns (wildcards `*` `?` supported) to the fields that should be scanned in their results. Unmatched tools fall back to `defaultScanFields` — set it to `[]` to scan everything for unknown tools (safe default).
|
|
297
|
+
|
|
298
|
+
**`neverScanFields`** lists structural fields that never contain user-generated content. These are skipped regardless of tool.
|
|
299
|
+
|
|
300
|
+
Hot-reloadable. No config = scan everything (backward compatible).
|
|
301
|
+
|
|
236
302
|
### Detection rules as code (hot-reload)
|
|
237
303
|
|
|
238
304
|
Shroud auto-generates a JSONC config file on first run containing every built-in detection rule:
|
|
@@ -409,7 +475,7 @@ shroud-stats --test "Contact john@acme.com" # test detection
|
|
|
409
475
|
|
|
410
476
|
## Entity categories
|
|
411
477
|
|
|
412
|
-
`person_name`, `email`, `phone`, `ip_address`, `api_key`, `url`, `org_name`, `location`, `file_path`, `credit_card`, `ssn`, `mac_address`, `hostname`, `snmp_community`, `bgp_asn`, `network_credential`, `vlan_id`, `interface_desc`, `route_map`, `ospf_id`, `acl_name`, `iban`, `national_id`, `jwt`, `ics_identifier`, `gps_coordinate`, `certificate`, `custom`
|
|
478
|
+
`person_name`, `email`, `phone`, `ip_address`, `api_key`, `url`, `org_name`, `location`, `file_path`, `credit_card`, `ssn`, `mac_address`, `hostname`, `snmp_community`, `bgp_asn`, `network_credential`, `vlan_id`, `interface_desc`, `route_map`, `ospf_id`, `acl_name`, `iban`, `national_id`, `jwt`, `ics_identifier`, `gps_coordinate`, `certificate`, `date_of_birth`, `medical_record_number`, `bank_account_number`, `tax_id`, `passport_number`, `drivers_license`, `case_number`, `cryptocurrency_address`, `aws_arn`, `custom`
|
|
413
479
|
|
|
414
480
|
---
|
|
415
481
|
|
package/dist/config-manager.js
CHANGED
|
@@ -154,6 +154,19 @@ export class ConfigManager {
|
|
|
154
154
|
" // Edit rules here. Changes hot-reload within 2 seconds (no restart needed).",
|
|
155
155
|
" // Priority: env vars > this file > plugin config > defaults.",
|
|
156
156
|
" //",
|
|
157
|
+
" // Per-tool field scoping — controls which fields get scanned for PII.",
|
|
158
|
+
" // Reduces false positives from structural fields (IDs, hashes, timestamps).",
|
|
159
|
+
' "fieldScoping": {',
|
|
160
|
+
' "toolFields": {',
|
|
161
|
+
' "Read": { "scanFields": ["content", "text"] },',
|
|
162
|
+
' "read": { "scanFields": ["content", "text"] },',
|
|
163
|
+
' "Bash": { "scanFields": ["output", "stdout", "stderr"] },',
|
|
164
|
+
' "exec": { "scanFields": ["output", "stdout", "stderr"] }',
|
|
165
|
+
" },",
|
|
166
|
+
' "neverScanFields": ["id", "created_at", "updated_at", "sha", "hash", "ref", "type", "status", "state", "mode"],',
|
|
167
|
+
' "defaultScanFields": []',
|
|
168
|
+
" },",
|
|
169
|
+
" //",
|
|
157
170
|
" // Rule format:",
|
|
158
171
|
' // "rule_name": {',
|
|
159
172
|
' // "pattern": "regex string", // override or define the detection regex',
|
package/dist/config.js
CHANGED
|
@@ -82,6 +82,31 @@ export function resolveConfig(pluginConfig) {
|
|
|
82
82
|
dryRun: typeof raw.dryRun === "boolean" ? raw.dryRun : false,
|
|
83
83
|
// LRU store eviction (0 = unlimited)
|
|
84
84
|
maxStoreMappings: typeof raw.maxStoreMappings === "number" ? raw.maxStoreMappings : 0,
|
|
85
|
+
// Field scoping (optional, backward compatible)
|
|
86
|
+
fieldScoping: (() => {
|
|
87
|
+
const fs = raw.fieldScoping;
|
|
88
|
+
if (!fs || typeof fs !== "object")
|
|
89
|
+
return undefined;
|
|
90
|
+
const fsc = fs;
|
|
91
|
+
const toolFields = {};
|
|
92
|
+
if (fsc.toolFields && typeof fsc.toolFields === "object") {
|
|
93
|
+
for (const [pattern, rule] of Object.entries(fsc.toolFields)) {
|
|
94
|
+
if (rule && typeof rule === "object" && Array.isArray(rule.scanFields)) {
|
|
95
|
+
toolFields[pattern] = { scanFields: rule.scanFields.filter((f) => typeof f === "string") };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
toolFields,
|
|
101
|
+
neverScanFields: Array.isArray(fsc.neverScanFields)
|
|
102
|
+
? fsc.neverScanFields.filter((f) => typeof f === "string")
|
|
103
|
+
: [],
|
|
104
|
+
defaultScanFields: Array.isArray(fsc.defaultScanFields)
|
|
105
|
+
? fsc.defaultScanFields.filter((f) => typeof f === "string")
|
|
106
|
+
: [],
|
|
107
|
+
useContractExemptions: typeof fsc.useContractExemptions === "boolean" ? fsc.useContractExemptions : false,
|
|
108
|
+
};
|
|
109
|
+
})(),
|
|
85
110
|
};
|
|
86
111
|
return config;
|
|
87
112
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool field scoping and per-agent category exemptions for obfuscation.
|
|
3
|
+
*
|
|
4
|
+
* Reduces false positives by only scanning relevant fields per tool and
|
|
5
|
+
* exempting entity categories that the agent's contract allows.
|
|
6
|
+
*/
|
|
7
|
+
import type { FieldScopingConfig, ScopeDecision } from "./types.js";
|
|
8
|
+
export declare class FieldScopeResolver {
|
|
9
|
+
private readonly toolPatterns;
|
|
10
|
+
private readonly neverScanFields;
|
|
11
|
+
private readonly defaultScanFields;
|
|
12
|
+
private readonly useContractExemptions;
|
|
13
|
+
private readonly enabled;
|
|
14
|
+
constructor(config?: FieldScopingConfig);
|
|
15
|
+
/** Resolve which fields to scan for a given tool name. */
|
|
16
|
+
resolveToolScope(toolName: string): ScopeDecision;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve which entity categories are exempt from obfuscation for an agent.
|
|
19
|
+
* Uses the agent contract's allowedDataClasses — those categories are data
|
|
20
|
+
* the agent is trusted to handle, so obfuscating them is counterproductive.
|
|
21
|
+
*/
|
|
22
|
+
resolveAgentExemptions(agentLabel: string, role: string): Set<string>;
|
|
23
|
+
/** Check if a field should be scanned given the resolved scope. */
|
|
24
|
+
shouldScanField(fieldName: string, scope: ScopeDecision): boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool field scoping and per-agent category exemptions for obfuscation.
|
|
3
|
+
*
|
|
4
|
+
* Reduces false positives by only scanning relevant fields per tool and
|
|
5
|
+
* exempting entity categories that the agent's contract allows.
|
|
6
|
+
*/
|
|
7
|
+
/** Simple wildcard matching (supports * and ?). */
|
|
8
|
+
function wildcardMatch(value, pattern) {
|
|
9
|
+
const escaped = pattern
|
|
10
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
11
|
+
.replace(/\*/g, ".*")
|
|
12
|
+
.replace(/\?/g, ".");
|
|
13
|
+
return new RegExp(`^${escaped}$`, "i").test(value);
|
|
14
|
+
}
|
|
15
|
+
export class FieldScopeResolver {
|
|
16
|
+
toolPatterns;
|
|
17
|
+
neverScanFields;
|
|
18
|
+
defaultScanFields;
|
|
19
|
+
useContractExemptions;
|
|
20
|
+
enabled;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
if (!config) {
|
|
23
|
+
this.enabled = false;
|
|
24
|
+
this.toolPatterns = [];
|
|
25
|
+
this.neverScanFields = new Set();
|
|
26
|
+
this.defaultScanFields = new Set();
|
|
27
|
+
this.useContractExemptions = false;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.enabled = true;
|
|
31
|
+
this.neverScanFields = new Set(config.neverScanFields ?? []);
|
|
32
|
+
this.defaultScanFields = new Set(config.defaultScanFields ?? []);
|
|
33
|
+
this.useContractExemptions = config.useContractExemptions ?? false;
|
|
34
|
+
this.toolPatterns = [];
|
|
35
|
+
for (const [pattern, rule] of Object.entries(config.toolFields ?? {})) {
|
|
36
|
+
this.toolPatterns.push({ pattern, scanFields: new Set(rule.scanFields) });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Resolve which fields to scan for a given tool name. */
|
|
40
|
+
resolveToolScope(toolName) {
|
|
41
|
+
if (!this.enabled) {
|
|
42
|
+
return { mode: "all", scanFields: new Set(), neverScanFields: new Set() };
|
|
43
|
+
}
|
|
44
|
+
// Find first matching tool pattern
|
|
45
|
+
for (const { pattern, scanFields } of this.toolPatterns) {
|
|
46
|
+
if (wildcardMatch(toolName, pattern)) {
|
|
47
|
+
return {
|
|
48
|
+
mode: "selected",
|
|
49
|
+
scanFields,
|
|
50
|
+
neverScanFields: this.neverScanFields,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// No match — use default
|
|
55
|
+
if (this.defaultScanFields.size > 0) {
|
|
56
|
+
return {
|
|
57
|
+
mode: "selected",
|
|
58
|
+
scanFields: this.defaultScanFields,
|
|
59
|
+
neverScanFields: this.neverScanFields,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Empty defaultScanFields = scan everything (backward compatible)
|
|
63
|
+
return { mode: "all", scanFields: new Set(), neverScanFields: this.neverScanFields };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve which entity categories are exempt from obfuscation for an agent.
|
|
67
|
+
* Uses the agent contract's allowedDataClasses — those categories are data
|
|
68
|
+
* the agent is trusted to handle, so obfuscating them is counterproductive.
|
|
69
|
+
*/
|
|
70
|
+
resolveAgentExemptions(agentLabel, role) {
|
|
71
|
+
if (!this.enabled || !this.useContractExemptions) {
|
|
72
|
+
return new Set();
|
|
73
|
+
}
|
|
74
|
+
// contracts.ts only exists on feature/transformer — graceful fallback
|
|
75
|
+
try {
|
|
76
|
+
const { resolveAgentContract } = require("./contracts.js");
|
|
77
|
+
const contract = resolveAgentContract(agentLabel, role);
|
|
78
|
+
return new Set(contract.allowedDataClasses);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return new Set();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Check if a field should be scanned given the resolved scope. */
|
|
85
|
+
shouldScanField(fieldName, scope) {
|
|
86
|
+
if (scope.neverScanFields.has(fieldName))
|
|
87
|
+
return false;
|
|
88
|
+
if (scope.mode === "all")
|
|
89
|
+
return true;
|
|
90
|
+
return scope.scanFields.has(fieldName);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/hooks.js
CHANGED
|
@@ -25,6 +25,7 @@ import { writeFileSync } from "node:fs";
|
|
|
25
25
|
import { BUILTIN_PATTERNS } from "./detectors/regex.js";
|
|
26
26
|
import { STATS_FILE, IS_TEST } from "./config.js";
|
|
27
27
|
import { DnsCache } from "./dns-cache.js";
|
|
28
|
+
import { FieldScopeResolver } from "./field-scope.js";
|
|
28
29
|
function getSharedObfuscator(fallback) {
|
|
29
30
|
return globalThis.__shroudObfuscator || fallback;
|
|
30
31
|
}
|
|
@@ -168,6 +169,36 @@ function walkStrings(value, fn) {
|
|
|
168
169
|
}
|
|
169
170
|
return value;
|
|
170
171
|
}
|
|
172
|
+
function walkStringsScoped(value, fn, shouldScan, currentField) {
|
|
173
|
+
if (typeof value === "string") {
|
|
174
|
+
if (currentField !== undefined && !shouldScan(currentField))
|
|
175
|
+
return value;
|
|
176
|
+
return fn(value);
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(value)) {
|
|
179
|
+
return value.map((item) => {
|
|
180
|
+
if (typeof item === "object" && item !== null && "text" in item && typeof item.text === "string") {
|
|
181
|
+
if (!shouldScan("text"))
|
|
182
|
+
return item;
|
|
183
|
+
return { ...item, text: fn(item.text) };
|
|
184
|
+
}
|
|
185
|
+
return walkStringsScoped(item, fn, shouldScan);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (typeof value === "object" && value !== null) {
|
|
189
|
+
const out = {};
|
|
190
|
+
for (const [k, v] of Object.entries(value)) {
|
|
191
|
+
if (typeof v === "string") {
|
|
192
|
+
out[k] = shouldScan(k) ? fn(v) : v;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
out[k] = v;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
171
202
|
// ---------------------------------------------------------------------------
|
|
172
203
|
// Hook registration
|
|
173
204
|
// ---------------------------------------------------------------------------
|
|
@@ -227,6 +258,16 @@ export function registerHooks(api, obfuscator) {
|
|
|
227
258
|
const ob = () => getSharedObfuscator(obfuscator);
|
|
228
259
|
const sessionScope = globalThis;
|
|
229
260
|
const config = ob().config;
|
|
261
|
+
let _fieldScopeResolver;
|
|
262
|
+
let _fieldScopeConfigRef;
|
|
263
|
+
function getFieldScopeResolver() {
|
|
264
|
+
const liveConfig = ob().config.fieldScoping;
|
|
265
|
+
if (_fieldScopeResolver && _fieldScopeConfigRef === liveConfig)
|
|
266
|
+
return _fieldScopeResolver;
|
|
267
|
+
_fieldScopeResolver = new FieldScopeResolver(liveConfig);
|
|
268
|
+
_fieldScopeConfigRef = liveConfig;
|
|
269
|
+
return _fieldScopeResolver;
|
|
270
|
+
}
|
|
230
271
|
const auditActive = config.auditEnabled || config.verboseLogging;
|
|
231
272
|
// -----------------------------------------------------------------------
|
|
232
273
|
// 1. before_prompt_build (async): obfuscate user prompt
|
|
@@ -299,12 +340,18 @@ export function registerHooks(api, obfuscator) {
|
|
|
299
340
|
}
|
|
300
341
|
}
|
|
301
342
|
let totalEntities = 0;
|
|
343
|
+
// Resolve per-agent category exemptions from contract
|
|
344
|
+
const _resolver = getFieldScopeResolver();
|
|
345
|
+
const _exemptCats = new Set();
|
|
346
|
+
// Note: main branch has no agent session tracker, so contract exemptions
|
|
347
|
+
// are resolved when useContractExemptions is enabled via config only.
|
|
348
|
+
// The full agent-aware path is on feature/transformer.
|
|
302
349
|
// Obfuscate the system prompt
|
|
303
350
|
const prompt = event?.prompt;
|
|
304
351
|
let obfuscatedPrompt;
|
|
305
352
|
if (typeof prompt === "string" && prompt) {
|
|
306
353
|
const cleaned = stripSlackLinksForHook(prompt);
|
|
307
|
-
const result = ob().obfuscate(cleaned);
|
|
354
|
+
const result = ob().obfuscate(cleaned, undefined, _exemptCats);
|
|
308
355
|
if (result.entities.length > 0 || cleaned !== prompt) {
|
|
309
356
|
obfuscatedPrompt = result.entities.length > 0 ? result.obfuscated : cleaned;
|
|
310
357
|
totalEntities += result.entities.length;
|
|
@@ -322,7 +369,7 @@ export function registerHooks(api, obfuscator) {
|
|
|
322
369
|
// String content (Anthropic/OpenAI)
|
|
323
370
|
if (typeof msg.content === "string") {
|
|
324
371
|
const cleaned = stripSlackLinksForHook(msg.content);
|
|
325
|
-
const result = ob().obfuscate(cleaned);
|
|
372
|
+
const result = ob().obfuscate(cleaned, undefined, _exemptCats);
|
|
326
373
|
totalEntities += result.entities.length;
|
|
327
374
|
if (result.entities.length > 0 || cleaned !== msg.content) {
|
|
328
375
|
msg.content = result.entities.length > 0 ? result.obfuscated : cleaned;
|
|
@@ -333,7 +380,7 @@ export function registerHooks(api, obfuscator) {
|
|
|
333
380
|
for (const b of msg.content) {
|
|
334
381
|
if (b?.type === "text" && typeof b.text === "string") {
|
|
335
382
|
const cleaned = stripSlackLinksForHook(b.text);
|
|
336
|
-
const result = ob().obfuscate(cleaned);
|
|
383
|
+
const result = ob().obfuscate(cleaned, undefined, _exemptCats);
|
|
337
384
|
totalEntities += result.entities.length;
|
|
338
385
|
if (result.entities.length > 0 || cleaned !== b.text) {
|
|
339
386
|
b.text = result.entities.length > 0 ? result.obfuscated : cleaned;
|
|
@@ -342,7 +389,7 @@ export function registerHooks(api, obfuscator) {
|
|
|
342
389
|
// tool_result blocks with string content
|
|
343
390
|
if (typeof b?.content === "string") {
|
|
344
391
|
const cleaned = stripSlackLinksForHook(b.content);
|
|
345
|
-
const result = ob().obfuscate(cleaned);
|
|
392
|
+
const result = ob().obfuscate(cleaned, undefined, _exemptCats);
|
|
346
393
|
totalEntities += result.entities.length;
|
|
347
394
|
if (result.entities.length > 0 || cleaned !== b.content) {
|
|
348
395
|
b.content = result.entities.length > 0 ? result.obfuscated : cleaned;
|
|
@@ -354,7 +401,7 @@ export function registerHooks(api, obfuscator) {
|
|
|
354
401
|
if (Array.isArray(msg.tool_calls)) {
|
|
355
402
|
for (const tc of msg.tool_calls) {
|
|
356
403
|
if (typeof tc.function?.arguments === "string") {
|
|
357
|
-
const result = ob().obfuscate(tc.function.arguments);
|
|
404
|
+
const result = ob().obfuscate(tc.function.arguments, undefined, _exemptCats);
|
|
358
405
|
totalEntities += result.entities.length;
|
|
359
406
|
if (result.entities.length > 0)
|
|
360
407
|
tc.function.arguments = result.obfuscated;
|
|
@@ -575,10 +622,15 @@ export function registerHooks(api, obfuscator) {
|
|
|
575
622
|
return;
|
|
576
623
|
// Exit tool depth
|
|
577
624
|
ob().exitToolCall();
|
|
578
|
-
|
|
625
|
+
// Resolve per-tool field scope
|
|
626
|
+
const toolName = event.toolName ?? "";
|
|
627
|
+
const resolver = getFieldScopeResolver();
|
|
628
|
+
const toolScope = resolver.resolveToolScope(toolName);
|
|
629
|
+
const shouldScan = (field) => resolver.shouldScanField(field, toolScope);
|
|
630
|
+
const obfuscated = walkStringsScoped(event.message, (s) => {
|
|
579
631
|
const result = ob().obfuscate(s);
|
|
580
632
|
return result.obfuscated;
|
|
581
|
-
});
|
|
633
|
+
}, shouldScan);
|
|
582
634
|
dumpStatsFile(obfuscator);
|
|
583
635
|
return { message: obfuscated };
|
|
584
636
|
});
|
package/dist/obfuscator.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export declare class Obfuscator {
|
|
|
55
55
|
* 6. Map and replace (with redaction level)
|
|
56
56
|
* 7. Inject canary if enabled
|
|
57
57
|
*/
|
|
58
|
-
obfuscate(text: string, context?: string): ObfuscationResult;
|
|
58
|
+
obfuscate(text: string, context?: string, exemptCategories?: Set<string>): ObfuscationResult;
|
|
59
59
|
/**
|
|
60
60
|
* Reverse-map fake values back to real values in text.
|
|
61
61
|
*
|
package/dist/obfuscator.js
CHANGED
|
@@ -303,7 +303,7 @@ export class Obfuscator {
|
|
|
303
303
|
* 6. Map and replace (with redaction level)
|
|
304
304
|
* 7. Inject canary if enabled
|
|
305
305
|
*/
|
|
306
|
-
obfuscate(text, context) {
|
|
306
|
+
obfuscate(text, context, exemptCategories) {
|
|
307
307
|
const startTime = Date.now();
|
|
308
308
|
// 0. Strip Slack/chat mrkdwn link formatting so detection sees clean text.
|
|
309
309
|
// Slack wraps emails as <mailto:X|DISPLAY> and URLs as <URL|DISPLAY>,
|
|
@@ -348,11 +348,16 @@ export class Obfuscator {
|
|
|
348
348
|
let belowThreshold = 0;
|
|
349
349
|
let allowlisted = 0;
|
|
350
350
|
let alreadyObfuscated = 0;
|
|
351
|
+
let exempted = 0;
|
|
351
352
|
const filtered = entities.filter((e) => {
|
|
352
353
|
if (e.confidence < this.config.minConfidence) {
|
|
353
354
|
belowThreshold++;
|
|
354
355
|
return false;
|
|
355
356
|
}
|
|
357
|
+
if (exemptCategories?.has(e.category)) {
|
|
358
|
+
exempted++;
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
356
361
|
if (allowExact.has(e.value) || allowWild.some((p) => wildcardMatch(e.value, p))) {
|
|
357
362
|
allowlisted++;
|
|
358
363
|
return false;
|
|
@@ -453,6 +458,7 @@ export class Obfuscator {
|
|
|
453
458
|
allowlisted,
|
|
454
459
|
docExamples: 0, // doc examples are filtered inside detectors before reaching here
|
|
455
460
|
alreadyObfuscated,
|
|
461
|
+
exempted,
|
|
456
462
|
};
|
|
457
463
|
this._obfuscationEvents++;
|
|
458
464
|
this._totalEntitiesObfuscated += filtered.length;
|
package/dist/types.d.ts
CHANGED
|
@@ -72,6 +72,32 @@ export interface FilterStats {
|
|
|
72
72
|
docExamples: number;
|
|
73
73
|
/** Entities skipped because they are already-known fakes. */
|
|
74
74
|
alreadyObfuscated: number;
|
|
75
|
+
/** Entities skipped because their category is exempt (per-agent contract). */
|
|
76
|
+
exempted: number;
|
|
77
|
+
}
|
|
78
|
+
/** Per-tool field rule: which fields to scan for obfuscation. */
|
|
79
|
+
export interface ToolFieldRule {
|
|
80
|
+
scanFields: string[];
|
|
81
|
+
}
|
|
82
|
+
/** Configuration for per-tool and per-agent obfuscation scoping. */
|
|
83
|
+
export interface FieldScopingConfig {
|
|
84
|
+
/** Per-tool field rules. Keys are tool name patterns (supports * and ? wildcards). */
|
|
85
|
+
toolFields: Record<string, ToolFieldRule>;
|
|
86
|
+
/** Fields that are NEVER scanned regardless of tool. */
|
|
87
|
+
neverScanFields: string[];
|
|
88
|
+
/** Default fields to scan for tools not matching any pattern. Empty = scan everything. */
|
|
89
|
+
defaultScanFields: string[];
|
|
90
|
+
/** When true, use agent contract allowedDataClasses to exempt categories from obfuscation. */
|
|
91
|
+
useContractExemptions: boolean;
|
|
92
|
+
}
|
|
93
|
+
/** Result of resolving field scope for a tool. */
|
|
94
|
+
export interface ScopeDecision {
|
|
95
|
+
/** "all" = scan every field; "selected" = only scan scanFields. */
|
|
96
|
+
mode: "all" | "selected";
|
|
97
|
+
/** Fields to scan (when mode is "selected"). */
|
|
98
|
+
scanFields: Set<string>;
|
|
99
|
+
/** Fields to never scan (always applied). */
|
|
100
|
+
neverScanFields: Set<string>;
|
|
75
101
|
}
|
|
76
102
|
/** Configuration for the Shroud plugin. */
|
|
77
103
|
export interface ShroudConfig {
|
|
@@ -120,4 +146,6 @@ export interface ShroudConfig {
|
|
|
120
146
|
dryRun: boolean;
|
|
121
147
|
/** Max mapping store size; oldest entries evicted when exceeded. 0 = unlimited. */
|
|
122
148
|
maxStoreMappings: number;
|
|
149
|
+
/** Per-tool and per-agent obfuscation scoping. Undefined = scan everything (backward compatible). */
|
|
150
|
+
fieldScoping?: FieldScopingConfig;
|
|
123
151
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shroud-privacy",
|
|
3
3
|
"name": "Shroud",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.1",
|
|
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.
|
|
3
|
+
"version": "2.5.1",
|
|
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",
|