@upx-us/shield 0.6.10 → 0.7.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/CHANGELOG.md +24 -0
- package/README.md +19 -1
- package/dist/src/attributor.d.ts +15 -0
- package/dist/src/attributor.js +257 -0
- package/dist/src/cli-cases.js +34 -5
- package/dist/src/events/base.d.ts +13 -0
- package/dist/src/index.js +1 -1
- package/dist/src/transformer.d.ts +1 -1
- package/dist/src/transformer.js +83 -1
- package/dist/src/validator.js +48 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/shield/SKILL.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.7.1] — 2026-03-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Case ownership awareness: `shield cases` now shows which cases belong to this instance vs other instances in the org
|
|
11
|
+
- `--mine` flag for `shield cases` and `shield cases list` to filter to locally-owned cases
|
|
12
|
+
- Sibling case detail view: `shield cases show <ID>` notes when a case belongs to another org instance and clarifies that remote investigation requires direct platform access
|
|
13
|
+
- Attribution summary line when listing mixed-ownership cases
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [0.7.0] — 2026-03-13
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Event authorship & source attribution** — Shield now captures the source of every monitored agent action: who or what triggered it. Each event is tagged with an authorship category — a user via a messaging channel (Telegram, Discord, WhatsApp, etc.), a scheduled job, a periodic health check, a spawned sub-agent, or an autonomous agent action. This context appears alongside every event in your security dashboard, enabling "who caused this?" forensics on any alert or case.
|
|
21
|
+
- **Sender identity captured when available** — when an action is triggered by a user via a messaging channel, the user's display name and channel are captured in plain text for human-readable alerting. The raw user identifier is stored as a reversible token (recoverable locally via `openclaw shield vault show`).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **README updated** — new "Authorship & Event Source" section documents the authorship categories available in the dashboard.
|
|
25
|
+
|
|
26
|
+
### Notes
|
|
27
|
+
- Fully backward compatible. Existing deployments continue to work without any configuration changes. Authorship context is captured automatically on upgrade.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
7
31
|
## [0.6.10] — 2026-03-12
|
|
8
32
|
|
|
9
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -350,6 +350,24 @@ Shield captures agent activity locally, applies on-device redaction, and forward
|
|
|
350
350
|
|
|
351
351
|
---
|
|
352
352
|
|
|
353
|
+
## Authorship & Event Source
|
|
354
|
+
|
|
355
|
+
Shield captures the **source** of every event — who or what triggered the agent action being monitored.
|
|
356
|
+
|
|
357
|
+
Each event is tagged with an authorship category:
|
|
358
|
+
|
|
359
|
+
| Category | Description |
|
|
360
|
+
|---|---|
|
|
361
|
+
| Communication channel | A user interacted with the agent via a messaging platform (e.g. Telegram, Discord, WhatsApp) |
|
|
362
|
+
| Scheduled job | The action was triggered by a scheduled or automated task |
|
|
363
|
+
| Heartbeat | The action was triggered by a periodic health check |
|
|
364
|
+
| Sub-agent | The action originated from a child agent spawned by a parent |
|
|
365
|
+
| Autonomous | The agent acted on its own initiative without a direct human trigger |
|
|
366
|
+
|
|
367
|
+
This information appears alongside every event in your security dashboard, enabling you to answer **"who caused this?"** for any alert or case.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
353
371
|
## What is sent to the platform
|
|
354
372
|
|
|
355
373
|
Shield uses two separate channels with different privacy properties:
|
|
@@ -514,7 +532,7 @@ This removes all local files Shield writes at runtime:
|
|
|
514
532
|
|
|
515
533
|
| File | Contents |
|
|
516
534
|
|---|---|
|
|
517
|
-
| `config.env` |
|
|
535
|
+
| `config.env` | Signing key and instance fingerprint |
|
|
518
536
|
| `data/cursor.json` | Event cursor positions per log source |
|
|
519
537
|
| `data/public-ip.cache` | Cached public IP for telemetry |
|
|
520
538
|
| `data/instance-cache.json` | Cached instance metadata |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type TriggerType = 'user_message' | 'cron' | 'heartbeat' | 'subagent' | 'autonomous' | 'unknown';
|
|
2
|
+
export declare const TRIGGER_TYPES: readonly TriggerType[];
|
|
3
|
+
export interface AttributionContext {
|
|
4
|
+
trigger_type: TriggerType;
|
|
5
|
+
author_name?: string;
|
|
6
|
+
author_channel?: string;
|
|
7
|
+
author_hash?: string;
|
|
8
|
+
prompt_hash?: string;
|
|
9
|
+
session_label?: string;
|
|
10
|
+
parent_agent_id?: string;
|
|
11
|
+
cron_schedule?: string;
|
|
12
|
+
conversation_depth?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function clearAttributionCache(): void;
|
|
15
|
+
export declare function resolveAttribution(sessionId: string, agentId: string, sessionDir: string): AttributionContext;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TRIGGER_TYPES = void 0;
|
|
37
|
+
exports.clearAttributionCache = clearAttributionCache;
|
|
38
|
+
exports.resolveAttribution = resolveAttribution;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const vault_1 = require("./redactor/vault");
|
|
42
|
+
exports.TRIGGER_TYPES = [
|
|
43
|
+
'user_message', 'cron', 'heartbeat', 'subagent', 'autonomous', 'unknown',
|
|
44
|
+
];
|
|
45
|
+
const _attributionCache = new Map();
|
|
46
|
+
function clearAttributionCache() {
|
|
47
|
+
_attributionCache.clear();
|
|
48
|
+
}
|
|
49
|
+
const MAX_SCAN_LINES = 50;
|
|
50
|
+
const READ_BUF_SIZE = 32768;
|
|
51
|
+
function readFirstLines(filePath, maxLines) {
|
|
52
|
+
if (!fs.existsSync(filePath))
|
|
53
|
+
return [];
|
|
54
|
+
let fd = null;
|
|
55
|
+
try {
|
|
56
|
+
fd = fs.openSync(filePath, 'r');
|
|
57
|
+
const buf = Buffer.alloc(READ_BUF_SIZE);
|
|
58
|
+
const bytesRead = fs.readSync(fd, buf, 0, READ_BUF_SIZE, 0);
|
|
59
|
+
const text = buf.toString('utf8', 0, bytesRead);
|
|
60
|
+
return text
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map(l => l.trim())
|
|
63
|
+
.filter(l => l.length > 0)
|
|
64
|
+
.slice(0, maxLines);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (fd !== null) {
|
|
71
|
+
try {
|
|
72
|
+
fs.closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function extractSenderBlock(text) {
|
|
79
|
+
const match = text.match(/Sender \(untrusted metadata\):\s*```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
80
|
+
if (!match)
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(match[1]);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function extractConversationBlock(text) {
|
|
90
|
+
const match = text.match(/Conversation info \(untrusted metadata\):\s*```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
91
|
+
if (!match)
|
|
92
|
+
return null;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(match[1]);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function inferChannel(block) {
|
|
101
|
+
if (!block)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (block.channel)
|
|
104
|
+
return block.channel;
|
|
105
|
+
const label = block.conversation_label || '';
|
|
106
|
+
if (!label)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (/id:-\d+/.test(label))
|
|
109
|
+
return 'telegram';
|
|
110
|
+
if (/discord/i.test(label))
|
|
111
|
+
return 'discord';
|
|
112
|
+
if (/whatsapp/i.test(label))
|
|
113
|
+
return 'whatsapp';
|
|
114
|
+
if (/slack/i.test(label))
|
|
115
|
+
return 'slack';
|
|
116
|
+
if (/signal/i.test(label))
|
|
117
|
+
return 'signal';
|
|
118
|
+
if (/imessage/i.test(label))
|
|
119
|
+
return 'imessage';
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const HEARTBEAT_PATTERN = /\bHEARTBEAT_OK\b|\bRead HEARTBEAT\.md\b|\bheartbeat\b/i;
|
|
123
|
+
const CRON_PATTERN = /\bcron\b|\bscheduled job\b|\bscheduled task\b/i;
|
|
124
|
+
const SCHEDULE_EXPR_RE = /(?:\*\/\d+|\d+|\*)\s+(?:\*\/\d+|\d+|\*)\s+(?:\*\/\d+|\d+|\*)\s+(?:\*\/\d+|\d+|\*)\s+(?:\*\/\d+|\d+|\*)/;
|
|
125
|
+
const SUBAGENT_PATTERN = /subagent|sub-agent/i;
|
|
126
|
+
function extractCronSchedule(text) {
|
|
127
|
+
const m = text.match(SCHEDULE_EXPR_RE);
|
|
128
|
+
return m ? m[0].trim() : undefined;
|
|
129
|
+
}
|
|
130
|
+
function resolveAttribution(sessionId, agentId, sessionDir) {
|
|
131
|
+
const cached = _attributionCache.get(sessionId);
|
|
132
|
+
if (cached)
|
|
133
|
+
return cached;
|
|
134
|
+
const filePath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
135
|
+
const lines = readFirstLines(filePath, MAX_SCAN_LINES);
|
|
136
|
+
if (lines.length === 0) {
|
|
137
|
+
return { trigger_type: 'unknown' };
|
|
138
|
+
}
|
|
139
|
+
let sessionLabel;
|
|
140
|
+
let parentAgentId;
|
|
141
|
+
try {
|
|
142
|
+
const first = JSON.parse(lines[0]);
|
|
143
|
+
if (first.type === 'session') {
|
|
144
|
+
if (first.label)
|
|
145
|
+
sessionLabel = String(first.label);
|
|
146
|
+
if (first.parentAgentId)
|
|
147
|
+
parentAgentId = String(first.parentAgentId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch { }
|
|
151
|
+
if ((sessionLabel && SUBAGENT_PATTERN.test(sessionLabel)) ||
|
|
152
|
+
SUBAGENT_PATTERN.test(sessionId) ||
|
|
153
|
+
(parentAgentId != null)) {
|
|
154
|
+
const ctx = {
|
|
155
|
+
trigger_type: 'subagent',
|
|
156
|
+
session_label: sessionLabel,
|
|
157
|
+
parent_agent_id: parentAgentId || agentId,
|
|
158
|
+
};
|
|
159
|
+
_attributionCache.set(sessionId, ctx);
|
|
160
|
+
return ctx;
|
|
161
|
+
}
|
|
162
|
+
let userMessageCount = 0;
|
|
163
|
+
let firstUserText;
|
|
164
|
+
let senderBlock = null;
|
|
165
|
+
let conversationBlock = null;
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
let entry;
|
|
168
|
+
try {
|
|
169
|
+
entry = JSON.parse(line);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (entry.type !== 'message')
|
|
175
|
+
continue;
|
|
176
|
+
const msg = entry.message;
|
|
177
|
+
if (!msg || msg.role !== 'user')
|
|
178
|
+
continue;
|
|
179
|
+
userMessageCount++;
|
|
180
|
+
const textContent = Array.isArray(msg.content)
|
|
181
|
+
? msg.content
|
|
182
|
+
.filter((c) => c.type === 'text')
|
|
183
|
+
.map((c) => String(c.text || ''))
|
|
184
|
+
.join('\n')
|
|
185
|
+
: typeof msg.content === 'string'
|
|
186
|
+
? msg.content
|
|
187
|
+
: '';
|
|
188
|
+
if (!firstUserText)
|
|
189
|
+
firstUserText = textContent;
|
|
190
|
+
if (!senderBlock)
|
|
191
|
+
senderBlock = extractSenderBlock(textContent);
|
|
192
|
+
if (!conversationBlock)
|
|
193
|
+
conversationBlock = extractConversationBlock(textContent);
|
|
194
|
+
if (senderBlock && conversationBlock)
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (userMessageCount === 0) {
|
|
198
|
+
const ctx = {
|
|
199
|
+
trigger_type: 'autonomous',
|
|
200
|
+
session_label: sessionLabel,
|
|
201
|
+
};
|
|
202
|
+
_attributionCache.set(sessionId, ctx);
|
|
203
|
+
return ctx;
|
|
204
|
+
}
|
|
205
|
+
if (firstUserText && HEARTBEAT_PATTERN.test(firstUserText)) {
|
|
206
|
+
const ctx = {
|
|
207
|
+
trigger_type: 'heartbeat',
|
|
208
|
+
session_label: sessionLabel,
|
|
209
|
+
conversation_depth: userMessageCount,
|
|
210
|
+
};
|
|
211
|
+
_attributionCache.set(sessionId, ctx);
|
|
212
|
+
return ctx;
|
|
213
|
+
}
|
|
214
|
+
if (firstUserText && (CRON_PATTERN.test(firstUserText) || SCHEDULE_EXPR_RE.test(firstUserText))) {
|
|
215
|
+
const cronSchedule = extractCronSchedule(firstUserText);
|
|
216
|
+
const ctx = {
|
|
217
|
+
trigger_type: 'cron',
|
|
218
|
+
session_label: sessionLabel,
|
|
219
|
+
cron_schedule: cronSchedule,
|
|
220
|
+
conversation_depth: userMessageCount,
|
|
221
|
+
};
|
|
222
|
+
_attributionCache.set(sessionId, ctx);
|
|
223
|
+
return ctx;
|
|
224
|
+
}
|
|
225
|
+
if (senderBlock) {
|
|
226
|
+
const authorName = senderBlock.label || senderBlock.name || senderBlock.username;
|
|
227
|
+
const authorChannel = inferChannel(conversationBlock) ?? (senderBlock.channel);
|
|
228
|
+
const rawId = senderBlock.id != null
|
|
229
|
+
? String(senderBlock.id)
|
|
230
|
+
: senderBlock.username || authorName;
|
|
231
|
+
const authorIdHash = rawId ? (0, vault_1.hmacHash)('trigger.author_id', rawId) : undefined;
|
|
232
|
+
const promptHash = firstUserText ? (0, vault_1.hmacHash)('trigger.prompt', firstUserText) : undefined;
|
|
233
|
+
const ctx = {
|
|
234
|
+
trigger_type: 'user_message',
|
|
235
|
+
author_name: authorName,
|
|
236
|
+
author_channel: authorChannel,
|
|
237
|
+
author_hash: authorIdHash,
|
|
238
|
+
prompt_hash: promptHash,
|
|
239
|
+
session_label: sessionLabel,
|
|
240
|
+
conversation_depth: userMessageCount,
|
|
241
|
+
};
|
|
242
|
+
_attributionCache.set(sessionId, ctx);
|
|
243
|
+
return ctx;
|
|
244
|
+
}
|
|
245
|
+
if (userMessageCount > 0 && firstUserText) {
|
|
246
|
+
const promptHash = (0, vault_1.hmacHash)('trigger.prompt', firstUserText);
|
|
247
|
+
const ctx = {
|
|
248
|
+
trigger_type: 'user_message',
|
|
249
|
+
prompt_hash: promptHash,
|
|
250
|
+
session_label: sessionLabel,
|
|
251
|
+
conversation_depth: userMessageCount,
|
|
252
|
+
};
|
|
253
|
+
_attributionCache.set(sessionId, ctx);
|
|
254
|
+
return ctx;
|
|
255
|
+
}
|
|
256
|
+
return { trigger_type: 'unknown' };
|
|
257
|
+
}
|
package/dist/src/cli-cases.js
CHANGED
|
@@ -48,7 +48,7 @@ function registerCasesCli(shieldCommand) {
|
|
|
48
48
|
const cases = shieldCommand.command('cases')
|
|
49
49
|
.description('List and manage Shield security cases');
|
|
50
50
|
cases.action(async () => {
|
|
51
|
-
await listCases({ status: 'open', limit: '20', format: 'table' });
|
|
51
|
+
await listCases({ status: 'open', limit: '20', format: 'table', mine: false });
|
|
52
52
|
});
|
|
53
53
|
cases
|
|
54
54
|
.command('list')
|
|
@@ -56,6 +56,7 @@ function registerCasesCli(shieldCommand) {
|
|
|
56
56
|
.option('--status <status>', 'Filter: open, resolved, all', 'open')
|
|
57
57
|
.option('--limit <n>', 'Max results', '20')
|
|
58
58
|
.option('--format <fmt>', 'Output format: table or json', 'table')
|
|
59
|
+
.option('--mine', 'Show only cases from this instance')
|
|
59
60
|
.action(listCases);
|
|
60
61
|
cases
|
|
61
62
|
.command('show')
|
|
@@ -108,15 +109,37 @@ async function listCases(opts) {
|
|
|
108
109
|
console.log(JSON.stringify(result, null, 2));
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
+
const own = result.cases.filter((c) => c.ownership === 'own').length;
|
|
113
|
+
const sibling = result.cases.filter((c) => c.ownership === 'sibling').length;
|
|
114
|
+
const unknown = result.cases.filter((c) => c.ownership === 'unknown' || c.ownership == null).length;
|
|
115
|
+
const visible = opts.mine
|
|
116
|
+
? result.cases.filter((c) => c.is_mine === true)
|
|
117
|
+
: result.cases;
|
|
118
|
+
if (visible.length === 0 && opts.mine && result.cases.length > 0) {
|
|
119
|
+
console.log('No cases attributed to this instance. Other org instances have open cases \u2014 run without --mine to see them.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (visible.length === 0) {
|
|
112
123
|
console.log('No open cases. \u2705');
|
|
113
124
|
return;
|
|
114
125
|
}
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
if (sibling > 0 || (own > 0 && unknown > 0)) {
|
|
127
|
+
const parts = [];
|
|
128
|
+
if (own > 0)
|
|
129
|
+
parts.push(`${own} from this instance`);
|
|
130
|
+
if (sibling > 0)
|
|
131
|
+
parts.push(`${sibling} from other instances`);
|
|
132
|
+
if (unknown > 0)
|
|
133
|
+
parts.push(`${unknown} unattributed`);
|
|
134
|
+
console.log(`Attribution: ${parts.join(', ')} (use --mine to filter)\n`);
|
|
135
|
+
}
|
|
136
|
+
console.log(`Cases (${visible.length} of ${result.total}):\n`);
|
|
137
|
+
for (const c of visible) {
|
|
117
138
|
const sev = (c.severity || '').padEnd(8);
|
|
118
139
|
const time = new Date(c.created_at).toLocaleString();
|
|
119
|
-
|
|
140
|
+
const ownershipTag = c.ownership === 'own' ? ' [mine]' :
|
|
141
|
+
c.ownership === 'sibling' ? ' [sibling]' : '';
|
|
142
|
+
console.log(` [${sev}]${ownershipTag} ${c.id}`);
|
|
120
143
|
console.log(` ${c.rule_title || c.rule_id}`);
|
|
121
144
|
console.log(` ${c.summary || ''}`);
|
|
122
145
|
console.log(` Created: ${time} Events: ${c.event_count || 0}\n`);
|
|
@@ -138,6 +161,12 @@ async function showCase(id, opts) {
|
|
|
138
161
|
}
|
|
139
162
|
console.log(`Case: ${result.id}`);
|
|
140
163
|
console.log(`Status: ${result.status} Severity: ${result.severity}`);
|
|
164
|
+
if (result.ownership === 'sibling') {
|
|
165
|
+
console.log(`Instance: sibling (belongs to another instance in your org \u2014 you can resolve it, but local log access is unavailable)`);
|
|
166
|
+
}
|
|
167
|
+
else if (result.ownership === 'own') {
|
|
168
|
+
console.log(`Instance: this instance`);
|
|
169
|
+
}
|
|
141
170
|
console.log(`Rule: ${result.rule_title || result.rule_id}`);
|
|
142
171
|
console.log(`Summary: ${result.summary || '\u2014'}`);
|
|
143
172
|
console.log(`Created: ${new Date(result.created_at).toLocaleString()}`);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TriggerType as _TriggerType } from '../attributor';
|
|
2
|
+
export type TriggerType = _TriggerType;
|
|
1
3
|
export interface BaseEvent {
|
|
2
4
|
timestamp: string;
|
|
3
5
|
event_type: 'TOOL_CALL' | 'TOOL_RESULT';
|
|
@@ -14,6 +16,17 @@ export interface BaseEvent {
|
|
|
14
16
|
user: string;
|
|
15
17
|
};
|
|
16
18
|
tool_metadata?: Record<string, string | null>;
|
|
19
|
+
trigger?: {
|
|
20
|
+
type: TriggerType;
|
|
21
|
+
author_name?: string;
|
|
22
|
+
author_channel?: string;
|
|
23
|
+
author_hash?: string;
|
|
24
|
+
prompt_hash?: string;
|
|
25
|
+
session_label?: string;
|
|
26
|
+
parent_agent_id?: string;
|
|
27
|
+
cron_schedule?: string;
|
|
28
|
+
conversation_depth?: number;
|
|
29
|
+
};
|
|
17
30
|
}
|
|
18
31
|
export interface NetworkBlock {
|
|
19
32
|
network: {
|
package/dist/src/index.js
CHANGED
|
@@ -172,7 +172,7 @@ async function poll() {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
if (entries.length > 0) {
|
|
175
|
-
let envelopes = (0, transformer_1.transformEntries)(entries);
|
|
175
|
+
let envelopes = (0, transformer_1.transformEntries)(entries, config.sessionDirs);
|
|
176
176
|
const { valid: validEnvelopes, quarantined } = (0, validator_1.validate)(envelopes.map(e => e.event));
|
|
177
177
|
if (quarantined > 0) {
|
|
178
178
|
log.warn('bridge', `${quarantined} events quarantined (see ~/.openclaw/shield/data/quarantine.jsonl)`);
|
|
@@ -15,5 +15,5 @@ export declare function resolveAgentLabel(agentId: string): string;
|
|
|
15
15
|
export declare function getCachedPublicIp(): string | null;
|
|
16
16
|
export declare function isPrivateIp(ip: string): boolean;
|
|
17
17
|
export declare function resolveOutboundIp(): Promise<string | null>;
|
|
18
|
-
export declare function transformEntries(entries: RawEntry[]): EnvelopeEvent[];
|
|
18
|
+
export declare function transformEntries(entries: RawEntry[], sessionDirs?: string[]): EnvelopeEvent[];
|
|
19
19
|
export declare function generateHostTelemetry(): EnvelopeEvent | null;
|
package/dist/src/transformer.js
CHANGED
|
@@ -52,6 +52,7 @@ const log = __importStar(require("./log"));
|
|
|
52
52
|
const version_1 = require("./version");
|
|
53
53
|
const counters_1 = require("./counters");
|
|
54
54
|
const inventory_1 = require("./inventory");
|
|
55
|
+
const attributor_1 = require("./attributor");
|
|
55
56
|
let _cachedOpenClawVersion = "";
|
|
56
57
|
function normalizeSoftwareVersion(v) {
|
|
57
58
|
return v
|
|
@@ -273,9 +274,72 @@ function isAdministrativeEvent(toolName, args, sessionId) {
|
|
|
273
274
|
return true;
|
|
274
275
|
return false;
|
|
275
276
|
}
|
|
276
|
-
function
|
|
277
|
+
function findSessionDir(agentId, sessionDirs) {
|
|
278
|
+
const normalized = agentId.toLowerCase();
|
|
279
|
+
return sessionDirs.find(dir => {
|
|
280
|
+
const parts = dir.replace(/\\/g, '/').split('/');
|
|
281
|
+
return parts.length >= 2 &&
|
|
282
|
+
parts[parts.length - 1] === 'sessions' &&
|
|
283
|
+
parts[parts.length - 2].toLowerCase() === normalized;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function flattenTriggerToMetadata(event, ctx) {
|
|
287
|
+
if (ctx.trigger_type === 'unknown')
|
|
288
|
+
return;
|
|
289
|
+
if (!event.tool_metadata)
|
|
290
|
+
event.tool_metadata = {};
|
|
291
|
+
const m = event.tool_metadata;
|
|
292
|
+
m['trigger.type'] = ctx.trigger_type;
|
|
293
|
+
if (ctx.author_hash)
|
|
294
|
+
m['trigger.author_hash'] = ctx.author_hash;
|
|
295
|
+
if (ctx.prompt_hash)
|
|
296
|
+
m['trigger.prompt_hash'] = ctx.prompt_hash;
|
|
297
|
+
if (ctx.conversation_depth !== undefined)
|
|
298
|
+
m['trigger.conversation_depth'] = String(ctx.conversation_depth);
|
|
299
|
+
if (ctx.author_name)
|
|
300
|
+
m['trigger.author_name'] = ctx.author_name;
|
|
301
|
+
if (ctx.author_channel)
|
|
302
|
+
m['trigger.author_channel'] = ctx.author_channel;
|
|
303
|
+
if (ctx.session_label)
|
|
304
|
+
m['trigger.session_label'] = ctx.session_label;
|
|
305
|
+
if (ctx.parent_agent_id)
|
|
306
|
+
m['trigger.parent_agent_id'] = ctx.parent_agent_id;
|
|
307
|
+
if (ctx.cron_schedule)
|
|
308
|
+
m['trigger.cron_schedule'] = ctx.cron_schedule;
|
|
309
|
+
}
|
|
310
|
+
function buildTriggerField(ctx) {
|
|
311
|
+
if (ctx.trigger_type === 'unknown')
|
|
312
|
+
return undefined;
|
|
313
|
+
return {
|
|
314
|
+
type: ctx.trigger_type,
|
|
315
|
+
...(ctx.author_name !== undefined && { author_name: ctx.author_name }),
|
|
316
|
+
...(ctx.author_channel !== undefined && { author_channel: ctx.author_channel }),
|
|
317
|
+
...(ctx.author_hash !== undefined && { author_hash: ctx.author_hash }),
|
|
318
|
+
...(ctx.prompt_hash !== undefined && { prompt_hash: ctx.prompt_hash }),
|
|
319
|
+
...(ctx.session_label !== undefined && { session_label: ctx.session_label }),
|
|
320
|
+
...(ctx.parent_agent_id !== undefined && { parent_agent_id: ctx.parent_agent_id }),
|
|
321
|
+
...(ctx.cron_schedule !== undefined && { cron_schedule: ctx.cron_schedule }),
|
|
322
|
+
...(ctx.conversation_depth !== undefined && { conversation_depth: ctx.conversation_depth }),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function transformEntries(entries, sessionDirs) {
|
|
277
326
|
const baseSource = getSourceInfo();
|
|
278
327
|
const envelopes = [];
|
|
328
|
+
const attributionMap = new Map();
|
|
329
|
+
if (sessionDirs && sessionDirs.length > 0) {
|
|
330
|
+
const seen = new Set();
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
const key = `${entry._agentId}::${entry._sessionId}`;
|
|
333
|
+
if (seen.has(key))
|
|
334
|
+
continue;
|
|
335
|
+
seen.add(key);
|
|
336
|
+
const dir = findSessionDir(entry._agentId, sessionDirs);
|
|
337
|
+
if (dir) {
|
|
338
|
+
const ctx = (0, attributor_1.resolveAttribution)(entry._sessionId, entry._agentId, dir);
|
|
339
|
+
attributionMap.set(key, ctx);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
279
343
|
for (const entry of entries) {
|
|
280
344
|
const msg = entry.message;
|
|
281
345
|
const agentId = entry._agentId || baseSource.openclaw.agent_id;
|
|
@@ -311,6 +375,15 @@ function transformEntries(entries) {
|
|
|
311
375
|
event.tool_metadata['openclaw.cross_workspace_access'] = 'true';
|
|
312
376
|
event.tool_metadata['openclaw.target_workspace'] = targetWorkspace;
|
|
313
377
|
}
|
|
378
|
+
const attrKey = `${agentId}::${entry._sessionId}`;
|
|
379
|
+
const attrCtx = attributionMap.get(attrKey);
|
|
380
|
+
if (attrCtx) {
|
|
381
|
+
const triggerField = buildTriggerField(attrCtx);
|
|
382
|
+
if (triggerField) {
|
|
383
|
+
event.trigger = triggerField;
|
|
384
|
+
flattenTriggerToMetadata(event, attrCtx);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
314
387
|
log.debug('transformer', `TOOL_CALL tool=${toolName} session=${entry._sessionId} agent=${agentId} schema=${schema.constructor?.name || 'unknown'} admin=${event.tool_metadata?.['openclaw.is_administrative'] === 'true'}`, log.isDebug ? event : undefined);
|
|
315
388
|
(0, counters_1.recordEventType)(event.event_type);
|
|
316
389
|
envelopes.push({ source, event });
|
|
@@ -323,6 +396,15 @@ function transformEntries(entries) {
|
|
|
323
396
|
event.tool_metadata = {};
|
|
324
397
|
event.tool_metadata['openclaw.is_administrative'] = 'true';
|
|
325
398
|
}
|
|
399
|
+
const attrKeyR = `${agentId}::${entry._sessionId}`;
|
|
400
|
+
const attrCtxR = attributionMap.get(attrKeyR);
|
|
401
|
+
if (attrCtxR) {
|
|
402
|
+
const triggerFieldR = buildTriggerField(attrCtxR);
|
|
403
|
+
if (triggerFieldR) {
|
|
404
|
+
event.trigger = triggerFieldR;
|
|
405
|
+
flattenTriggerToMetadata(event, attrCtxR);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
326
408
|
log.debug('transformer', `TOOL_RESULT tool=${event.tool_name} session=${entry._sessionId} agent=${agentId}`, log.isDebug ? event : undefined);
|
|
327
409
|
(0, counters_1.recordEventType)(event.event_type);
|
|
328
410
|
envelopes.push({ source, event });
|
package/dist/src/validator.js
CHANGED
|
@@ -40,9 +40,31 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const os = __importStar(require("os"));
|
|
41
41
|
const events_1 = require("./events");
|
|
42
42
|
const base_1 = require("./events/base");
|
|
43
|
+
const attributor_1 = require("./attributor");
|
|
43
44
|
const log = __importStar(require("./log"));
|
|
44
45
|
const SHIELD_DATA_DIR = path.join(os.homedir(), '.openclaw', 'shield', 'data');
|
|
45
46
|
exports.QUARANTINE_FILE = path.join(SHIELD_DATA_DIR, 'quarantine.jsonl');
|
|
47
|
+
function validateTriggerField(event) {
|
|
48
|
+
const trigger = event.trigger;
|
|
49
|
+
if (!trigger)
|
|
50
|
+
return null;
|
|
51
|
+
if (!attributor_1.TRIGGER_TYPES.includes(trigger.type)) {
|
|
52
|
+
return `trigger.type '${trigger.type}' is not a valid TriggerType`;
|
|
53
|
+
}
|
|
54
|
+
if (trigger.author_hash !== undefined) {
|
|
55
|
+
if (typeof trigger.author_hash !== 'string' ||
|
|
56
|
+
!/^trigger\.author_id:[a-f0-9]{12}$/.test(trigger.author_hash)) {
|
|
57
|
+
return `trigger.author_hash has invalid format (expected trigger.author_id:[a-f0-9]{12})`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (trigger.prompt_hash !== undefined) {
|
|
61
|
+
if (typeof trigger.prompt_hash !== 'string' ||
|
|
62
|
+
!/^trigger\.prompt:[a-f0-9]{12}$/.test(trigger.prompt_hash)) {
|
|
63
|
+
return `trigger.prompt_hash has invalid format (expected trigger.prompt:[a-f0-9]{12})`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
46
68
|
function ensureDataDir() {
|
|
47
69
|
fs.mkdirSync(SHIELD_DATA_DIR, { recursive: true });
|
|
48
70
|
}
|
|
@@ -66,6 +88,18 @@ function validate(events) {
|
|
|
66
88
|
if (!schema) {
|
|
67
89
|
const baseResult = base_1.baseValidations.validate(event);
|
|
68
90
|
if (baseResult.valid) {
|
|
91
|
+
const triggerError = validateTriggerField(event);
|
|
92
|
+
if (triggerError) {
|
|
93
|
+
quarantined++;
|
|
94
|
+
quarantineBuffer.push(JSON.stringify({
|
|
95
|
+
quarantined_at: new Date().toISOString(),
|
|
96
|
+
validation_error: triggerError,
|
|
97
|
+
validation_field: 'trigger',
|
|
98
|
+
event,
|
|
99
|
+
}) + '\n');
|
|
100
|
+
log.warn('validator', `QUARANTINE tool=${event.tool_name} field=trigger error=${triggerError}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
69
103
|
valid.push(event);
|
|
70
104
|
}
|
|
71
105
|
else {
|
|
@@ -80,6 +114,20 @@ function validate(events) {
|
|
|
80
114
|
continue;
|
|
81
115
|
}
|
|
82
116
|
const result = (0, base_1.validateEvent)(event, schema);
|
|
117
|
+
if (result.valid) {
|
|
118
|
+
const triggerError = validateTriggerField(event);
|
|
119
|
+
if (triggerError) {
|
|
120
|
+
quarantined++;
|
|
121
|
+
quarantineBuffer.push(JSON.stringify({
|
|
122
|
+
quarantined_at: new Date().toISOString(),
|
|
123
|
+
validation_error: triggerError,
|
|
124
|
+
validation_field: 'trigger',
|
|
125
|
+
event,
|
|
126
|
+
}) + '\n');
|
|
127
|
+
log.warn('validator', `QUARANTINE tool=${event.tool_name} field=trigger error=${triggerError}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
83
131
|
if (result.valid) {
|
|
84
132
|
log.debug('validator', `PASS tool=${event.tool_name} category=${event.tool_category}`);
|
|
85
133
|
valid.push(event);
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/skills/shield/SKILL.md
CHANGED
|
@@ -31,6 +31,8 @@ Shield requires the `@upx-us/shield` plugin and an active subscription.
|
|
|
31
31
|
| `openclaw shield logs --format json` | JSON output |
|
|
32
32
|
| `openclaw shield vault show` | Agent and workspace inventory, redaction summary (hashed IDs) |
|
|
33
33
|
| `openclaw shield cases` | List open security cases |
|
|
34
|
+
| `openclaw shield cases --mine` | List only cases from this instance |
|
|
35
|
+
| `openclaw shield cases list --mine` | Show only cases from this instance |
|
|
34
36
|
| `openclaw shield cases show <ID>` | Full case detail with events, rule, playbook |
|
|
35
37
|
| `openclaw shield cases resolve <ID>` | Resolve a case (--resolution, --root-cause, --comment) |
|
|
36
38
|
| `openclaw shield monitor` | Case notification cron — status, --on, --off, --interval |
|
|
@@ -78,6 +80,8 @@ Proceed normally. No onboarding message needed.
|
|
|
78
80
|
|
|
79
81
|
When a Shield case fires or the user asks about an alert: use `openclaw shield cases` to list open cases and `openclaw shield cases --id <id>` for full detail (timeline, matched events, playbook). Severity guidance: **CRITICAL/HIGH** → surface immediately and ask if they want to investigate; **MEDIUM** → present and offer a playbook walkthrough; **LOW/INFO** → mention without interrupting the current task. Always include: rule name, what it detects, when it fired, and the first recommended remediation step. Confirm with the user before resolving — never resolve autonomously.
|
|
80
82
|
|
|
83
|
+
When listing cases, note how many belong to this instance vs other org instances. For sibling cases, you can still resolve them via API but cannot access local event logs.
|
|
84
|
+
|
|
81
85
|
## Case Investigation Workflow
|
|
82
86
|
|
|
83
87
|
When a Shield case fires, correlate three data sources to determine true positive vs. false positive:
|