@upx-us/shield 0.7.0 → 0.7.2

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 CHANGED
@@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.7.2] — 2026-03-13
8
+
9
+ ### Fixed
10
+ - Events now always include a meaningful `trigger.type` field — previously, unresolved sessions silently omitted the field; they now report `unknown` for full visibility
11
+ - Attribution correctly handles sessions where the first user message contains non-text content blocks (images, structured context)
12
+ - 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
13
+ - Gateway restart is now blocked if critical plugin files are missing or empty after install, preventing boot loops from incomplete updates
14
+
15
+ ### Improved
16
+ - `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
17
+ - 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
18
+ - New `src/utils.ts` shared module — `trunc()` and `urlHost()` helpers consolidated with full unit test coverage
19
+
20
+ ---
21
+
22
+ ## [0.7.1] — 2026-03-13
23
+
24
+ ### Added
25
+ - Case ownership awareness: `shield cases` now shows which cases belong to this instance vs other instances in the org
26
+ - `--mine` flag for `shield cases` and `shield cases list` to filter to locally-owned cases
27
+ - 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
28
+ - Attribution summary line when listing mixed-ownership cases
29
+
30
+ ---
31
+
7
32
  ## [0.7.0] — 2026-03-13
8
33
 
9
34
  ### Added
package/dist/index.js CHANGED
@@ -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
- console.log(` ${time} ${e.type.padEnd(12)} ${e.tool.padEnd(10)} ${summaryTrunc}${flag}`);
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.`);
@@ -177,7 +177,7 @@ function resolveAttribution(sessionId, agentId, sessionDir) {
177
177
  if (!msg || msg.role !== 'user')
178
178
  continue;
179
179
  userMessageCount++;
180
- const textContent = Array.isArray(msg.content)
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 && firstUserText) {
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
  };
@@ -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
- if (result.cases.length === 0) {
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
- console.log(`Cases (${result.cases.length} of ${result.total}):\n`);
116
- for (const c of result.cases) {
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
- console.log(` [${sev}] ${c.id}`);
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()}`);
@@ -6,6 +6,8 @@ export interface EventSummary {
6
6
  session: string;
7
7
  model: string;
8
8
  redacted: boolean;
9
+ details?: string;
10
+ trigger_type?: string;
9
11
  }
10
12
  export interface EventStoreConfig {
11
13
  filePath: string;
@@ -27,6 +27,7 @@ export interface BaseEvent {
27
27
  cron_schedule?: string;
28
28
  conversation_depth?: number;
29
29
  };
30
+ display_summary?: string;
30
31
  }
31
32
  export interface NetworkBlock {
32
33
  network: {
@@ -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: (0, base_1.stringifyMetadata)(meta),
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
  }
@@ -1,3 +1,3 @@
1
- import { RawToolCall, EnrichmentContext } from '../base';
1
+ import { EnrichmentContext, RawToolCall } from '../base';
2
2
  import { HostTelemetryEvent } from './event';
3
3
  export declare function enrich(_tool: RawToolCall, ctx: EnrichmentContext): HostTelemetryEvent;
@@ -24,5 +24,6 @@ function enrich(_tool, ctx) {
24
24
  'openclaw.version': version,
25
25
  'openclaw.version_sortable': versionSortable,
26
26
  }),
27
+ display_summary: `host_telemetry: ${version}`,
27
28
  };
28
29
  }
@@ -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
- return {
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: (0, base_1.stringifyMetadata)(meta),
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
  }
@@ -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>;
@@ -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)
@@ -358,6 +359,11 @@ function transformEntries(entries, sessionDirs) {
358
359
  if (!schema)
359
360
  continue;
360
361
  const event = schema.enrich({ name: toolName, id: item.id, arguments: args }, { sessionId: entry._sessionId, agentId, timestamp: entry.timestamp, model, source });
362
+ if (!event.tool_metadata)
363
+ event.tool_metadata = {};
364
+ event.tool_metadata['openclaw.display_summary'] =
365
+ (event.display_summary || event.tool_name || 'event').slice(0, 120);
366
+ delete event.display_summary;
361
367
  if (isAdministrativeEvent(toolName, args, entry._sessionId)) {
362
368
  if (!event.tool_metadata)
363
369
  event.tool_metadata = {};
@@ -378,10 +384,10 @@ function transformEntries(entries, sessionDirs) {
378
384
  const attrKey = `${agentId}::${entry._sessionId}`;
379
385
  const attrCtx = attributionMap.get(attrKey);
380
386
  if (attrCtx) {
387
+ flattenTriggerToMetadata(event, attrCtx);
381
388
  const triggerField = buildTriggerField(attrCtx);
382
389
  if (triggerField) {
383
390
  event.trigger = triggerField;
384
- flattenTriggerToMetadata(event, attrCtx);
385
391
  }
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);
@@ -399,10 +405,10 @@ function transformEntries(entries, sessionDirs) {
399
405
  const attrKeyR = `${agentId}::${entry._sessionId}`;
400
406
  const attrCtxR = attributionMap.get(attrKeyR);
401
407
  if (attrCtxR) {
408
+ flattenTriggerToMetadata(event, attrCtxR);
402
409
  const triggerFieldR = buildTriggerField(attrCtxR);
403
410
  if (triggerFieldR) {
404
411
  event.trigger = triggerFieldR;
405
- flattenTriggerToMetadata(event, attrCtxR);
406
412
  }
407
413
  }
408
414
  log.debug('transformer', `TOOL_RESULT tool=${event.tool_name} session=${entry._sessionId} agent=${agentId}`, log.isDebug ? event : undefined);
@@ -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 {
@@ -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,2 @@
1
+ export declare function trunc(s: string | undefined | null, maxLen: number): string;
2
+ export declare function urlHost(u: string | undefined | null): string;
@@ -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
+ }
@@ -2,7 +2,7 @@
2
2
  "id": "shield",
3
3
  "name": "OpenClaw Shield",
4
4
  "description": "Real-time security monitoring \u2014 streams enriched, redacted security events to the Shield detection platform.",
5
- "version": "0.7.0",
5
+ "version": "0.7.2",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upx-us/shield",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Security monitoring plugin for OpenClaw agents — streams enriched security events to the Shield detection platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,12 +25,15 @@ 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 from the local buffer (last 24h) |
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) |
33
34
  | `openclaw shield cases` | List open security cases |
35
+ | `openclaw shield cases --mine` | List only cases from this instance |
36
+ | `openclaw shield cases list --mine` | Show only cases from this instance |
34
37
  | `openclaw shield cases show <ID>` | Full case detail with events, rule, playbook |
35
38
  | `openclaw shield cases resolve <ID>` | Resolve a case (--resolution, --root-cause, --comment) |
36
39
  | `openclaw shield monitor` | Case notification cron — status, --on, --off, --interval |
@@ -78,13 +81,17 @@ Proceed normally. No onboarding message needed.
78
81
 
79
82
  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
83
 
84
+ 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.
85
+
86
+ 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.
87
+
81
88
  ## Case Investigation Workflow
82
89
 
83
90
  When a Shield case fires, correlate three data sources to determine true positive vs. false positive:
84
91
 
85
92
  **Step 1 — Case detail** (`openclaw shield cases show <CASE_ID>`): What triggered the rule. Note the case timestamp — it anchors the correlation window.
86
93
 
87
- **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.
94
+ **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.
88
95
 
89
96
  **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.
90
97