@upx-us/shield 0.6.4 → 0.6.5

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,20 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.6.5] — 2026-03-09
8
+
9
+ ### Added
10
+ - **Smart degradation detection in `shield status`** — `getMonitorHealth()` now implements a three-gate algorithm to eliminate false positives from transient connectivity blips:
11
+ 1. **Count gate**: `consecutiveFailures ≥ 5` (unchanged from v0.6.4)
12
+ 2. **Time gate**: first failure in the current streak must be `> 3 minutes` old — a 1-minute connectivity blip (3 failures at 1m intervals) can never trigger DEGRADED
13
+ 3. **Sticky recovery**: requires `≥ 2 consecutive successes` to clear DEGRADED — prevents flapping where one success clears it and then it degrades again next cycle
14
+ - **`MonitorHealth` interface** exported from `src/case-monitor.ts` — new fields: `status: 'ok' | 'degraded' | 'unknown'`, `consecutiveSuccesses`, `degradedSinceMs`, `lastCheckMs`, `lastErrorMessage`
15
+ - `status: 'unknown'` before the monitor has completed its first check cycle
16
+ - **`shield.status` RPC** — `caseMonitor` section now includes smart health fields: `status`, `degradedSince` (ISO string), `consecutiveFailures`, `lastError`, `lastCheck` (ISO string), and a pre-formatted `display` string (e.g. `⚠️ Case Monitor: DEGRADED — 7 consecutive failures since 2026-03-09T12:00:00.000Z. Last error: ...` or `✅ Case Monitor: ok`)
17
+ - **7 new tests** covering all degradation scenarios: unknown initial state, transient blip (count gate), time gate, true DEGRADED positive, sticky recovery (1 success not enough), recovery (2 successes clear), and RPC handler shape
18
+
19
+ ---
20
+
7
21
  ## [0.6.4] — 2026-03-09
8
22
 
9
23
  ### Fixed
package/dist/index.js CHANGED
@@ -879,6 +879,10 @@ exports.default = {
879
879
  const creds = (0, config_1.loadCredentials)();
880
880
  const activated = state.activated || hasValidCredentials(creds);
881
881
  const caseStatus = (0, case_monitor_1.getCaseMonitorStatus)();
882
+ const monitorHealth = (0, case_monitor_1.getMonitorHealth)();
883
+ const caseMonitorDisplay = monitorHealth.status === 'degraded'
884
+ ? `⚠️ Case Monitor: DEGRADED — ${monitorHealth.consecutiveFailures} consecutive failures since ${monitorHealth.degradedSinceMs ? new Date(monitorHealth.degradedSinceMs).toISOString() : 'unknown'}. Last error: ${monitorHealth.lastErrorMessage}`
885
+ : `✅ Case Monitor: ok`;
882
886
  respond(true, {
883
887
  activated,
884
888
  running: state.running,
@@ -893,6 +897,16 @@ exports.default = {
893
897
  intervalMs: caseStatus.intervalMs,
894
898
  nextCheckInMs: caseStatus.nextCheckIn,
895
899
  lastCheckAt: caseStatus.lastCheckAt,
900
+ status: monitorHealth.status,
901
+ degradedSince: monitorHealth.degradedSinceMs
902
+ ? new Date(monitorHealth.degradedSinceMs).toISOString()
903
+ : null,
904
+ consecutiveFailures: monitorHealth.consecutiveFailures,
905
+ lastError: monitorHealth.lastErrorMessage,
906
+ lastCheck: monitorHealth.lastCheckMs
907
+ ? new Date(monitorHealth.lastCheckMs).toISOString()
908
+ : null,
909
+ display: caseMonitorDisplay,
896
910
  },
897
911
  });
898
912
  });
@@ -56,10 +56,15 @@ export interface CaseDetail extends CaseSummary {
56
56
  }
57
57
  export declare function initCaseMonitor(dataDir: string): void;
58
58
  export declare function notifyCaseMonitorActivity(): void;
59
- export declare function getMonitorHealth(): {
60
- status: 'ok' | 'degraded';
59
+ export interface MonitorHealth {
60
+ status: 'ok' | 'degraded' | 'unknown';
61
61
  consecutiveFailures: number;
62
- };
62
+ consecutiveSuccesses: number;
63
+ degradedSinceMs: number | null;
64
+ lastCheckMs: number | null;
65
+ lastErrorMessage: string | null;
66
+ }
67
+ export declare function getMonitorHealth(): MonitorHealth;
63
68
  export declare function getCaseMonitorStatus(): {
64
69
  intervalMs: number;
65
70
  nextCheckIn: number;
@@ -70,4 +75,6 @@ export declare function getPendingCases(): CaseSummary[];
70
75
  export declare function acknowledgeCases(caseIds: string[]): void;
71
76
  export declare function formatCaseNotification(c: CaseSummary | CaseDetail): string;
72
77
  export declare function _resetForTesting(): void;
78
+ export declare function _simulateFailuresForTesting(count: number, firstFailureAgeMs: number): void;
79
+ export declare function _simulateSuccessForTesting(): void;
73
80
  export {};
@@ -44,6 +44,8 @@ exports.getPendingCases = getPendingCases;
44
44
  exports.acknowledgeCases = acknowledgeCases;
45
45
  exports.formatCaseNotification = formatCaseNotification;
46
46
  exports._resetForTesting = _resetForTesting;
47
+ exports._simulateFailuresForTesting = _simulateFailuresForTesting;
48
+ exports._simulateSuccessForTesting = _simulateSuccessForTesting;
47
49
  const safe_io_1 = require("./safe-io");
48
50
  const client_1 = require("./rpc/client");
49
51
  const exclusions_1 = require("./exclusions");
@@ -164,7 +166,14 @@ let _lastEventAt = 0;
164
166
  let _currentIntervalMs = MAX_CHECK_INTERVAL_MS;
165
167
  let _exclusionsWarnedOnce = false;
166
168
  let _consecutiveCheckFailures = 0;
169
+ let _consecutiveSuccesses = 0;
170
+ let _degradedSinceMs = null;
171
+ let _firstFailureInStreakMs = null;
172
+ let _lastCheckMs = null;
173
+ let _lastErrorMessage = null;
167
174
  const DEGRADED_THRESHOLD = 5;
175
+ const DEGRADED_TIME_GATE_MS = 3 * 60 * 1000;
176
+ const RECOVERY_THRESHOLD = 2;
168
177
  function computeInterval() {
169
178
  if (_lastCheckAt === 0)
170
179
  return MIN_CHECK_INTERVAL_MS;
@@ -186,11 +195,58 @@ function notifyCaseMonitorActivity() {
186
195
  _lastEventAt = Date.now();
187
196
  }
188
197
  function getMonitorHealth() {
198
+ const now = Date.now();
199
+ const isDegraded = _consecutiveCheckFailures >= DEGRADED_THRESHOLD &&
200
+ _firstFailureInStreakMs !== null &&
201
+ (now - _firstFailureInStreakMs) >= DEGRADED_TIME_GATE_MS;
202
+ if (isDegraded && _degradedSinceMs === null) {
203
+ _degradedSinceMs = now;
204
+ }
205
+ let status;
206
+ if (_lastCheckMs === null) {
207
+ status = 'unknown';
208
+ }
209
+ else if (isDegraded) {
210
+ status = 'degraded';
211
+ }
212
+ else {
213
+ status = 'ok';
214
+ }
189
215
  return {
190
- status: _consecutiveCheckFailures >= DEGRADED_THRESHOLD ? 'degraded' : 'ok',
216
+ status,
191
217
  consecutiveFailures: _consecutiveCheckFailures,
218
+ consecutiveSuccesses: _consecutiveSuccesses,
219
+ degradedSinceMs: _degradedSinceMs,
220
+ lastCheckMs: _lastCheckMs,
221
+ lastErrorMessage: _lastErrorMessage,
192
222
  };
193
223
  }
224
+ function recordCheckFailure(message) {
225
+ const now = Date.now();
226
+ if (_consecutiveCheckFailures === 0) {
227
+ _firstFailureInStreakMs = now;
228
+ }
229
+ _consecutiveCheckFailures++;
230
+ _consecutiveSuccesses = 0;
231
+ _lastCheckMs = now;
232
+ _lastErrorMessage = message;
233
+ if (_degradedSinceMs === null &&
234
+ _consecutiveCheckFailures >= DEGRADED_THRESHOLD &&
235
+ _firstFailureInStreakMs !== null &&
236
+ (now - _firstFailureInStreakMs) >= DEGRADED_TIME_GATE_MS) {
237
+ _degradedSinceMs = now;
238
+ }
239
+ }
240
+ function recordCheckSuccess() {
241
+ _consecutiveSuccesses++;
242
+ _lastCheckMs = Date.now();
243
+ _lastErrorMessage = null;
244
+ if (_consecutiveSuccesses >= RECOVERY_THRESHOLD) {
245
+ _consecutiveCheckFailures = 0;
246
+ _firstFailureInStreakMs = null;
247
+ _degradedSinceMs = null;
248
+ }
249
+ }
194
250
  function getCaseMonitorStatus() {
195
251
  const interval = computeInterval();
196
252
  const nextCheckIn = Math.max(0, (_lastCheckAt + interval) - Date.now());
@@ -220,7 +276,7 @@ async function checkForNewCases(config) {
220
276
  const result = await (0, client_1.callPlatformApi)(config, '/v1/agent/cases', params, 'GET');
221
277
  if (!result.ok) {
222
278
  if (!result.error?.includes('not configured')) {
223
- _consecutiveCheckFailures++;
279
+ recordCheckFailure(result.error ?? 'unknown error');
224
280
  log.warn('case-monitor', `Failed to check cases: ${result.error}`);
225
281
  if (_consecutiveCheckFailures >= DEGRADED_THRESHOLD) {
226
282
  log.warn('case-monitor', `[case-monitor] DEGRADED: ${_consecutiveCheckFailures} consecutive check failures — notifications may be silently dropped`);
@@ -260,11 +316,12 @@ async function checkForNewCases(config) {
260
316
  }
261
317
  state.lastCheckAt = new Date().toISOString();
262
318
  (0, safe_io_1.writeJsonSafe)(stateFile, state);
263
- _consecutiveCheckFailures = 0;
319
+ recordCheckSuccess();
264
320
  }
265
321
  catch (err) {
266
- _consecutiveCheckFailures++;
267
- log.warn('case-monitor', `Check failed: ${err instanceof Error ? err.message : String(err)}`);
322
+ const msg = err instanceof Error ? err.message : String(err);
323
+ recordCheckFailure(msg);
324
+ log.warn('case-monitor', `Check failed: ${msg}`);
268
325
  if (_consecutiveCheckFailures >= DEGRADED_THRESHOLD) {
269
326
  log.warn('case-monitor', `[case-monitor] DEGRADED: ${_consecutiveCheckFailures} consecutive check failures — notifications may be silently dropped`);
270
327
  }
@@ -369,6 +426,11 @@ function _resetForTesting() {
369
426
  _currentIntervalMs = MAX_CHECK_INTERVAL_MS;
370
427
  _exclusionsWarnedOnce = false;
371
428
  _consecutiveCheckFailures = 0;
429
+ _consecutiveSuccesses = 0;
430
+ _degradedSinceMs = null;
431
+ _firstFailureInStreakMs = null;
432
+ _lastCheckMs = null;
433
+ _lastErrorMessage = null;
372
434
  _enqueueSystemEvent = null;
373
435
  _requestHeartbeatNow = null;
374
436
  _agentId = 'main';
@@ -376,3 +438,21 @@ function _resetForTesting() {
376
438
  _directSendTo = null;
377
439
  _directSendChannel = null;
378
440
  }
441
+ function _simulateFailuresForTesting(count, firstFailureAgeMs) {
442
+ const now = Date.now();
443
+ _firstFailureInStreakMs = now - firstFailureAgeMs;
444
+ _consecutiveCheckFailures = count;
445
+ _consecutiveSuccesses = 0;
446
+ _lastCheckMs = now - firstFailureAgeMs;
447
+ _lastErrorMessage = 'simulated failure';
448
+ }
449
+ function _simulateSuccessForTesting() {
450
+ _consecutiveSuccesses++;
451
+ _lastCheckMs = Date.now();
452
+ _lastErrorMessage = null;
453
+ if (_consecutiveSuccesses >= RECOVERY_THRESHOLD) {
454
+ _consecutiveCheckFailures = 0;
455
+ _firstFailureInStreakMs = null;
456
+ _degradedSinceMs = null;
457
+ }
458
+ }
@@ -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.6.4",
5
+ "version": "0.6.5",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
@@ -78,7 +78,7 @@
78
78
  },
79
79
  "clawhub": {
80
80
  "slug": "openclaw-shield-upx",
81
- "skillVersion": "1.2.5",
81
+ "skillVersion": "0.6.5",
82
82
  "note": "ClawHub auto-increments on publish. Update this after each clawhub submission."
83
83
  }
84
- }
84
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upx-us/shield",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
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",