shroud-privacy 2.2.9 → 2.2.11
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 +33 -11
- package/dist/agent-session.d.ts +259 -0
- package/dist/agent-session.js +693 -0
- package/dist/dashboard.d.ts +42 -0
- package/dist/dashboard.js +1558 -0
- package/dist/detectors/context.js +7 -0
- package/dist/detectors/injection-multilingual.d.ts +27 -0
- package/dist/detectors/injection-multilingual.js +399 -0
- package/dist/detectors/injection-signatures.d.ts +26 -0
- package/dist/detectors/injection-signatures.js +508 -0
- package/dist/detectors/injection.d.ts +56 -0
- package/dist/detectors/injection.js +269 -0
- package/dist/detectors/regex.js +2 -2
- package/dist/detectors/tool-guard.d.ts +27 -0
- package/dist/detectors/tool-guard.js +418 -0
- package/dist/event-grader.d.ts +97 -0
- package/dist/event-grader.js +214 -0
- package/dist/policy.d.ts +93 -43
- package/dist/policy.js +193 -86
- package/dist/profiler-analysis.d.ts +35 -0
- package/dist/profiler-analysis.js +230 -0
- package/dist/profiler-store.d.ts +33 -0
- package/dist/profiler-store.js +118 -0
- package/dist/profiler-types.d.ts +128 -0
- package/dist/profiler-types.js +16 -0
- package/dist/profiler.d.ts +81 -0
- package/dist/profiler.js +392 -0
- package/dist/security-event.d.ts +70 -0
- package/dist/security-event.js +80 -0
- package/dist/siem.d.ts +38 -24
- package/dist/siem.js +90 -68
- package/dist/signature-loader.d.ts +113 -0
- package/dist/signature-loader.js +255 -0
- package/openclaw.plugin.json +155 -30
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -77,16 +77,16 @@ Shroud does not guarantee compliance — regex-based detection has limitations (
|
|
|
77
77
|
|
|
78
78
|
> **How it works:** Shroud intercepts ALL outbound LLM API calls (Anthropic, OpenAI, Google, any provider) at the `fetch` level and obfuscates detected entities in every message — including assistant history and Slack `<mailto:>` markup — before it leaves the process. On the response side, SSE streaming is deobfuscated per content block with buffered flushing. Every delivery path (Slack, WhatsApp, TUI, Telegram, Discord, Signal, web) gets real text automatically. Zero host patches required.
|
|
79
79
|
|
|
80
|
-
> **Requires OpenClaw 2026.3.
|
|
80
|
+
> **Requires OpenClaw 2026.3.22 or later.**
|
|
81
81
|
|
|
82
82
|
---
|
|
83
83
|
|
|
84
84
|
## Install
|
|
85
85
|
|
|
86
|
-
### OpenClaw (2026.3.
|
|
86
|
+
### OpenClaw (2026.3.22+)
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
openclaw --version # ensure 2026.3.
|
|
89
|
+
openclaw --version # ensure 2026.3.22+
|
|
90
90
|
openclaw plugins install shroud-privacy
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -127,7 +127,7 @@ node node_modules/shroud-privacy/app-server.mjs node_modules/shroud-privacy/dist
|
|
|
127
127
|
|
|
128
128
|
Handshake (server writes on startup):
|
|
129
129
|
```json
|
|
130
|
-
{"app":"1.0","engine":"shroud","version":"2.2.
|
|
130
|
+
{"app":"1.0","engine":"shroud","version":"2.2.9","capabilities":["obfuscate","deobfuscate","batch","stats","health","configure","audit","partitions"]}
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
Obfuscate:
|
|
@@ -288,7 +288,7 @@ Shroud includes a `ContextDetector` that wraps the regex engine with post-detect
|
|
|
288
288
|
|
|
289
289
|
- **Context-aware boosting**: Text blocks containing config keywords (`interface`, `router ospf`, `hostname`) get +10% confidence for detected entities.
|
|
290
290
|
- **Proximity clustering**: When a name, email, and phone appear within 200 characters, each gets a confidence boost.
|
|
291
|
-
- **Hostname propagation**: `hostname
|
|
291
|
+
- **Hostname propagation**: `hostname CORE-RTR-01` in one place → bare `CORE-RTR-01` detected everywhere in the text.
|
|
292
292
|
- **Learned entities**: Hostnames and infra identifiers seen in previous messages are remembered and detected in future messages without requiring config-line context.
|
|
293
293
|
- **Documentation filtering**: RFC 3849 IPv6 doc prefix (`2001:db8::/32`), IPv6 loopback (`::1`), `example.com` emails, and well-known placeholders are automatically skipped.
|
|
294
294
|
- **DNS-based URL classification**: External URLs pass through to the LLM; internal URLs are obfuscated. See [URL handling](#url-handling).
|
|
@@ -406,14 +406,36 @@ client.stop()
|
|
|
406
406
|
|
|
407
407
|
```bash
|
|
408
408
|
npm install
|
|
409
|
-
npm
|
|
410
|
-
npm run
|
|
411
|
-
npm
|
|
412
|
-
npm run test:
|
|
413
|
-
npm run
|
|
414
|
-
npm run lint # type-check without emitting
|
|
409
|
+
npm run build # compile TypeScript
|
|
410
|
+
npm run lint # type-check without emitting
|
|
411
|
+
npm test # unit + harness (1,229 tests, no Docker)
|
|
412
|
+
npm run test:docker # Docker E2E — real OpenClaw, all channels (192 tests)
|
|
413
|
+
npm run test:all # everything (1,421 tests)
|
|
415
414
|
```
|
|
416
415
|
|
|
416
|
+
### Test layers
|
|
417
|
+
|
|
418
|
+
| Layer | Command | Tests | What it covers |
|
|
419
|
+
|-------|---------|-------|---------------|
|
|
420
|
+
| Unit | `npm run test:unit` | 870 | Obfuscator, detectors, generators, store, config |
|
|
421
|
+
| APP Harness | `npm run test:integration` | 359 | 48 scenario files via mock LLM, no OpenClaw |
|
|
422
|
+
| Docker E2E | `npm run test:docker` | 192 | Real OpenClaw gateway, Slack/WhatsApp/Cron/TUI channels, 153 regression scenarios |
|
|
423
|
+
| Sandbox E2E | `run-compat.sh <ver> --sandbox` | +8 | Docker-in-Docker, sandboxed agent exec, tool call deobfuscation |
|
|
424
|
+
|
|
425
|
+
Docker E2E runs inside an isolated container (`--internal` network, no external routing). Both OpenClaw and Shroud are installed from npm — the same path real users take. A single gateway process handles all tests via WebSocket RPC. Channel tests use mock servers with real SDK code paths (Slack via Bolt HTTP, WhatsApp via Baileys intercept).
|
|
426
|
+
|
|
427
|
+
### OpenClaw compatibility matrix
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
bash compat/run-compat.sh latest # test against latest OpenClaw
|
|
431
|
+
bash compat/run-compat.sh latest --sandbox # include sandboxed agent exec tests
|
|
432
|
+
bash compat/run-matrix.sh # interactive: current or current + last 3
|
|
433
|
+
bash compat/run-matrix.sh --latest 3 # latest 3 versions
|
|
434
|
+
bash compat/run-matrix.sh --parallel # parallel execution
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Supported versions are tracked in `compat/versions.json`. CI checks for new OpenClaw releases daily.
|
|
438
|
+
|
|
417
439
|
---
|
|
418
440
|
|
|
419
441
|
## Disclaimer
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent session tracking — maps LLM API calls to local agent identities.
|
|
3
|
+
*
|
|
4
|
+
* Each agent has a unique identity derived from its system prompt, plugin set,
|
|
5
|
+
* and model. This module tracks which agent is making each LLM call, enabling:
|
|
6
|
+
* - Per-agent WAF rules (different injection policies per agent)
|
|
7
|
+
* - Per-agent behavioural baselines (Track 3)
|
|
8
|
+
* - Per-agent canary attribution (Track 2)
|
|
9
|
+
* - Multi-agent session correlation
|
|
10
|
+
*/
|
|
11
|
+
/** A logged LLM API call. */
|
|
12
|
+
export interface LlmCallRecord {
|
|
13
|
+
timestamp: number;
|
|
14
|
+
agentLabel: string;
|
|
15
|
+
url: string;
|
|
16
|
+
model: string;
|
|
17
|
+
inputTokens: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
cacheReadTokens: number;
|
|
20
|
+
cacheWriteTokens: number;
|
|
21
|
+
cacheHitPct: number;
|
|
22
|
+
responseTimeMs: number;
|
|
23
|
+
channel: string;
|
|
24
|
+
securityEvents: number;
|
|
25
|
+
/** Why the call was made (slack message, heartbeat, cron, tool call, etc.) */
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
/** Agent role classification derived from name, channel, and behaviour. */
|
|
29
|
+
export interface AgentClassification {
|
|
30
|
+
/** Primary role category. */
|
|
31
|
+
role: string;
|
|
32
|
+
/** Confidence percentage (0-100). */
|
|
33
|
+
confidencePct: number;
|
|
34
|
+
/** Confidence tier for display. */
|
|
35
|
+
confidence: "high" | "medium" | "low";
|
|
36
|
+
/** Colour code for dashboard rendering. */
|
|
37
|
+
colour: string;
|
|
38
|
+
/** Keywords that triggered the classification. */
|
|
39
|
+
signals: string[];
|
|
40
|
+
}
|
|
41
|
+
/** Agent health and behavioural compliance status. */
|
|
42
|
+
export interface AgentHealth {
|
|
43
|
+
/** Overall health: "healthy", "warning", "critical". */
|
|
44
|
+
status: "healthy" | "warning" | "critical";
|
|
45
|
+
/** Health colour for dashboard. */
|
|
46
|
+
colour: string;
|
|
47
|
+
/** Is the agent behaving according to its classification? */
|
|
48
|
+
compliant: boolean;
|
|
49
|
+
/** Compliance detail messages. */
|
|
50
|
+
issues: string[];
|
|
51
|
+
/** Last active relative indicator. */
|
|
52
|
+
lastActiveAgo: string;
|
|
53
|
+
/** Security event rate per 100 calls. */
|
|
54
|
+
eventRate: number;
|
|
55
|
+
}
|
|
56
|
+
/** Represents a tracked agent session. */
|
|
57
|
+
export interface AgentSession {
|
|
58
|
+
/** Stable identity hash: SHA256(systemPrompt + pluginList + modelId). */
|
|
59
|
+
agentBuildId: string;
|
|
60
|
+
/** Human-readable label extracted from system prompt (first 60 chars). */
|
|
61
|
+
agentLabel: string;
|
|
62
|
+
/** Session-scoped unique ID. */
|
|
63
|
+
sessionId: string;
|
|
64
|
+
/** When this session was first seen. */
|
|
65
|
+
startedAt: number;
|
|
66
|
+
/** Total LLM API calls made by this agent session. */
|
|
67
|
+
llmCallCount: number;
|
|
68
|
+
/** Total security events attributed to this agent. */
|
|
69
|
+
securityEventCount: number;
|
|
70
|
+
/** Last LLM call timestamp. */
|
|
71
|
+
lastCallAt: number;
|
|
72
|
+
/** LLM model ID detected from API calls (e.g. "claude-3-opus", "gpt-4"). */
|
|
73
|
+
detectedModel: string;
|
|
74
|
+
/** Channel source if detected (e.g. "slack:C00000001", "whatsapp:+353..."). */
|
|
75
|
+
channelSource: string;
|
|
76
|
+
/** Inferred role classification. */
|
|
77
|
+
classification: AgentClassification;
|
|
78
|
+
/** Tool names available to the agent (from body.tools). */
|
|
79
|
+
toolInventory: string[];
|
|
80
|
+
/** SOUL.md extract — agent's core identity/instructions from early messages. */
|
|
81
|
+
soulExtract: string;
|
|
82
|
+
/** Per-agent LLM cache stats. */
|
|
83
|
+
cache: AgentCacheStats;
|
|
84
|
+
/** Active channels this agent has been seen on. */
|
|
85
|
+
channels: string[];
|
|
86
|
+
/** Heartbeat tracking. */
|
|
87
|
+
heartbeat: AgentHeartbeat;
|
|
88
|
+
}
|
|
89
|
+
/** Per-agent heartbeat tracking. */
|
|
90
|
+
export interface AgentHeartbeat {
|
|
91
|
+
/** Whether heartbeat has been detected for this agent. */
|
|
92
|
+
enabled: boolean;
|
|
93
|
+
/** Timestamps of recent heartbeats (last 10). */
|
|
94
|
+
recent: number[];
|
|
95
|
+
/** Average interval between heartbeats (ms). -1 if not enough data. */
|
|
96
|
+
avgIntervalMs: number;
|
|
97
|
+
/** Last heartbeat timestamp. */
|
|
98
|
+
lastAt: number;
|
|
99
|
+
/** Status: "alive", "stale" (2x interval missed), "dead" (5x missed). */
|
|
100
|
+
status: "alive" | "stale" | "dead" | "unknown";
|
|
101
|
+
/** Last heartbeat response (HEARTBEAT_OK or alert text). */
|
|
102
|
+
lastResponse: string;
|
|
103
|
+
}
|
|
104
|
+
/** Per-agent LLM cache tracking for anomaly detection. */
|
|
105
|
+
export interface AgentCacheStats {
|
|
106
|
+
totalInputTokens: number;
|
|
107
|
+
totalOutputTokens: number;
|
|
108
|
+
totalCacheRead: number;
|
|
109
|
+
totalCacheWrite: number;
|
|
110
|
+
/** Running average cache hit ratio (0-1). */
|
|
111
|
+
avgHitRatio: number;
|
|
112
|
+
/** Baseline hit ratio (from first N calls). -1 if not established. */
|
|
113
|
+
baselineHitRatio: number;
|
|
114
|
+
/** Number of calls contributing to the baseline. */
|
|
115
|
+
baselineSamples: number;
|
|
116
|
+
/** Number of calls with cache data. */
|
|
117
|
+
callsWithCache: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Tracks agent sessions and maps LLM calls to agent identities.
|
|
121
|
+
* One instance shared via globalThis across all plugin loads.
|
|
122
|
+
*/
|
|
123
|
+
export declare class AgentSessionTracker {
|
|
124
|
+
/** Active sessions keyed by agent label (the stable identity). */
|
|
125
|
+
private _sessions;
|
|
126
|
+
/** Current active agent label. */
|
|
127
|
+
private _currentLabel;
|
|
128
|
+
/** LLM call log (ring buffer, last 200 calls). */
|
|
129
|
+
private _callLog;
|
|
130
|
+
/** Timestamp when the current LLM call started (for response time). */
|
|
131
|
+
private _callStartTime;
|
|
132
|
+
/**
|
|
133
|
+
* Register or update an agent session from system prompt content.
|
|
134
|
+
*
|
|
135
|
+
* Identity strategy: the extracted LABEL is the primary key, not the
|
|
136
|
+
* prompt skeleton hash. System prompts contain too much dynamic content
|
|
137
|
+
* (conversation context, RAG, tool results) to produce stable hashes.
|
|
138
|
+
* The label — extracted from "- Name: X", "You are X", etc. — is the
|
|
139
|
+
* stable identity that humans recognise.
|
|
140
|
+
*
|
|
141
|
+
* The buildId is still computed for fingerprinting but is NOT used as
|
|
142
|
+
* the session key.
|
|
143
|
+
*/
|
|
144
|
+
registerAgent(systemPrompt: string, pluginList?: string[], modelId?: string): AgentSession;
|
|
145
|
+
/** Update detected model from LLM API request body. */
|
|
146
|
+
updateModel(model: string): void;
|
|
147
|
+
/** Update channel source (e.g. "slack:C00000001"). */
|
|
148
|
+
updateChannel(source: string): void;
|
|
149
|
+
/**
|
|
150
|
+
* Update per-agent cache stats from an LLM response.
|
|
151
|
+
* Returns anomaly alerts if cache behaviour deviates from baseline.
|
|
152
|
+
*/
|
|
153
|
+
updateCache(usage: {
|
|
154
|
+
inputTokens: number;
|
|
155
|
+
outputTokens: number;
|
|
156
|
+
cacheReadTokens: number;
|
|
157
|
+
cacheWriteTokens: number;
|
|
158
|
+
}): {
|
|
159
|
+
alert: string;
|
|
160
|
+
severity: "medium" | "high";
|
|
161
|
+
} | null;
|
|
162
|
+
/** Record a heartbeat for the current agent. Returns alert if missed. */
|
|
163
|
+
recordHeartbeat(response?: string): {
|
|
164
|
+
alert: string;
|
|
165
|
+
severity: "medium" | "high";
|
|
166
|
+
} | null;
|
|
167
|
+
/** Check all agents for missed heartbeats. Call periodically. */
|
|
168
|
+
checkHeartbeatHealth(): Array<{
|
|
169
|
+
agentLabel: string;
|
|
170
|
+
status: string;
|
|
171
|
+
alert: string;
|
|
172
|
+
}>;
|
|
173
|
+
/** Detect and record the channel from prompt metadata. */
|
|
174
|
+
updateChannelFromPrompt(prompt: string): string | null;
|
|
175
|
+
/** Update tool inventory from body.tools array. Only sets once (first call). */
|
|
176
|
+
updateTools(tools: string[]): void;
|
|
177
|
+
/** Update SOUL extract from early messages. Only sets once. */
|
|
178
|
+
updateSoul(soul: string): void;
|
|
179
|
+
/** Record an LLM API call for the current agent. */
|
|
180
|
+
recordLlmCall(): AgentSession | null;
|
|
181
|
+
/** Record a security event for the current agent. */
|
|
182
|
+
recordSecurityEvent(count?: number): void;
|
|
183
|
+
/** Get the current active agent session. */
|
|
184
|
+
getCurrentSession(): AgentSession | null;
|
|
185
|
+
/** Get the current agent build ID. */
|
|
186
|
+
getCurrentBuildId(): string;
|
|
187
|
+
/** Get all tracked agent sessions. */
|
|
188
|
+
getAllSessions(): AgentSession[];
|
|
189
|
+
/** Get session by build ID. */
|
|
190
|
+
getSession(buildId: string): AgentSession | null;
|
|
191
|
+
/** Get session by label (primary key). */
|
|
192
|
+
getSessionByLabel(label: string): AgentSession | null;
|
|
193
|
+
/** Mark the start of an LLM call (for response time tracking). */
|
|
194
|
+
markCallStart(): void;
|
|
195
|
+
/** Log a completed LLM call with full details. */
|
|
196
|
+
logCall(details: {
|
|
197
|
+
url: string;
|
|
198
|
+
model: string;
|
|
199
|
+
inputTokens: number;
|
|
200
|
+
outputTokens: number;
|
|
201
|
+
cacheReadTokens: number;
|
|
202
|
+
cacheWriteTokens: number;
|
|
203
|
+
channel: string;
|
|
204
|
+
securityEvents: number;
|
|
205
|
+
reason: string;
|
|
206
|
+
}): void;
|
|
207
|
+
/** Get the LLM call log. */
|
|
208
|
+
getCallLog(): readonly LlmCallRecord[];
|
|
209
|
+
/** Reset all session tracking. */
|
|
210
|
+
reset(): void;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Compute a stable agent build ID.
|
|
214
|
+
*
|
|
215
|
+
* Uses a "skeleton" of the system prompt rather than the full text.
|
|
216
|
+
* This makes the ID resilient to:
|
|
217
|
+
* - Dynamic timestamps, dates, session IDs injected into prompts
|
|
218
|
+
* - User names or account-specific context
|
|
219
|
+
* - Retrieved RAG snippets appended to the base prompt
|
|
220
|
+
* - Minor wording tweaks during prompt iteration
|
|
221
|
+
*
|
|
222
|
+
* The skeleton is: first 500 chars of the prompt with numbers, dates,
|
|
223
|
+
* emails, UUIDs, and hex strings normalized to placeholders.
|
|
224
|
+
*/
|
|
225
|
+
export declare function computeBuildId(systemPrompt: string, pluginList: string[], modelId: string): string;
|
|
226
|
+
/**
|
|
227
|
+
* Extract a stable "skeleton" from a system prompt by normalizing
|
|
228
|
+
* dynamic content to placeholders.
|
|
229
|
+
*
|
|
230
|
+
* Normalizes: timestamps, dates, numbers >4 digits, emails, UUIDs,
|
|
231
|
+
* hex strings >8 chars, IP addresses, URLs with path components.
|
|
232
|
+
* Keeps: the structural words, role definitions, tool descriptions,
|
|
233
|
+
* behavioral instructions — the parts that define the agent's identity.
|
|
234
|
+
*/
|
|
235
|
+
export declare function extractPromptSkeleton(prompt: string): string;
|
|
236
|
+
/**
|
|
237
|
+
* Classify an agent's role from its label and system prompt content.
|
|
238
|
+
* Uses keyword matching against a role taxonomy — no LLM call needed.
|
|
239
|
+
*
|
|
240
|
+
* Confidence scoring:
|
|
241
|
+
* 90% — role keyword in agent label (explicit naming)
|
|
242
|
+
* 70% — role keyword in SOUL.md "You are a [role]" declaration
|
|
243
|
+
* 40% — role keyword found in general prompt metadata
|
|
244
|
+
* 10% — no match, "General Agent"
|
|
245
|
+
*
|
|
246
|
+
* Multiple signal matches boost confidence by 5% each (capped at 95%).
|
|
247
|
+
*/
|
|
248
|
+
export declare function classifyAgent(label: string, systemPrompt: string): AgentClassification;
|
|
249
|
+
/**
|
|
250
|
+
* Enhanced classifier that uses tools + SOUL.md + label.
|
|
251
|
+
* Called when new data (tools or SOUL) becomes available.
|
|
252
|
+
*/
|
|
253
|
+
export declare function classifyAgentWithTools(label: string, soulExtract: string, tools: string[]): AgentClassification;
|
|
254
|
+
/** Detect the channel type from OpenClaw prompt metadata. */
|
|
255
|
+
export declare function detectChannel(prompt: string): string | null;
|
|
256
|
+
/** Check if a prompt is a heartbeat prompt. */
|
|
257
|
+
export declare function isHeartbeatPrompt(prompt: string): boolean;
|
|
258
|
+
/** Check if a response is a heartbeat OK response. */
|
|
259
|
+
export declare function isHeartbeatOk(response: string): boolean;
|