@upx-us/shield 0.7.1 → 0.7.3
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/dist/index.js +6 -2
- package/dist/src/attributor.js +15 -4
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +23 -1
- package/dist/src/event-store.d.ts +2 -0
- package/dist/src/events/base.d.ts +1 -0
- package/dist/src/events/browser/enrich.js +3 -0
- package/dist/src/events/cron/enrich.js +6 -1
- package/dist/src/events/exec/enrich.js +1 -0
- package/dist/src/events/file/enrich.js +2 -0
- package/dist/src/events/gateway/enrich.js +1 -0
- package/dist/src/events/generic/enrich.js +7 -6
- package/dist/src/events/host-telemetry/enrich.d.ts +1 -1
- package/dist/src/events/host-telemetry/enrich.js +1 -0
- package/dist/src/events/message/enrich.js +7 -1
- package/dist/src/events/sessions-spawn/enrich.js +5 -1
- package/dist/src/events/web/enrich.js +5 -0
- package/dist/src/index.js +11 -0
- package/dist/src/sender.d.ts +1 -1
- package/dist/src/transformer.js +21 -17
- package/dist/src/updater.d.ts +2 -0
- package/dist/src/updater.js +65 -1
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +21 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/shield/SKILL.md +10 -3
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.3] — 2026-03-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Trigger attribution (`trigger_type`, `author_name`, `trigger_type` fields) now works correctly in OpenClaw plugin mode — the plugin entry point was calling `transformEntries` without forwarding `sessionDirs`, so attribution was silently skipped on every event for all plugin-mode installations
|
|
11
|
+
- Session directory discovery is now robust when the plugin process is spawned with a different `HOME` environment variable than the main OpenClaw process (common on VPS and Docker deployments) — discovery now walks up from the plugin install path to find the `agents/` directory rather than relying solely on `homedir()`
|
|
12
|
+
- `trigger.type` is now always written to `tool_metadata` (as `"unknown"`) even when session directory lookup fails entirely, ensuring the field is never silently absent from SIEM events
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## [0.7.2] — 2026-03-13
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Events now always include a meaningful `trigger.type` field — previously, unresolved sessions silently omitted the field; they now report `unknown` for full visibility
|
|
20
|
+
- Attribution correctly handles sessions where the first user message contains non-text content blocks (images, structured context)
|
|
21
|
+
- Auto-updater no longer prunes the previous version backup during the install window — the safety net is preserved until the new version is confirmed running
|
|
22
|
+
- Gateway restart is now blocked if critical plugin files are missing or empty after install, preventing boot loops from incomplete updates
|
|
23
|
+
|
|
24
|
+
### Improved
|
|
25
|
+
- `shield logs` entries now include a `details` line (file path, command, or URL) and a `[trigger_type]` tag per event — significantly more useful for case investigation
|
|
26
|
+
- Each event type generates a meaningful one-line summary (`read: path/to/file`, `exec: npm test`, `fetch: example.com/api`) instead of just the tool name
|
|
27
|
+
- New `src/utils.ts` shared module — `trunc()` and `urlHost()` helpers consolidated with full unit test coverage
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
7
31
|
## [0.7.1] — 2026-03-13
|
|
8
32
|
|
|
9
33
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -775,7 +775,7 @@ exports.default = {
|
|
|
775
775
|
state.lastCaptureAt = Date.now();
|
|
776
776
|
state.captureSeenSinceLastSync = true;
|
|
777
777
|
markStateDirty();
|
|
778
|
-
let envelopes = transformEntries(entries);
|
|
778
|
+
let envelopes = transformEntries(entries, config.sessionDirs);
|
|
779
779
|
const { valid: validEvents, quarantined } = validate(envelopes.map(e => e.event));
|
|
780
780
|
if (quarantined > 0) {
|
|
781
781
|
state.quarantineCount += quarantined;
|
|
@@ -1233,7 +1233,11 @@ exports.default = {
|
|
|
1233
1233
|
const time = new Date(e.ts).toLocaleTimeString();
|
|
1234
1234
|
const flag = e.redacted ? ' 🔒' : '';
|
|
1235
1235
|
const summaryTrunc = e.summary.length > 60 ? e.summary.slice(0, 57) + '...' : e.summary;
|
|
1236
|
-
|
|
1236
|
+
const triggerTag = (e.trigger_type && e.trigger_type !== 'unknown') ? ` [${e.trigger_type}]` : '';
|
|
1237
|
+
console.log(` ${time} ${e.type.padEnd(12)} ${e.tool.padEnd(10)} ${summaryTrunc}${flag}${triggerTag}`);
|
|
1238
|
+
if (e.details) {
|
|
1239
|
+
console.log(` ↳ ${e.details}`);
|
|
1240
|
+
}
|
|
1237
1241
|
}
|
|
1238
1242
|
console.log('');
|
|
1239
1243
|
console.log(` Showing ${events.length} events. Use --last N for more, --format json for details.`);
|
package/dist/src/attributor.js
CHANGED
|
@@ -177,7 +177,7 @@ function resolveAttribution(sessionId, agentId, sessionDir) {
|
|
|
177
177
|
if (!msg || msg.role !== 'user')
|
|
178
178
|
continue;
|
|
179
179
|
userMessageCount++;
|
|
180
|
-
|
|
180
|
+
let textContent = Array.isArray(msg.content)
|
|
181
181
|
? msg.content
|
|
182
182
|
.filter((c) => c.type === 'text')
|
|
183
183
|
.map((c) => String(c.text || ''))
|
|
@@ -185,6 +185,17 @@ function resolveAttribution(sessionId, agentId, sessionDir) {
|
|
|
185
185
|
: typeof msg.content === 'string'
|
|
186
186
|
? msg.content
|
|
187
187
|
: '';
|
|
188
|
+
if (!textContent && Array.isArray(msg.content)) {
|
|
189
|
+
for (const item of msg.content) {
|
|
190
|
+
if (item && typeof item === 'object') {
|
|
191
|
+
const val = item.text || item.content || item.value || item.data;
|
|
192
|
+
if (typeof val === 'string' && val.trim()) {
|
|
193
|
+
textContent = val.trim();
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
188
199
|
if (!firstUserText)
|
|
189
200
|
firstUserText = textContent;
|
|
190
201
|
if (!senderBlock)
|
|
@@ -242,11 +253,11 @@ function resolveAttribution(sessionId, agentId, sessionDir) {
|
|
|
242
253
|
_attributionCache.set(sessionId, ctx);
|
|
243
254
|
return ctx;
|
|
244
255
|
}
|
|
245
|
-
if (userMessageCount > 0
|
|
246
|
-
const promptHash = (0, vault_1.hmacHash)('trigger.prompt', firstUserText);
|
|
256
|
+
if (userMessageCount > 0) {
|
|
257
|
+
const promptHash = firstUserText ? (0, vault_1.hmacHash)('trigger.prompt', firstUserText) : undefined;
|
|
247
258
|
const ctx = {
|
|
248
259
|
trigger_type: 'user_message',
|
|
249
|
-
prompt_hash: promptHash,
|
|
260
|
+
...(promptHash ? { prompt_hash: promptHash } : {}),
|
|
250
261
|
session_label: sessionLabel,
|
|
251
262
|
conversation_depth: userMessageCount,
|
|
252
263
|
};
|
package/dist/src/config.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface Config {
|
|
|
17
17
|
localEventLimit: number;
|
|
18
18
|
credentials: ShieldCredentials;
|
|
19
19
|
}
|
|
20
|
+
export declare function deriveAgentsDirFromInstallPath(startDir?: string): string | null;
|
|
20
21
|
export declare const SHIELD_CONFIG_PATH: string;
|
|
21
22
|
export declare function injectConfigEnv(): void;
|
|
22
23
|
export declare function loadCredentials(): ShieldCredentials;
|
package/dist/src/config.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.SHIELD_CONFIG_PATH = void 0;
|
|
37
|
+
exports.deriveAgentsDirFromInstallPath = deriveAgentsDirFromInstallPath;
|
|
37
38
|
exports.injectConfigEnv = injectConfigEnv;
|
|
38
39
|
exports.loadCredentials = loadCredentials;
|
|
39
40
|
exports.loadCredentialsFromPluginConfig = loadCredentialsFromPluginConfig;
|
|
@@ -42,7 +43,28 @@ const os_1 = require("os");
|
|
|
42
43
|
const path_1 = require("path");
|
|
43
44
|
const fs_1 = require("fs");
|
|
44
45
|
const log = __importStar(require("./log"));
|
|
45
|
-
|
|
46
|
+
function deriveAgentsDirFromInstallPath(startDir) {
|
|
47
|
+
let dir = startDir ?? __dirname;
|
|
48
|
+
for (let i = 0; i < 8; i++) {
|
|
49
|
+
const parent = (0, path_1.dirname)(dir);
|
|
50
|
+
if (parent === dir)
|
|
51
|
+
break;
|
|
52
|
+
dir = parent;
|
|
53
|
+
const candidate = (0, path_1.join)(dir, 'agents');
|
|
54
|
+
if ((0, fs_1.existsSync)(candidate))
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function resolveAgentsDir() {
|
|
60
|
+
if (process.env.OPENCLAW_AGENTS_DIR)
|
|
61
|
+
return process.env.OPENCLAW_AGENTS_DIR;
|
|
62
|
+
const derived = deriveAgentsDirFromInstallPath();
|
|
63
|
+
if (derived)
|
|
64
|
+
return derived;
|
|
65
|
+
return (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'agents');
|
|
66
|
+
}
|
|
67
|
+
const OPENCLAW_AGENTS_DIR = resolveAgentsDir();
|
|
46
68
|
function safeParseInt(value, fallback) {
|
|
47
69
|
if (!value)
|
|
48
70
|
return fallback;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.enrich = enrich;
|
|
4
4
|
const base_1 = require("../base");
|
|
5
|
+
const utils_1 = require("../../utils");
|
|
5
6
|
function enrich(tool, ctx) {
|
|
6
7
|
const args = tool.arguments;
|
|
7
8
|
const url = args.targetUrl || args.url || '';
|
|
@@ -14,6 +15,7 @@ function enrich(tool, ctx) {
|
|
|
14
15
|
url_domain: null,
|
|
15
16
|
url_is_internal: false,
|
|
16
17
|
};
|
|
18
|
+
const action = args.action || '';
|
|
17
19
|
const event = {
|
|
18
20
|
timestamp: ctx.timestamp,
|
|
19
21
|
event_type: 'TOOL_CALL',
|
|
@@ -32,6 +34,7 @@ function enrich(tool, ctx) {
|
|
|
32
34
|
url,
|
|
33
35
|
target: { url },
|
|
34
36
|
tool_metadata: (0, base_1.stringifyMetadata)(meta),
|
|
37
|
+
display_summary: `browser: ${action}${url ? ' ' + (0, utils_1.urlHost)(url) : ''}`,
|
|
35
38
|
};
|
|
36
39
|
try {
|
|
37
40
|
const u = new URL(url);
|
|
@@ -23,6 +23,10 @@ function enrich(tool, ctx) {
|
|
|
23
23
|
meta.cron_has_exec_instruction = /\b(exec|run|execute|shell|command|delete|rm|kill)\b/i.test(payload.text);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
const metaStr = (0, base_1.stringifyMetadata)(meta);
|
|
27
|
+
const cronAction = String(args.action || '');
|
|
28
|
+
const cronJobId = String(args.jobId || args.id || '');
|
|
29
|
+
const cronDetail = cronJobId ? ` ${cronJobId}` : cronAction ? ` ${cronAction}` : '';
|
|
26
30
|
return {
|
|
27
31
|
timestamp: ctx.timestamp,
|
|
28
32
|
event_type: 'TOOL_CALL',
|
|
@@ -39,6 +43,7 @@ function enrich(tool, ctx) {
|
|
|
39
43
|
user: ctx.agentId,
|
|
40
44
|
},
|
|
41
45
|
arguments_summary: (0, base_1.truncate)(JSON.stringify(args || {})),
|
|
42
|
-
tool_metadata:
|
|
46
|
+
tool_metadata: metaStr,
|
|
47
|
+
display_summary: `cron:${cronDetail}`,
|
|
43
48
|
};
|
|
44
49
|
}
|
|
@@ -126,6 +126,7 @@ function enrich(tool, ctx) {
|
|
|
126
126
|
workdir: args.workdir || null,
|
|
127
127
|
target: { command_line: (0, base_1.truncate)(cmd) },
|
|
128
128
|
tool_metadata: (0, base_1.stringifyMetadata)(meta),
|
|
129
|
+
display_summary: `exec: ${cmd.slice(0, 80)}`,
|
|
129
130
|
};
|
|
130
131
|
const sshSegment = segments.find(s => /^\s*(ssh|scp|rsync)\b/.test(s));
|
|
131
132
|
const sshRoot = sshSegment
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enrich = enrich;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const base_1 = require("../base");
|
|
6
|
+
const utils_1 = require("../../utils");
|
|
6
7
|
function enrich(tool, ctx) {
|
|
7
8
|
const args = tool.arguments;
|
|
8
9
|
const fp = args.file_path || args.path || args.filePath || '';
|
|
@@ -59,6 +60,7 @@ function enrich(tool, ctx) {
|
|
|
59
60
|
file_directory: (0, path_1.dirname)(fp),
|
|
60
61
|
},
|
|
61
62
|
tool_metadata: (0, base_1.stringifyMetadata)(meta),
|
|
63
|
+
display_summary: `${toolName}: ${(0, utils_1.trunc)(fp, 100)}`,
|
|
62
64
|
};
|
|
63
65
|
if (isSystem) {
|
|
64
66
|
event.security_result = { severity: 'HIGH', summary: `System ${toolName} operation`, category: 'system_tampering' };
|
|
@@ -38,6 +38,7 @@ function enrich(tool, ctx) {
|
|
|
38
38
|
},
|
|
39
39
|
arguments_summary: (0, base_1.truncate)(JSON.stringify(args || {})),
|
|
40
40
|
tool_metadata: (0, base_1.stringifyMetadata)(meta),
|
|
41
|
+
display_summary: `gateway: ${action}`,
|
|
41
42
|
};
|
|
42
43
|
if (meta.gateway_is_config_change || meta.gateway_is_restart || meta.gateway_is_update) {
|
|
43
44
|
event.security_result = {
|
|
@@ -4,6 +4,12 @@ exports.enrich = enrich;
|
|
|
4
4
|
const base_1 = require("../base");
|
|
5
5
|
function enrich(tool, ctx) {
|
|
6
6
|
const args = tool.arguments;
|
|
7
|
+
const meta = {
|
|
8
|
+
tool_name: tool.name,
|
|
9
|
+
'openclaw.session_id': ctx.sessionId,
|
|
10
|
+
'openclaw.agent_id': ctx.agentId,
|
|
11
|
+
sub_action: args.action || null,
|
|
12
|
+
};
|
|
7
13
|
return {
|
|
8
14
|
timestamp: ctx.timestamp,
|
|
9
15
|
event_type: 'TOOL_CALL',
|
|
@@ -20,11 +26,6 @@ function enrich(tool, ctx) {
|
|
|
20
26
|
user: ctx.agentId,
|
|
21
27
|
},
|
|
22
28
|
arguments_summary: (0, base_1.truncate)(JSON.stringify(args || {})),
|
|
23
|
-
tool_metadata: (0, base_1.stringifyMetadata)(
|
|
24
|
-
tool_name: tool.name,
|
|
25
|
-
'openclaw.session_id': ctx.sessionId,
|
|
26
|
-
'openclaw.agent_id': ctx.agentId,
|
|
27
|
-
sub_action: args.action || null,
|
|
28
|
-
}),
|
|
29
|
+
tool_metadata: (0, base_1.stringifyMetadata)(meta),
|
|
29
30
|
};
|
|
30
31
|
}
|
|
@@ -42,7 +42,9 @@ function enrich(tool, ctx) {
|
|
|
42
42
|
const raw = typeof args.buffer === 'string' ? args.buffer.replace(/^data:[^;]+;base64,/, '') : '';
|
|
43
43
|
meta.payload_size_bytes = String(Math.floor(raw.length * 0.75));
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
const msgAction = String(args.action || '');
|
|
46
|
+
const msgTarget = String(args.target || args.channel || '');
|
|
47
|
+
const event = {
|
|
46
48
|
timestamp: ctx.timestamp,
|
|
47
49
|
event_type: 'TOOL_CALL',
|
|
48
50
|
tool_name: 'message',
|
|
@@ -59,5 +61,9 @@ function enrich(tool, ctx) {
|
|
|
59
61
|
},
|
|
60
62
|
arguments_summary: (0, base_1.truncate)(JSON.stringify(args || {})),
|
|
61
63
|
tool_metadata: (0, base_1.stringifyMetadata)(meta),
|
|
64
|
+
display_summary: msgTarget
|
|
65
|
+
? `message: ${msgAction} → ${msgTarget}`
|
|
66
|
+
: `message: ${msgAction}`,
|
|
62
67
|
};
|
|
68
|
+
return event;
|
|
63
69
|
}
|
|
@@ -17,6 +17,9 @@ function enrich(tool, ctx) {
|
|
|
17
17
|
spawn_task_has_cred: /\b(password|credential|secret|key|token|ssh)\b/i.test(task),
|
|
18
18
|
spawn_task_has_exfil: /\b(send|upload|post|transfer|exfil)/i.test(task),
|
|
19
19
|
};
|
|
20
|
+
const metaStr = (0, base_1.stringifyMetadata)(meta);
|
|
21
|
+
const spawnLabel = String(args.label || task);
|
|
22
|
+
const display = spawnLabel.length > 80 ? spawnLabel.slice(0, 79) + '…' : spawnLabel;
|
|
20
23
|
return {
|
|
21
24
|
timestamp: ctx.timestamp,
|
|
22
25
|
event_type: 'TOOL_CALL',
|
|
@@ -34,6 +37,7 @@ function enrich(tool, ctx) {
|
|
|
34
37
|
},
|
|
35
38
|
arguments_summary: (0, base_1.truncate)(task),
|
|
36
39
|
target: { command_line: (0, base_1.truncate)(task) },
|
|
37
|
-
tool_metadata:
|
|
40
|
+
tool_metadata: metaStr,
|
|
41
|
+
display_summary: `spawn: ${display}`,
|
|
38
42
|
};
|
|
39
43
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isInternalUrl = isInternalUrl;
|
|
4
4
|
exports.enrich = enrich;
|
|
5
5
|
const base_1 = require("../base");
|
|
6
|
+
const utils_1 = require("../../utils");
|
|
6
7
|
function isInternalUrl(u) {
|
|
7
8
|
const h = u.hostname;
|
|
8
9
|
const bare = h.startsWith('[') && h.endsWith(']') ? h.slice(1, -1) : h;
|
|
@@ -26,6 +27,9 @@ function enrich(tool, ctx) {
|
|
|
26
27
|
const url = rawUrl || (tool.name === 'web_search' && args.query
|
|
27
28
|
? `https://search.brave.com/search?q=${encodeURIComponent(String(args.query).slice(0, 200))}`
|
|
28
29
|
: '');
|
|
30
|
+
const displaySummary = tool.name === 'web_search'
|
|
31
|
+
? `search: ${(0, utils_1.trunc)(args.query, 80)}`
|
|
32
|
+
: `fetch: ${(0, utils_1.urlHost)(rawUrl)}`;
|
|
29
33
|
const event = {
|
|
30
34
|
timestamp: ctx.timestamp,
|
|
31
35
|
event_type: 'TOOL_CALL',
|
|
@@ -49,6 +53,7 @@ function enrich(tool, ctx) {
|
|
|
49
53
|
'openclaw.session_id': ctx.sessionId,
|
|
50
54
|
'openclaw.agent_id': ctx.agentId,
|
|
51
55
|
}),
|
|
56
|
+
display_summary: displaySummary,
|
|
52
57
|
};
|
|
53
58
|
try {
|
|
54
59
|
const u = new URL(url);
|
package/dist/src/index.js
CHANGED
|
@@ -49,6 +49,15 @@ const event_store_1 = require("./event-store");
|
|
|
49
49
|
const case_monitor_1 = require("./case-monitor");
|
|
50
50
|
const exclusions_1 = require("./exclusions");
|
|
51
51
|
const SHIELD_DATA_DIR = path.join(os.homedir(), '.openclaw', 'shield', 'data');
|
|
52
|
+
function extractEventDetails(event) {
|
|
53
|
+
const path = event.target?.file_path || event.target?.file?.path || event.target?.process?.command_line;
|
|
54
|
+
const url = event.network?.http?.url || event.target?.url || event.url;
|
|
55
|
+
if (path)
|
|
56
|
+
return path.length > 100 ? path.slice(0, 97) + '…' : path;
|
|
57
|
+
if (url)
|
|
58
|
+
return url.length > 100 ? url.slice(0, 97) + '…' : url;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
52
61
|
let running = true;
|
|
53
62
|
let lastTelemetryAt = 0;
|
|
54
63
|
let consecutiveFailures = 0;
|
|
@@ -208,6 +217,8 @@ async function poll() {
|
|
|
208
217
|
session: env.event.session_id || '?',
|
|
209
218
|
model: env.event.model || '?',
|
|
210
219
|
redacted: !!env.source?.plugin?.redaction_applied,
|
|
220
|
+
details: extractEventDetails(env.event),
|
|
221
|
+
trigger_type: env.event.tool_metadata?.['trigger.type'] ?? undefined,
|
|
211
222
|
}));
|
|
212
223
|
(0, event_store_1.appendEvents)(summaries);
|
|
213
224
|
}
|
package/dist/src/sender.d.ts
CHANGED
|
@@ -24,4 +24,4 @@ export interface ReportInstanceResult {
|
|
|
24
24
|
score?: InstanceScore;
|
|
25
25
|
}
|
|
26
26
|
export declare function reportInstance(payload: Record<string, unknown>, credentials: ShieldCredentials): Promise<ReportInstanceResult>;
|
|
27
|
-
export declare function reportLifecycleEvent(type: 'plugin_started' | 'update_restart_failed' | 'plugin_integrity_drift' | 'update_check_failing', data: Record<string, unknown>, credentials: ShieldCredentials): Promise<void>;
|
|
27
|
+
export declare function reportLifecycleEvent(type: 'plugin_started' | 'update_restart_failed' | 'plugin_integrity_drift' | 'update_check_failing' | 'update_integrity_failed', data: Record<string, unknown>, credentials: ShieldCredentials): Promise<void>;
|
package/dist/src/transformer.js
CHANGED
|
@@ -284,12 +284,13 @@ function findSessionDir(agentId, sessionDirs) {
|
|
|
284
284
|
});
|
|
285
285
|
}
|
|
286
286
|
function flattenTriggerToMetadata(event, ctx) {
|
|
287
|
-
if (ctx.trigger_type === 'unknown')
|
|
288
|
-
return;
|
|
289
287
|
if (!event.tool_metadata)
|
|
290
288
|
event.tool_metadata = {};
|
|
291
289
|
const m = event.tool_metadata;
|
|
292
290
|
m['trigger.type'] = ctx.trigger_type;
|
|
291
|
+
if (ctx.trigger_type === 'unknown')
|
|
292
|
+
return;
|
|
293
|
+
m['trigger.type'] = ctx.trigger_type;
|
|
293
294
|
if (ctx.author_hash)
|
|
294
295
|
m['trigger.author_hash'] = ctx.author_hash;
|
|
295
296
|
if (ctx.prompt_hash)
|
|
@@ -333,7 +334,9 @@ function transformEntries(entries, sessionDirs) {
|
|
|
333
334
|
if (seen.has(key))
|
|
334
335
|
continue;
|
|
335
336
|
seen.add(key);
|
|
336
|
-
const dir =
|
|
337
|
+
const dir = sessionDirs.length === 1
|
|
338
|
+
? sessionDirs[0]
|
|
339
|
+
: findSessionDir(entry._agentId, sessionDirs);
|
|
337
340
|
if (dir) {
|
|
338
341
|
const ctx = (0, attributor_1.resolveAttribution)(entry._sessionId, entry._agentId, dir);
|
|
339
342
|
attributionMap.set(key, ctx);
|
|
@@ -358,6 +361,11 @@ function transformEntries(entries, sessionDirs) {
|
|
|
358
361
|
if (!schema)
|
|
359
362
|
continue;
|
|
360
363
|
const event = schema.enrich({ name: toolName, id: item.id, arguments: args }, { sessionId: entry._sessionId, agentId, timestamp: entry.timestamp, model, source });
|
|
364
|
+
if (!event.tool_metadata)
|
|
365
|
+
event.tool_metadata = {};
|
|
366
|
+
event.tool_metadata['openclaw.display_summary'] =
|
|
367
|
+
(event.display_summary || event.tool_name || 'event').slice(0, 120);
|
|
368
|
+
delete event.display_summary;
|
|
361
369
|
if (isAdministrativeEvent(toolName, args, entry._sessionId)) {
|
|
362
370
|
if (!event.tool_metadata)
|
|
363
371
|
event.tool_metadata = {};
|
|
@@ -376,13 +384,11 @@ function transformEntries(entries, sessionDirs) {
|
|
|
376
384
|
event.tool_metadata['openclaw.target_workspace'] = targetWorkspace;
|
|
377
385
|
}
|
|
378
386
|
const attrKey = `${agentId}::${entry._sessionId}`;
|
|
379
|
-
const attrCtx = attributionMap.get(attrKey);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
flattenTriggerToMetadata(event, attrCtx);
|
|
385
|
-
}
|
|
387
|
+
const attrCtx = attributionMap.get(attrKey) ?? { trigger_type: 'unknown' };
|
|
388
|
+
flattenTriggerToMetadata(event, attrCtx);
|
|
389
|
+
const triggerField = buildTriggerField(attrCtx);
|
|
390
|
+
if (triggerField) {
|
|
391
|
+
event.trigger = triggerField;
|
|
386
392
|
}
|
|
387
393
|
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);
|
|
388
394
|
(0, counters_1.recordEventType)(event.event_type);
|
|
@@ -397,13 +403,11 @@ function transformEntries(entries, sessionDirs) {
|
|
|
397
403
|
event.tool_metadata['openclaw.is_administrative'] = 'true';
|
|
398
404
|
}
|
|
399
405
|
const attrKeyR = `${agentId}::${entry._sessionId}`;
|
|
400
|
-
const attrCtxR = attributionMap.get(attrKeyR);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
flattenTriggerToMetadata(event, attrCtxR);
|
|
406
|
-
}
|
|
406
|
+
const attrCtxR = attributionMap.get(attrKeyR) ?? { trigger_type: 'unknown' };
|
|
407
|
+
flattenTriggerToMetadata(event, attrCtxR);
|
|
408
|
+
const triggerFieldR = buildTriggerField(attrCtxR);
|
|
409
|
+
if (triggerFieldR) {
|
|
410
|
+
event.trigger = triggerFieldR;
|
|
407
411
|
}
|
|
408
412
|
log.debug('transformer', `TOOL_RESULT tool=${event.tool_name} session=${entry._sessionId} agent=${agentId}`, log.isDebug ? event : undefined);
|
|
409
413
|
(0, counters_1.recordEventType)(event.event_type);
|
package/dist/src/updater.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export declare function saveUpdateState(state: UpdateState): void;
|
|
|
37
37
|
export declare function checkNpmVersion(): string | null;
|
|
38
38
|
export declare function checkForUpdate(overrideInterval?: number): UpdateCheckResult | null;
|
|
39
39
|
export declare function backupCurrentVersion(): string | null;
|
|
40
|
+
export declare function shouldPruneBackups(): boolean;
|
|
41
|
+
export declare function validateInstallIntegrity(pluginDir: string): boolean;
|
|
40
42
|
export declare function restoreFromBackup(backupPath: string): boolean;
|
|
41
43
|
export declare function downloadAndInstall(targetVersion: string): boolean;
|
|
42
44
|
export interface UpdateResult {
|
package/dist/src/updater.js
CHANGED
|
@@ -41,6 +41,8 @@ exports.saveUpdateState = saveUpdateState;
|
|
|
41
41
|
exports.checkNpmVersion = checkNpmVersion;
|
|
42
42
|
exports.checkForUpdate = checkForUpdate;
|
|
43
43
|
exports.backupCurrentVersion = backupCurrentVersion;
|
|
44
|
+
exports.shouldPruneBackups = shouldPruneBackups;
|
|
45
|
+
exports.validateInstallIntegrity = validateInstallIntegrity;
|
|
44
46
|
exports.restoreFromBackup = restoreFromBackup;
|
|
45
47
|
exports.downloadAndInstall = downloadAndInstall;
|
|
46
48
|
exports.performAutoUpdate = performAutoUpdate;
|
|
@@ -225,7 +227,6 @@ function backupCurrentVersion() {
|
|
|
225
227
|
if ((0, fs_1.existsSync)(skillsSrc)) {
|
|
226
228
|
copyRecursive(skillsSrc, (0, path_1.join)(backupPath, 'skills'));
|
|
227
229
|
}
|
|
228
|
-
pruneBackups();
|
|
229
230
|
log.info('updater', `Backup created: ${backupName}`);
|
|
230
231
|
return backupPath;
|
|
231
232
|
}
|
|
@@ -270,6 +271,50 @@ function listBackups() {
|
|
|
270
271
|
.map(d => (0, path_1.join)(BACKUP_DIR, d))
|
|
271
272
|
.sort((a, b) => (0, fs_1.statSync)(b).mtimeMs - (0, fs_1.statSync)(a).mtimeMs);
|
|
272
273
|
}
|
|
274
|
+
function shouldPruneBackups() {
|
|
275
|
+
if (!(0, fs_1.existsSync)(BACKUP_DIR))
|
|
276
|
+
return false;
|
|
277
|
+
try {
|
|
278
|
+
const backups = (0, fs_1.readdirSync)(BACKUP_DIR)
|
|
279
|
+
.map(n => ({ name: n, mtime: (0, fs_1.statSync)((0, path_1.join)(BACKUP_DIR, n)).mtime }))
|
|
280
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
281
|
+
if (backups.length === 0)
|
|
282
|
+
return false;
|
|
283
|
+
const latestBackupPkg = (0, path_1.join)(BACKUP_DIR, backups[0].name, 'package.json');
|
|
284
|
+
if (!(0, fs_1.existsSync)(latestBackupPkg))
|
|
285
|
+
return false;
|
|
286
|
+
const backupVersion = JSON.parse((0, fs_1.readFileSync)(latestBackupPkg, 'utf-8')).version;
|
|
287
|
+
return version_1.VERSION !== backupVersion;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function validateInstallIntegrity(pluginDir) {
|
|
294
|
+
const required = [
|
|
295
|
+
(0, path_1.join)(pluginDir, 'dist', 'index.js'),
|
|
296
|
+
(0, path_1.join)(pluginDir, 'openclaw.plugin.json'),
|
|
297
|
+
(0, path_1.join)(pluginDir, 'package.json'),
|
|
298
|
+
];
|
|
299
|
+
for (const f of required) {
|
|
300
|
+
if (!(0, fs_1.existsSync)(f)) {
|
|
301
|
+
log.error('updater', `Integrity check failed: missing ${f}`);
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const size = (0, fs_1.statSync)(f).size;
|
|
306
|
+
if (size === 0) {
|
|
307
|
+
log.error('updater', `Integrity check failed: empty file ${f}`);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
log.error('updater', `Integrity check failed: cannot stat ${f}`);
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
273
318
|
function restoreFromBackup(backupPath) {
|
|
274
319
|
if (!(0, fs_1.existsSync)(backupPath)) {
|
|
275
320
|
log.error('updater', `Backup not found: ${backupPath}`);
|
|
@@ -456,6 +501,8 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
456
501
|
return noOp;
|
|
457
502
|
}
|
|
458
503
|
}
|
|
504
|
+
if (shouldPruneBackups())
|
|
505
|
+
pruneBackups();
|
|
459
506
|
const check = checkForUpdate(checkIntervalMs);
|
|
460
507
|
if (!check || !check.updateAvailable)
|
|
461
508
|
return noOp;
|
|
@@ -531,6 +578,23 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
531
578
|
requiresRestart: false,
|
|
532
579
|
};
|
|
533
580
|
}
|
|
581
|
+
if (!validateInstallIntegrity(PLUGIN_DIR)) {
|
|
582
|
+
log.error('updater', 'Install integrity check failed — rolling back to previous version');
|
|
583
|
+
if (backupPath)
|
|
584
|
+
restoreFromBackup(backupPath);
|
|
585
|
+
try {
|
|
586
|
+
const creds = (0, config_1.loadCredentials)();
|
|
587
|
+
void (0, sender_1.reportLifecycleEvent)('update_integrity_failed', {
|
|
588
|
+
version: check.latestVersion,
|
|
589
|
+
error: 'missing or empty critical files after install',
|
|
590
|
+
}, creds);
|
|
591
|
+
}
|
|
592
|
+
catch { }
|
|
593
|
+
state.consecutiveFailures++;
|
|
594
|
+
state.lastError = 'Integrity check failed — rolled back';
|
|
595
|
+
saveUpdateState(state);
|
|
596
|
+
return noOp;
|
|
597
|
+
}
|
|
534
598
|
const restarted = requestGatewayRestart();
|
|
535
599
|
if (!restarted) {
|
|
536
600
|
state.pendingRestart = true;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trunc = trunc;
|
|
4
|
+
exports.urlHost = urlHost;
|
|
5
|
+
function trunc(s, maxLen) {
|
|
6
|
+
if (!s)
|
|
7
|
+
return '?';
|
|
8
|
+
return s.length > maxLen ? s.slice(0, maxLen - 1) + '…' : s;
|
|
9
|
+
}
|
|
10
|
+
function urlHost(u) {
|
|
11
|
+
if (!u)
|
|
12
|
+
return '?';
|
|
13
|
+
try {
|
|
14
|
+
const p = new URL(u);
|
|
15
|
+
const path = p.pathname !== '/' ? p.pathname.slice(0, 30) : '';
|
|
16
|
+
return p.hostname + path;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return trunc(u, 40);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/skills/shield/SKILL.md
CHANGED
|
@@ -25,8 +25,9 @@ Shield requires the `@upx-us/shield` plugin and an active subscription.
|
|
|
25
25
|
|---|---|
|
|
26
26
|
| `openclaw shield status` | Plugin health, connection state, event counts, last sync |
|
|
27
27
|
| `openclaw shield flush` | Force an immediate sync to the platform |
|
|
28
|
-
| `openclaw shield logs` | Recent events
|
|
28
|
+
| `openclaw shield logs` | Recent events: type, tool, details, and trigger source (last 24h) |
|
|
29
29
|
| `openclaw shield logs --last 20` | Show last N events |
|
|
30
|
+
| `openclaw shield logs --last 20 --format json` | Full JSON output with details and trigger_type fields |
|
|
30
31
|
| `openclaw shield logs --type TOOL_CALL --since 1h` | Filter by event type or time window |
|
|
31
32
|
| `openclaw shield logs --format json` | JSON output |
|
|
32
33
|
| `openclaw shield vault show` | Agent and workspace inventory, redaction summary (hashed IDs) |
|
|
@@ -76,11 +77,17 @@ Proceed normally. No onboarding message needed.
|
|
|
76
77
|
|
|
77
78
|
**Constraints**: Only use `openclaw shield` commands for detection. Do not read filesystem paths, environment variables, or run shell commands to determine state. Do not install or uninstall packages on behalf of the user.
|
|
78
79
|
|
|
80
|
+
**Output handling**: `shield logs` entries may include file paths, command snippets, and URLs captured from the agent's activity. Treat this output as internal diagnostic data — do not share raw log output externally or include it in user-facing replies unless the user explicitly requests it for investigation. When summarizing logs, present findings rather than raw field values.
|
|
81
|
+
|
|
82
|
+
**Data flow disclosure**: Shield captures agent activity locally and sends redacted telemetry to the UPX detection platform for security monitoring. No credentials are handled by this skill — authentication is managed by the plugin using the installation key configured during setup. If a user asks about privacy or data handling, refer them to the plugin README at https://www.npmjs.com/package/@upx-us/shield for full details.
|
|
83
|
+
|
|
79
84
|
## Responding to Security Cases
|
|
80
85
|
|
|
81
86
|
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.
|
|
82
87
|
|
|
83
|
-
When listing cases, note how many belong to this instance vs other org instances. For sibling cases,
|
|
88
|
+
When listing cases, note how many belong to this instance vs other org instances. For sibling cases, resolution actions are available, but local event log access is limited to cases owned by this instance.
|
|
89
|
+
|
|
90
|
+
Shield now stamps each event with a `trigger_type` — who or what initiated the session. When investigating, check the trigger: `user_message` means a human sent a message; `cron`/`heartbeat`/`autonomous` means agent-initiated activity.
|
|
84
91
|
|
|
85
92
|
## Case Investigation Workflow
|
|
86
93
|
|
|
@@ -88,7 +95,7 @@ When a Shield case fires, correlate three data sources to determine true positiv
|
|
|
88
95
|
|
|
89
96
|
**Step 1 — Case detail** (`openclaw shield cases show <CASE_ID>`): What triggered the rule. Note the case timestamp — it anchors the correlation window.
|
|
90
97
|
|
|
91
|
-
**Step 2 — Surrounding logs** (`openclaw shield logs --since 30m --type TOOL_CALL`): Look for events 5–15 minutes before and after the case timestamp. Reveals if the alert was isolated or part of a sequence.
|
|
98
|
+
**Step 2 — Surrounding logs** (`openclaw shield logs --since 30m --type TOOL_CALL`): Look for events 5–15 minutes before and after the case timestamp. Reveals if the alert was isolated or part of a sequence. Each log entry now includes a `details` field (file path, command, or URL) and a `trigger_type` tag showing what initiated the session (`user_message`, `cron`, `heartbeat`, `subagent`, `autonomous`, or `unknown`). Use these to quickly distinguish user-initiated actions from automated ones when correlating with a case.
|
|
92
99
|
|
|
93
100
|
**Step 3 — Vault context** (`openclaw shield vault show`): If the case involves redacted credentials, hostnames, or commands, the vault reveals hashed representations and redaction categories.
|
|
94
101
|
|