@upx-us/shield 0.8.0 → 0.8.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,28 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.8.2] — 2026-03-17
8
+
9
+ ### Fixed
10
+
11
+ - **Log output handling rules are now explicit and non-negotiable** — the SKILL instruction for handling sensitive log content (file paths, commands, URLs) was advisory; it is now a strict rule set that the agent must follow regardless of context.
12
+
13
+ ---
14
+
15
+ ## [0.8.1] — 2026-03-17
16
+
17
+ ### Fixed
18
+
19
+ - **Disconnected status after failed startup no longer wipes prior healthy activity** — when a new plugin runtime starts but fails before completing its first sync, Shield now preserves the last known-good state (poll time, sync time, session activity) instead of replacing it with a blank slate. Instances that appeared disconnected after a gateway reload now show accurate prior activity.
20
+ - **Plugin no longer advances into startup with partially-formed credentials** — a change in v0.7.5 caused the canonical ingest URL to be treated as a valid credential even when instance ID and HMAC secret were missing, allowing startup to proceed further than it should before failing and writing zero-state. Credential validation now requires all three fields.
21
+ - **Rapid version bumps no longer trigger multiple zero-state writes** — consecutive `openclaw.plugin.json` version changes (as seen in the v0.7.3→v0.7.6 series) could trigger multiple gateway hot-reload cycles, each writing a blank initial state before credential checks failed. The startup guard now prevents redundant state resets during re-registration.
22
+
23
+ ### Changed
24
+
25
+ - **Status output shows last known-good activity when current runtime is disconnected** — if Shield is not running, the status command now surfaces when the last healthy poll and sync occurred, giving a clearer picture of what happened before the disconnect.
26
+
27
+ ---
28
+
7
29
  ## [0.8.0] — 2026-03-17
8
30
 
9
31
  ### Fixed
package/README.md CHANGED
@@ -1,14 +1,31 @@
1
1
  # OpenClaw Shield
2
2
 
3
3
  > **OpenClaw Shield is a paid security service by UPX.**
4
- > Start your **free 30-day trial** at [upx.com/en/lp/openclaw-shield-upx](https://www.upx.com/en/lp/openclaw-shield-upx).
4
+ > Start your **free 60-day trial (no credit card required)** at [upx.com/en/lp/openclaw-shield-upx](https://www.upx.com/en/lp/openclaw-shield-upx).
5
5
 
6
- Real-time security monitoring for your OpenClaw agents — powered by the UPX Shield detection platform.
6
+ **OpenClaw Shield** is the first SIEM-powered security solution built for AI agents — not to control what they *can* do, but to detect what they *did*.
7
+
8
+ While agent sandboxes block at the gate, Shield watches from inside: every tool call, file read, command execution, and outbound request is ingested, enriched, and matched against behavioral detection rules in real time. Anomalies become cases. Cases get playbooks.
9
+
10
+ **What you get:**
11
+ - 🔍 **90+ detection rules** tuned for OpenClaw agents — continuously curated and expanded by UPX security experts
12
+ - 📋 Automated case generation with remediation playbooks
13
+ - 🔒 Cryptographic event integrity (HMAC-SHA256, AES-256-GCM)
14
+ - 🏷️ Full trigger attribution — who prompted what, when, and why
15
+ - 🧹 Built-in data redaction before events leave the host
16
+ - ☁️ **Powered by Google SecOps (Chronicle)** — the same enterprise SIEM trusted by security teams worldwide
17
+
18
+ > Shield is designed to complement the security controls built into OpenClaw and any Claw-based solution — not replace them. Zero-trust policies, sandboxed execution, and private routing control what your agent *can* do. Shield adds what they can't: a continuous forensic record of what it *did*, with behavioral detection and automated case management on top.
19
+ >
20
+ > Works alongside any hardening strategy, out of the box.
21
+ > *For example, solutions like NemoClaw control the perimeter. Shield owns the forensics.*
22
+
23
+ ---
7
24
 
8
25
  **Shield is a local collector.** It captures agent activity, redacts sensitive data, and forwards clean telemetry to the UPX platform at [uss.upx.com](https://uss.upx.com) — where security rules, correlation, alerting, playbooks, and case management give your team full visibility. The plugin itself stays lean and transparent; all the heavy lifting happens on the platform side.
9
26
 
10
27
  ```
11
- Your Agent → Shield (local: capture + redact) → UPX Platform (analysis, alerts, detections)
28
+ Your Agent → Shield (local: capture + redact) → UPX Platform → Google SecOps (detection, cases, forensics)
12
29
  ```
13
30
 
14
31
  ---
@@ -176,6 +193,8 @@ openclaw gateway restart
176
193
 
177
194
  ## What to expect after activation
178
195
 
196
+ Shield's agent skill communicates with you in your language. Alerts, case summaries, and recommendations are presented in whichever language you use — raw command output and technical identifiers (rule names, case IDs, field names) are always kept as-is.
197
+
179
198
  After restart, Shield exchanges your key for permanent credentials — this takes a few seconds. You should see your first events within the first poll cycle (~30 seconds). Within 1–2 minutes, those events will appear on the platform at [uss.upx.com](https://uss.upx.com).
180
199
 
181
200
  Run `openclaw shield status` to confirm:
@@ -183,41 +202,39 @@ Run `openclaw shield status` to confirm:
183
202
  **Just activated (first minute):**
184
203
 
185
204
  ```
186
- OpenClaw Shield — v0.3.x (5s ago)
205
+ OpenClaw Shield — v0.8.x (5s ago)
187
206
 
188
- ── Plugin Health ─────────────────────────────
189
- Connection: ✅ Connected
190
- Version: 0.3.x
207
+ ── Status ────────────────────────────────────
208
+ Running · Connected
209
+ Version: 0.8.x
191
210
  Instance: a1b2c3d4…
192
- Last poll: 5s ago
193
- Last capture: 5s ago
194
- Events sent: 3 (all-time)
195
- Quarantine: 0 (all-time)
196
- Failures: 0 (consecutive)
197
- Daemon PID: 12345
198
- Session: 0m
211
+
212
+ ── Monitoring ────────────────────────────────
213
+ Events: 3 sent · 0 quarantined
214
+ Failures: 0 poll · 0 telemetry
215
+ Session: 0m active
216
+ Last data: poll 5s ago · capture 5s ago
199
217
  ```
200
218
 
201
- A low event count and a recent `Last capture` time means Shield is working correctly. Events accumulate as your agent uses tools.
219
+ A low event count and a recent `Last data` time means Shield is working correctly. Events accumulate as your agent uses tools.
202
220
 
203
221
  > **Note:** The Activity and Redactions sections appear after your first sync cycle (~30s). If your status looks shorter than the "extended use" example below, that's normal — they'll populate automatically.
204
222
 
205
223
  **After extended use:**
206
224
 
207
225
  ```
208
- OpenClaw Shield — v0.3.x (5s ago)
226
+ OpenClaw Shield — v0.8.x (5s ago)
209
227
 
210
- ── Plugin Health ─────────────────────────────
211
- Connection: ✅ Connected
212
- Version: 0.3.x
228
+ ── Status ────────────────────────────────────
229
+ Running · Connected
230
+ Version: 0.8.x
213
231
  Instance: a1b2c3d4…
214
- Last poll: 5s ago
215
- Last capture: 14s ago
216
- Events sent: 1,842 (all-time)
217
- Quarantine: 0 (all-time)
218
- Failures: 0 (consecutive)
219
- Daemon PID: 12345
220
- Session: 4h 12m
232
+
233
+ ── Monitoring ────────────────────────────────
234
+ Events: 1,842 sent · 0 quarantined
235
+ Failures: 0 poll · 0 telemetry
236
+ Session: 4h 12m active
237
+ Last data: poll 5s ago · capture 14s ago
221
238
 
222
239
  ── Activity ──────────────────────────────────
223
240
 
@@ -238,7 +255,7 @@ OpenClaw Shield — v0.3.x (5s ago)
238
255
  **When not activated:**
239
256
 
240
257
  ```
241
- OpenClaw Shield — v0.3.x
258
+ OpenClaw Shield — v0.8.x
242
259
 
243
260
  Status: Loaded (not activated)
244
261
 
package/dist/index.js CHANGED
@@ -287,6 +287,27 @@ function hasMeaningfulRuntimeState(snapshot) {
287
287
  snapshot.lastStartupCheckpoint ||
288
288
  snapshot.instanceId);
289
289
  }
290
+ function hasHealthyRuntimeState(snapshot) {
291
+ return Boolean(snapshot.lastSuccessfulPollAt || snapshot.lastSync?.at);
292
+ }
293
+ function buildLastKnownGoodSnapshot(updatedAt) {
294
+ return {
295
+ pid: process.pid,
296
+ startedAt: state.startedAt,
297
+ lastPollAt: state.lastPollAt,
298
+ lastSuccessfulPollAt: state.lastSuccessfulPollAt,
299
+ lastCaptureAt: state.lastCaptureAt,
300
+ lastSync: state.lastSync,
301
+ instanceId: state.instanceId,
302
+ eventsProcessed: state.eventsProcessed,
303
+ quarantineCount: state.quarantineCount,
304
+ updatedAt,
305
+ version: version_1.VERSION,
306
+ lastLifecyclePhase: state.lastLifecyclePhase,
307
+ lastStopReason: state.lastStopReason,
308
+ lastStartupCheckpoint: state.lastStartupCheckpoint,
309
+ };
310
+ }
290
311
  function persistState(extra = {}) {
291
312
  flushAllTimeStats();
292
313
  if (!_stateDirty)
@@ -295,6 +316,7 @@ function persistState(extra = {}) {
295
316
  const dir = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data');
296
317
  if (!(0, fs_1.existsSync)(dir))
297
318
  (0, fs_1.mkdirSync)(dir, { recursive: true });
319
+ const previous = (0, safe_io_1.readJsonSafe)(STATUS_FILE, {}, 'status-merge');
298
320
  let countersSnapshot = {};
299
321
  try {
300
322
  countersSnapshot = {
@@ -305,13 +327,22 @@ function persistState(extra = {}) {
305
327
  };
306
328
  }
307
329
  catch { }
308
- (0, safe_io_1.writeJsonSafe)(STATUS_FILE, {
330
+ const updatedAt = Date.now();
331
+ const mergedCurrent = {
309
332
  ...state, ...extra,
310
333
  version: version_1.VERSION,
311
- updatedAt: Date.now(),
334
+ updatedAt,
312
335
  pid: process.pid,
313
336
  counters: countersSnapshot,
314
337
  allTime: readAllTimeStats(),
338
+ };
339
+ const previousLastKnownGood = previous.lastKnownGood ?? null;
340
+ const nextLastKnownGood = hasHealthyRuntimeState(state)
341
+ ? buildLastKnownGoodSnapshot(updatedAt)
342
+ : previousLastKnownGood;
343
+ (0, safe_io_1.writeJsonSafe)(STATUS_FILE, {
344
+ ...mergedCurrent,
345
+ lastKnownGood: nextLastKnownGood,
315
346
  });
316
347
  _stateDirty = false;
317
348
  }
@@ -428,10 +459,10 @@ function printActivatedStatus() {
428
459
  if (!s) {
429
460
  console.log(`OpenClaw Shield — v${version_1.VERSION}`);
430
461
  console.log('');
431
- console.log('── Plugin Health ─────────────────────────────');
432
- console.log(' Connection: ⚠️ Status unavailable');
462
+ console.log('── Status ────────────────────────────────────');
463
+ console.log(' ⚠️ Unknown · Status unavailable');
433
464
  console.log(` Version: ${version_1.VERSION}`);
434
- console.log(` Warning: ${snapshot.message ?? 'Shield has not persisted runtime state yet.'}`);
465
+ console.log(` Note: ${snapshot.message ?? 'Shield has not persisted runtime state yet.'}`);
435
466
  console.log(' - This usually means the gateway has not started the plugin yet, or state persistence is unavailable.');
436
467
  console.log(' - If this persists, run: openclaw gateway restart');
437
468
  return;
@@ -445,46 +476,85 @@ function printActivatedStatus() {
445
476
  const shortId = instanceId ? `${instanceId.slice(0, 8)}…` : '';
446
477
  const lifecyclePhase = s.lastLifecyclePhase ?? 'unknown';
447
478
  const stopReason = s.lastStopReason ?? 'unknown';
479
+ const lastKnownGood = (s.lastKnownGood ?? null);
448
480
  console.log(`OpenClaw Shield — v${s.version ?? version_1.VERSION}${ageLabel ? ` (${ageLabel})` : ''}`);
481
+ const cachedUpdate = (0, updater_1.loadUpdateState)();
449
482
  console.log('');
450
- console.log('── Plugin Health ─────────────────────────────');
451
- console.log(` Connection: ${isRunning ? '✅ Connected' : snapshot.source === 'stale' ? '⚠️ Status stale' : '❌ Disconnected'}`);
452
- console.log(` Version: ${s.version ?? version_1.VERSION}`);
453
- if (shortId)
454
- console.log(` Instance: ${shortId}`);
455
- console.log(` Last poll: ${lastPollLabel}`);
456
483
  const lastCaptureMs = s.lastCaptureAt ? Date.now() - s.lastCaptureAt : null;
457
484
  const lastCaptureLabel = s.lastCaptureAt ? fmtTime(lastCaptureMs) : null;
458
- if (lastCaptureLabel)
459
- console.log(` Last capture: ${lastCaptureLabel}`);
460
- const updateState = (0, updater_1.loadUpdateState)();
485
+ const updateState = cachedUpdate;
461
486
  const allTime = (s.allTime ?? readAllTimeStats());
462
- console.log(` Events sent: ${allTime.eventsProcessed.toLocaleString()} (all-time)`);
463
- console.log(` Quarantine: ${allTime.quarantineCount.toLocaleString()} (all-time)`);
464
- console.log(` Failures: ${s.consecutiveFailures ?? 0} (poll)`);
465
- if (s.telemetryConsecutiveFailures != null) {
466
- console.log(` Telemetry: ${s.telemetryConsecutiveFailures ?? 0} (consecutive failures)`);
467
- }
468
- console.log(` Phase: ${lifecyclePhase}`);
487
+ const phaseIcon = isRunning ? '✅' : (snapshot.source === 'stale' ? '⚠️' : '❌');
488
+ const phaseDisplay = lifecyclePhase.charAt(0).toUpperCase() + lifecyclePhase.slice(1);
489
+ const connectionDisplay = isRunning ? 'Connected' : snapshot.source === 'stale' ? 'Status stale' : 'Disconnected';
490
+ console.log('── Status ────────────────────────────────────');
491
+ console.log(` ${phaseIcon} ${phaseDisplay} · ${connectionDisplay}`);
492
+ console.log(` Version: ${s.version ?? version_1.VERSION}`);
493
+ if (shortId)
494
+ console.log(` Instance: ${shortId}`);
495
+ if (snapshot.message)
496
+ console.log(` Note: ${snapshot.message}`);
469
497
  if (stopReason && stopReason !== 'none')
470
498
  console.log(` Stop reason: ${stopReason}`);
471
- if (s.pid) {
472
- let pidAlive = false;
473
- try {
474
- process.kill(s.pid, 0);
475
- pidAlive = true;
476
- }
477
- catch { }
478
- console.log(` Daemon PID: ${s.pid}${pidAlive ? '' : ' ⚠️ stale (process not running)'}`);
479
- }
480
- if (snapshot.message) {
481
- console.log(` Status note: ${snapshot.message}`);
499
+ if (lastKnownGood && !isRunning) {
500
+ const lastGoodPoll = lastKnownGood.lastPollAt ? fmtTime(Date.now() - lastKnownGood.lastPollAt) : 'never';
501
+ const lastGoodCapture = lastKnownGood.lastCaptureAt ? fmtTime(Date.now() - lastKnownGood.lastCaptureAt) : null;
502
+ const lastGoodParts = [`poll ${lastGoodPoll}`];
503
+ if (lastGoodCapture)
504
+ lastGoodParts.push(`capture ${lastGoodCapture}`);
505
+ console.log(` Last healthy: ${lastGoodParts.join(' · ')}`);
482
506
  }
483
- if (s.lastStartupCheckpoint) {
484
- console.log(` Checkpoint: ${s.lastStartupCheckpoint}`);
507
+ console.log('');
508
+ const telFailures = s.telemetryConsecutiveFailures ?? 0;
509
+ const pollFailures = s.consecutiveFailures ?? 0;
510
+ const sessionMs = s.startedAt ? Date.now() - s.startedAt : null;
511
+ const sessionLabel = sessionMs != null
512
+ ? sessionMs < 60_000 ? `${Math.round(sessionMs / 1000)}s`
513
+ : sessionMs < 3_600_000 ? `${Math.floor(sessionMs / 60_000)}m`
514
+ : `${(sessionMs / 3_600_000).toFixed(1)}h`
515
+ : null;
516
+ const lastDataParts = [];
517
+ if (lastPollLabel !== 'never')
518
+ lastDataParts.push(`poll ${lastPollLabel}`);
519
+ if (lastCaptureLabel)
520
+ lastDataParts.push(`capture ${lastCaptureLabel}`);
521
+ console.log('── Monitoring ────────────────────────────────');
522
+ console.log(` Events: ${allTime.eventsProcessed.toLocaleString()} sent · ${allTime.quarantineCount.toLocaleString()} quarantined`);
523
+ console.log(` Failures: ${pollFailures} poll · ${telFailures} telemetry`);
524
+ const checkpointHuman = (() => {
525
+ const cp = s.lastStartupCheckpoint ?? '';
526
+ if (cp.includes('not_activated'))
527
+ return 'Not activated';
528
+ if (cp.includes('activation_failed'))
529
+ return 'Activation failed';
530
+ if (cp.includes('update_restart'))
531
+ return 'Restarting for update';
532
+ if (cp === 'checkpoint:11' || cp === 'checkpoint:9' || cp === 'checkpoint:9a')
533
+ return 'Monitoring active';
534
+ if (cp === 'checkpoint:8')
535
+ return 'Starting up';
536
+ if (cp.includes('checkpoint:') || cp === 'SIGTERM')
537
+ return 'Starting up';
538
+ if (cp.includes('registration_invalid') || cp.includes('poll_registration_invalid'))
539
+ return 'Registration invalid';
540
+ return null;
541
+ })();
542
+ if (sessionLabel)
543
+ console.log(` Session: ${sessionLabel} active${checkpointHuman ? ` (${checkpointHuman})` : ''}`);
544
+ if (lastDataParts.length > 0)
545
+ console.log(` Last data: ${lastDataParts.join(' · ')}`);
546
+ if (lastKnownGood && !isRunning) {
547
+ const lastGoodSync = lastKnownGood.lastSync?.at ? fmtTime(Date.now() - lastKnownGood.lastSync.at) : null;
548
+ const lastGoodSession = lastKnownGood.startedAt ? fmtTime(Date.now() - lastKnownGood.startedAt) : null;
549
+ const lastGoodLabel = [
550
+ lastGoodSession ? `session ${lastGoodSession}` : null,
551
+ lastGoodSync ? `sync ${lastGoodSync}` : null,
552
+ ].filter(Boolean).join(' · ');
553
+ if (lastGoodLabel)
554
+ console.log(` Last good: ${lastGoodLabel}`);
485
555
  }
486
- if (s.lastError) {
487
- console.log(` Last error: ${s.lastError}`);
556
+ if (s.eventsRetainedForRetry && Number(s.eventsRetainedForRetry) > 0) {
557
+ console.log(` Retry backlog:${String(s.eventsRetainedForRetry).padStart(3)} event(s) retained`);
488
558
  }
489
559
  if (updateState.pendingRestart || updateState.lastFailureStage || updateState.rollbackPending) {
490
560
  console.log(` Update: ${updateState.pendingRestart ? 'pending restart' : updateState.rollbackPending ? 'rollback needed' : 'attention required'}`);
@@ -493,11 +563,19 @@ function printActivatedStatus() {
493
563
  if (updateState.lastError)
494
564
  console.log(` Update error: ${updateState.lastError}`);
495
565
  }
566
+ if (s.lastError) {
567
+ console.log(` Last error: ${s.lastError}`);
568
+ }
496
569
  if (s.lastDeliveryIssue) {
497
570
  console.log(` Delivery: ${s.lastDeliveryIssue}`);
498
571
  }
499
- if (s.eventsRetainedForRetry && Number(s.eventsRetainedForRetry) > 0) {
500
- console.log(` Retry backlog:${String(s.eventsRetainedForRetry).padStart(3)} event(s) retained for retry`);
572
+ if (cachedUpdate.updateAvailable && cachedUpdate.latestVersion) {
573
+ console.log('');
574
+ console.log(`⚡ Update available: v${cachedUpdate.latestVersion}`);
575
+ console.log(` To update, run:`);
576
+ console.log(` openclaw plugins update shield`);
577
+ console.log(` openclaw gateway restart`);
578
+ console.log(` Or tell your agent: "Update the OpenClaw Shield plugin to the latest version"`);
501
579
  }
502
580
  const statusWarnings = getStatusWarnings({
503
581
  running: isRunning,
@@ -525,14 +603,6 @@ function printActivatedStatus() {
525
603
  console.log(' - If this persists, run: openclaw gateway restart');
526
604
  }
527
605
  const startedAt = s.startedAt;
528
- if (startedAt) {
529
- const uptimeMs = Date.now() - startedAt;
530
- const uptimeLabel = uptimeMs < 3_600_000
531
- ? `${Math.floor(uptimeMs / 60_000)}m`
532
- : `${(uptimeMs / 3_600_000).toFixed(1)}h`;
533
- console.log(` Session: ${uptimeLabel}`);
534
- }
535
- console.log('');
536
606
  console.log('── Activity ──────────────────────────────────');
537
607
  const BAR_CHARS = '████████████████████';
538
608
  const BAR_MAX = 8;
@@ -554,7 +624,12 @@ function printActivatedStatus() {
554
624
  }
555
625
  else {
556
626
  console.log('📡 Last sync');
557
- console.log(` ${snapshot.source === 'stale' ? 'Last known sync is unavailable because persisted state is stale.' : 'No sync yet. Bridge will send on the next poll cycle.'}`);
627
+ if (lastKnownGood?.lastSync?.at && !isRunning) {
628
+ console.log(` Current runtime has no sync yet. Last healthy sync was ${fmtTime(Date.now() - lastKnownGood.lastSync.at)}.`);
629
+ }
630
+ else {
631
+ console.log(` ${snapshot.source === 'stale' ? 'Last known sync is unavailable because persisted state is stale.' : 'No sync yet. Bridge will send on the next poll cycle.'}`);
632
+ }
558
633
  }
559
634
  const counters = (s.counters ?? {});
560
635
  const sessionEvents = counters.totalEvents ?? 0;
@@ -562,10 +637,10 @@ function printActivatedStatus() {
562
637
  const sessionRows = Object.entries(sessionTypes).sort(([, a], [, b]) => b - a);
563
638
  const sessionMax = sessionRows[0]?.[1] ?? 0;
564
639
  console.log('');
565
- const sessionLabel = startedAt
640
+ const sessionActivityLabel = startedAt
566
641
  ? `since restart ${fmtTime(Date.now() - startedAt)}`
567
642
  : 'this session';
568
- console.log(`📊 This session (${sessionLabel} — ${sessionEvents} event${sessionEvents !== 1 ? 's' : ''})`);
643
+ console.log(`📊 This session (${sessionActivityLabel} — ${sessionEvents} event${sessionEvents !== 1 ? 's' : ''})`);
569
644
  if (sessionRows.length === 0) {
570
645
  console.log(' No events recorded yet.');
571
646
  }
@@ -704,11 +779,15 @@ exports.default = {
704
779
  catch { }
705
780
  }
706
781
  };
707
- if (teardownPreviousRuntime) {
708
- pendingTeardown = teardownPreviousRuntime()
709
- .catch((err) => log.warn('shield', `Runtime cleanup before re-register failed: ${err instanceof Error ? err.message : String(err)}`));
710
- }
711
- teardownPreviousRuntime = () => cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: false });
782
+ const inheritedRuntimeTeardown = teardownPreviousRuntime;
783
+ let inheritedRuntimeHandedOff = false;
784
+ const currentRuntimeTeardown = () => cleanupRuntime({
785
+ markStopped: true,
786
+ resetGuard: true,
787
+ flushRedactor: false,
788
+ stopReason: 'runtime_replaced',
789
+ });
790
+ teardownPreviousRuntime = inheritedRuntimeTeardown;
712
791
  const deactivateForRegistrationInvalidation = async (checkpoint, message) => {
713
792
  setLifecyclePhase('deactivated', checkpoint);
714
793
  setStopReason('registration_invalidated');
@@ -922,8 +1001,23 @@ exports.default = {
922
1001
  log.info('shield', '[checkpoint:8] Dynamic imports loaded');
923
1002
  if (config.redactionEnabled)
924
1003
  initRedactor();
1004
+ if (inheritedRuntimeTeardown && !inheritedRuntimeHandedOff) {
1005
+ try {
1006
+ log.info('shield', '[checkpoint:8a] Taking over previous healthy runtime');
1007
+ pendingTeardown = inheritedRuntimeTeardown()
1008
+ .catch((err) => log.warn('shield', `Previous runtime teardown during takeover failed: ${err instanceof Error ? err.message : String(err)}`));
1009
+ await pendingTeardown.catch(() => { });
1010
+ pendingTeardown = null;
1011
+ inheritedRuntimeHandedOff = true;
1012
+ }
1013
+ catch (err) {
1014
+ log.warn('shield', `Takeover handoff failed: ${err instanceof Error ? err.message : String(err)}`);
1015
+ throw err;
1016
+ }
1017
+ }
925
1018
  state.running = true;
926
1019
  setLifecyclePhase('running', 'checkpoint:9');
1020
+ teardownPreviousRuntime = currentRuntimeTeardown;
927
1021
  persistState();
928
1022
  log.info('shield', '[checkpoint:9] state.running=true — entering poll loop');
929
1023
  const runTelemetry = async () => {
@@ -19,6 +19,11 @@ export declare function setDirectSendDispatcher(opts: {
19
19
  to: string;
20
20
  channel: string;
21
21
  }): void;
22
+ declare const SEVERITY_ORDER: Record<string, number>;
23
+ declare function formatCaseTimestamp(isoDate: string): string;
24
+ declare function buildCaseUrl(c: CaseSummary | CaseDetail): string;
25
+ declare function formatBatchCaseBlock(c: CaseSummary | CaseDetail): string;
26
+ declare function dispatchCaseNotifications(cases: (CaseSummary | CaseDetail)[]): void;
22
27
  export interface CaseSummary {
23
28
  id: string;
24
29
  rule_id: string;
@@ -77,4 +82,8 @@ export declare function formatCaseNotification(c: CaseSummary | CaseDetail): str
77
82
  export declare function _resetForTesting(): void;
78
83
  export declare function _simulateFailuresForTesting(count: number, firstFailureAgeMs: number): void;
79
84
  export declare function _simulateSuccessForTesting(): void;
80
- export {};
85
+ export { SEVERITY_ORDER as _SEVERITY_ORDER };
86
+ export declare const _formatCaseTimestamp: typeof formatCaseTimestamp;
87
+ export declare const _buildCaseUrl: typeof buildCaseUrl;
88
+ export declare const _formatBatchCaseBlock: typeof formatBatchCaseBlock;
89
+ export declare const _dispatchCaseNotifications: typeof dispatchCaseNotifications;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports._dispatchCaseNotifications = exports._formatBatchCaseBlock = exports._buildCaseUrl = exports._formatCaseTimestamp = exports._SEVERITY_ORDER = void 0;
36
37
  exports.setCaseNotificationDispatcher = setCaseNotificationDispatcher;
37
38
  exports.setDirectSendDispatcher = setDirectSendDispatcher;
38
39
  exports.initCaseMonitor = initCaseMonitor;
@@ -96,6 +97,42 @@ function setDirectSendDispatcher(opts) {
96
97
  _directSendChannel = opts.channel;
97
98
  log.info('case-monitor', `Direct send configured (channel: ${opts.channel}, to: ${opts.to})`);
98
99
  }
100
+ const SEVERITY_ORDER = {
101
+ CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3, INFO: 4,
102
+ };
103
+ exports._SEVERITY_ORDER = SEVERITY_ORDER;
104
+ function formatCaseTimestamp(isoDate) {
105
+ const d = new Date(isoDate);
106
+ if (isNaN(d.getTime()))
107
+ return isoDate;
108
+ const pad = (n) => String(n).padStart(2, '0');
109
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
110
+ }
111
+ function buildCaseUrl(c) {
112
+ if (isCaseDetail(c) && c.url)
113
+ return c.url;
114
+ return `https://uss.upx.com/cases/${c.id}`;
115
+ }
116
+ function formatBatchCaseBlock(c) {
117
+ const severityEmoji = {
118
+ CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵', INFO: 'ℹ️',
119
+ };
120
+ const emoji = severityEmoji[c.severity] || '⚠️';
121
+ const sev = c.severity || '';
122
+ const description = (isCaseDetail(c) && c.rule?.description)
123
+ ? c.rule.description
124
+ : (c.description || c.summary || '');
125
+ const ts = formatCaseTimestamp(c.created_at);
126
+ const url = buildCaseUrl(c);
127
+ const lines = [
128
+ `${emoji} ${c.rule_title} · ${sev}`,
129
+ ];
130
+ if (description)
131
+ lines.push(description);
132
+ lines.push(`🕐 ${ts}`);
133
+ lines.push(`🔗 View case: ${url}`);
134
+ return lines.join('\n');
135
+ }
99
136
  function dispatchCaseNotifications(cases) {
100
137
  if (cases.length === 0)
101
138
  return;
@@ -104,19 +141,18 @@ function dispatchCaseNotifications(cases) {
104
141
  text = formatCaseNotification(cases[0]);
105
142
  }
106
143
  else {
107
- const lines = cases.map(c => {
108
- const emoji = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
109
- const sev = c.severity ? ` (${c.severity})` : '';
110
- return `${emoji[c.severity] || '⚠️'} **${c.rule_title}**${sev} — ${c.event_count} events`;
144
+ const sorted = [...cases].sort((a, b) => {
145
+ const oa = SEVERITY_ORDER[a.severity] ?? 99;
146
+ const ob = SEVERITY_ORDER[b.severity] ?? 99;
147
+ return oa - ob;
111
148
  });
149
+ const agentLabel = _agentId || 'agent';
150
+ const separator = '─────────────────────';
151
+ const blocks = sorted.map(c => `${separator}\n${formatBatchCaseBlock(c)}`);
112
152
  text = [
113
- `⚠️ **Shield Alert — ${cases.length} New Cases**`,
114
- '',
115
- ...lines,
116
- '',
153
+ `⚠️ Shield — ${cases.length} new cases · ${agentLabel}`,
117
154
  '',
118
- '💡 **Next steps:**',
119
- ...cases.map(c => ` • _"Investigate case ${c.id}"_`),
155
+ ...blocks,
120
156
  ].join('\n');
121
157
  }
122
158
  if (_directSendFn && _directSendTo) {
@@ -456,3 +492,7 @@ function _simulateSuccessForTesting() {
456
492
  _degradedSinceMs = null;
457
493
  }
458
494
  }
495
+ exports._formatCaseTimestamp = formatCaseTimestamp;
496
+ exports._buildCaseUrl = buildCaseUrl;
497
+ exports._formatBatchCaseBlock = formatBatchCaseBlock;
498
+ exports._dispatchCaseNotifications = dispatchCaseNotifications;
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validate = validate;
4
+ const URL_REQUIRED_ACTIONS = new Set(['navigate', 'open']);
4
5
  function validate(event) {
5
- if (!event.url)
6
- return { valid: false, field: 'url', error: 'missing url' };
7
- if (!event.target?.url)
8
- return { valid: false, field: 'target.url', error: 'missing target.url' };
6
+ const action = event.tool_metadata?.browser_action ?? null;
7
+ if (URL_REQUIRED_ACTIONS.has(action)) {
8
+ if (!event.url)
9
+ return { valid: false, field: 'url', error: 'missing url' };
10
+ if (!event.target?.url)
11
+ return { valid: false, field: 'target.url', error: 'missing target.url' };
12
+ }
9
13
  return { valid: true };
10
14
  }
@@ -2,7 +2,7 @@
2
2
  "id": "shield",
3
3
  "name": "OpenClaw Shield",
4
4
  "description": "Real-time security monitoring — streams enriched, redacted security events to the Shield detection platform.",
5
- "version": "0.8.0",
5
+ "version": "0.8.2",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@upx-us/shield",
3
- "version": "0.8.0",
4
- "description": "Security monitoring plugin for OpenClaw agents — streams enriched security events to the Shield detection platform",
3
+ "version": "0.8.2",
4
+ "description": "Security monitoring and SIEM integration for OpenClaw agents — behavioral detection, case generation, and forensic audit trail via Google SecOps (Chronicle).",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: openclaw-shield-upx
3
- description: "Security monitoring and threat detection for OpenClaw agents — protect your agent with real-time SIEM, detect threats, monitor agent activity, and audit events. Use when: user asks about security status, Shield health, event logs, redaction vault, setting up agent protection, enabling SIEM, detecting threats, monitoring agent activity, or auditing agent actions. NOT for: general OS hardening, firewall config, or network security unrelated to OpenClaw agents."
3
+ description: "Security monitoring and threat detection for OpenClaw agents — powered by Google SecOps (Chronicle). Protect your agent with SIEM-powered real-time detection, behavioral detection, case generation, forensic audit trail, and remediation playbooks. Use when: user asks about security status, Shield health, event logs, redaction vault, setting up agent protection, enabling SIEM, detecting threats, monitoring agent activity, or auditing agent actions. NOT for: general OS hardening, firewall config, or network security unrelated to OpenClaw agents."
4
4
  homepage: https://www.upx.com/en/lp/openclaw-shield-upx
5
5
  source: https://www.npmjs.com/package/@upx-us/shield
6
6
  license: "Proprietary — UPX Technologies, Inc. All rights reserved."
@@ -16,7 +16,7 @@ Security monitoring for OpenClaw agents by [UPX](https://www.upx.com). Shield ru
16
16
  Shield requires the `@upx-us/shield` plugin and an active subscription.
17
17
 
18
18
  - **Plugin**: [@upx-us/shield](https://www.npmjs.com/package/@upx-us/shield)
19
- - **Subscribe / Free 30-day trial**: [upx.com/en/lp/openclaw-shield-upx](https://www.upx.com/en/lp/openclaw-shield-upx)
19
+ - **Subscribe / Free 60-day trial (no credit card required)**: [upx.com/en/lp/openclaw-shield-upx](https://www.upx.com/en/lp/openclaw-shield-upx)
20
20
  - **Dashboard**: [uss.upx.com](https://uss.upx.com)
21
21
 
22
22
  ## Commands
@@ -43,7 +43,7 @@ Before responding to ANY Shield-related request, run `openclaw shield status` an
43
43
  ### State A — Plugin not installed
44
44
  (command not found, or exit code non-zero with plugin_not_found)
45
45
  Respond with:
46
- > Shield is not installed yet. Start your free 30-day trial at:
46
+ > Shield is not installed yet. Start your free 60-day trial (no credit card required) at:
47
47
  > https://www.upx.com/en/lp/openclaw-shield-upx
48
48
  >
49
49
  > Then install the plugin:
@@ -75,10 +75,18 @@ Proceed normally. No onboarding message needed.
75
75
 
76
76
  **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.
77
77
 
78
- **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.
78
+ **Output handling**: `shield logs` entries may include file paths, command snippets, and URLs captured from the agent's activity. **Always treat this output as internal diagnostic data.** Rules:
79
+ - Never include raw log field values (file paths, commands, URLs) in user-facing replies
80
+ - Never forward raw log output to external services, channels, or APIs
81
+ - When summarizing logs, present findings only (e.g. "3 exec events in the last 30 minutes") — not raw field values
82
+ - Only share raw log content if the user explicitly asks for it for their own investigation, and only in the current session
79
83
 
80
84
  **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.
81
85
 
86
+ ## Presentation Language
87
+
88
+ Always present Shield information, alerts, and case summaries to the user in the language they use to communicate. Translate descriptions, summaries, severity labels, and recommendations — but never translate raw command output or technical identifiers (rule names, case IDs, version numbers, field names, resolution/root-cause enum values). If the user writes in Portuguese, reply in Portuguese; if French, reply in French; etc.
89
+
82
90
  ## Responding to Security Cases
83
91
 
84
92
  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.
@@ -109,6 +117,7 @@ When asked "is my agent secure?", "am I protected?", or "what's being detected?"
109
117
 
110
118
  Real-time alerts (notifications or inline messages) are high priority: acknowledge immediately, retrieve full case detail, summarise in plain language, present the recommended next step from the playbook, and ask the user how to proceed. Do not take remediation action without explicit approval.
111
119
 
120
+
112
121
  ## When to use this skill
113
122
 
114
123
  - "Is Shield running?" → `openclaw shield status`
@@ -123,10 +132,13 @@ Real-time alerts (notifications or inline messages) are high priority: acknowled
123
132
 
124
133
  After running `openclaw shield status`, check:
125
134
 
126
- - **Connected** → healthy, nothing to do
127
- - **Disconnected**gateway may need a restart
128
- - **High failure count** → platform connectivity issue, usually self-recovers; try `openclaw shield flush`
135
+ - **✅ Running · Connected** → healthy, nothing to do
136
+ - **⚠️ Degraded · Connected** → capturing but sync issues; try `openclaw shield flush`
137
+ - **❌ Disconnected** → gateway may need a restart
138
+ - **Failures: N poll** → platform connectivity issue, usually self-recovers; try `openclaw shield flush`
139
+ - **Failures: N telemetry** → instance reporting failing, monitoring still active
129
140
  - **Rising quarantine** → possible version mismatch, suggest checking for plugin updates
141
+ - **Last data: capture Xm ago** (stale) → agent may be idle, or capture pipeline issue
130
142
 
131
143
  ## RPCs
132
144