@upx-us/shield 0.7.13 → 0.8.0

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.
@@ -163,7 +163,7 @@ function loadConfig(overrides) {
163
163
  sessionDirs = discoverSessionDirs();
164
164
  }
165
165
  if (sessionDirs.length === 0) {
166
- log.warn('config', 'No OpenClaw session directories found. Run: npx shield-setup');
166
+ log.warn('config', `No OpenClaw session directories found under ${OPENCLAW_AGENTS_DIR}. Shield is loaded but has no event sources. Set OPENCLAW_AGENTS_DIR or SESSION_DIR if your gateway stores sessions elsewhere.`);
167
167
  }
168
168
  const credentials = overrides?.credentials ?? loadCredentials();
169
169
  return {
@@ -8,6 +8,7 @@ export interface SendResult {
8
8
  statusCode?: number;
9
9
  body?: string;
10
10
  eventCount: number;
11
+ failureKind?: 'missing_credentials' | 'pending_namespace' | 'needs_registration' | 'quota_exceeded' | 'http_error' | 'network_error' | 'circuit_breaker';
11
12
  pendingNamespace?: boolean;
12
13
  retryAfterMs?: number;
13
14
  needsRegistration?: boolean;
@@ -22,6 +23,9 @@ export interface InstanceScore {
22
23
  export interface ReportInstanceResult {
23
24
  ok: boolean;
24
25
  score?: InstanceScore;
26
+ statusCode?: number;
27
+ error?: string;
28
+ needsRegistration?: boolean;
25
29
  }
26
30
  export declare function reportInstance(payload: Record<string, unknown>, credentials: ShieldCredentials): Promise<ReportInstanceResult>;
27
31
  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>;
@@ -95,7 +95,7 @@ async function sendEvents(events, config) {
95
95
  const { apiUrl, instanceId, hmacSecret, shieldEnv } = config.credentials;
96
96
  if (!apiUrl || !instanceId || !hmacSecret) {
97
97
  log.error('sender', 'Missing credentials (apiUrl, instanceId, or hmacSecret). Run: npx shield-setup');
98
- return [{ success: false, statusCode: 0, body: 'missing credentials', eventCount: events.length }];
98
+ return [{ success: false, statusCode: 0, body: 'missing credentials', eventCount: events.length, failureKind: 'missing_credentials' }];
99
99
  }
100
100
  const results = [];
101
101
  let consecutiveBatchFailures = 0;
@@ -103,7 +103,7 @@ async function sendEvents(events, config) {
103
103
  if (consecutiveBatchFailures >= exports.CIRCUIT_BREAKER_THRESHOLD) {
104
104
  const remaining = events.slice(i);
105
105
  log.warn('sender', `Circuit breaker: ${consecutiveBatchFailures} consecutive failures — skipping ${remaining.length} remaining events`);
106
- results.push({ success: false, statusCode: 0, body: 'circuit breaker', eventCount: remaining.length });
106
+ results.push({ success: false, statusCode: 0, body: 'circuit breaker', eventCount: remaining.length, failureKind: 'circuit_breaker' });
107
107
  break;
108
108
  }
109
109
  if (i > 0)
@@ -144,7 +144,7 @@ async function sendEvents(events, config) {
144
144
  catch { }
145
145
  log.warn('sender', `Batch ${batchNum} — namespace pending (202). Holding events, retry in ${retryAfterMs / 1000}s`);
146
146
  results.push({ success: false, statusCode: 202, body: data, eventCount: batch.length,
147
- pendingNamespace: true, retryAfterMs });
147
+ pendingNamespace: true, retryAfterMs, failureKind: 'pending_namespace' });
148
148
  break;
149
149
  }
150
150
  if (res.status === 403) {
@@ -156,7 +156,7 @@ async function sendEvents(events, config) {
156
156
  if (needsReg) {
157
157
  log.error('sender', `Batch ${batchNum} — instance not registered (403). Shield deactivated — re-run wizard.`);
158
158
  results.push({ success: false, statusCode: 403, body: data, eventCount: batch.length,
159
- needsRegistration: true });
159
+ needsRegistration: true, failureKind: 'needs_registration' });
160
160
  break;
161
161
  }
162
162
  }
@@ -170,21 +170,21 @@ async function sendEvents(events, config) {
170
170
  }
171
171
  catch { }
172
172
  if (res.status === 402 || (res.status === 429 && parsedData?.error === 'quota_exceeded')) {
173
- log.warn('sender', `⚠ Event quota exhausted or subscription inactive — events are being dropped. Renew your subscription to resume delivery.`);
174
- results.push({ success: false, statusCode: res.status, body: data, eventCount: batch.length });
173
+ log.warn('sender', '⚠ Event quota exhausted or subscription inactive — delivery is paused. Shield will retain cursor position and retry after service is restored.');
174
+ results.push({ success: false, statusCode: res.status, body: data, eventCount: batch.length, failureKind: 'quota_exceeded' });
175
175
  break;
176
176
  }
177
177
  const safeBody = sanitizeResponseBodyForLog(data);
178
178
  log.error('sender', `Batch ${batchNum} — HTTP ${res.status}: ${safeBody}`);
179
179
  consecutiveBatchFailures++;
180
- results.push({ success: false, statusCode: res.status, body: data, eventCount: batch.length });
180
+ results.push({ success: false, statusCode: res.status, body: data, eventCount: batch.length, failureKind: 'http_error' });
181
181
  break;
182
182
  }
183
183
  catch (err) {
184
184
  log.error('sender', `Batch ${batchNum} attempt ${attempt + 1} — ${errMsg(err)}`);
185
185
  if (attempt === 1) {
186
186
  consecutiveBatchFailures++;
187
- results.push({ success: false, statusCode: 0, body: errMsg(err), eventCount: batch.length });
187
+ results.push({ success: false, statusCode: 0, body: errMsg(err), eventCount: batch.length, failureKind: 'network_error' });
188
188
  }
189
189
  }
190
190
  }
@@ -195,7 +195,7 @@ async function reportInstance(payload, credentials) {
195
195
  const { apiUrl, instanceId, hmacSecret, shieldEnv } = credentials;
196
196
  if (!apiUrl || !instanceId || !hmacSecret) {
197
197
  log.warn('sender', 'reportInstance: missing credentials, skipping');
198
- return { ok: false };
198
+ return { ok: false, statusCode: 0, error: 'missing credentials' };
199
199
  }
200
200
  const nonce = generateNonce();
201
201
  const signature = signRequest(instanceId, nonce, hmacSecret);
@@ -217,7 +217,12 @@ async function reportInstance(payload, credentials) {
217
217
  if (!res.ok) {
218
218
  const body = await res.text();
219
219
  log.warn('sender', `reportInstance HTTP ${res.status}: ${sanitizeResponseBodyForLog(body)}`);
220
- return { ok: false };
220
+ let needsRegistration = false;
221
+ try {
222
+ needsRegistration = JSON.parse(body).needs_registration === true;
223
+ }
224
+ catch { }
225
+ return { ok: false, statusCode: res.status, error: body, needsRegistration };
221
226
  }
222
227
  let score;
223
228
  try {
@@ -227,11 +232,11 @@ async function reportInstance(payload, credentials) {
227
232
  }
228
233
  catch {
229
234
  }
230
- return { ok: true, score };
235
+ return { ok: true, score, statusCode: res.status };
231
236
  }
232
237
  catch (err) {
233
238
  log.warn('sender', `reportInstance error: ${errMsg(err)}`);
234
- return { ok: false };
239
+ return { ok: false, statusCode: 0, error: errMsg(err) };
235
240
  }
236
241
  }
237
242
  async function reportLifecycleEvent(type, data, credentials) {
@@ -10,6 +10,9 @@ export interface UpdateState {
10
10
  consecutiveFailures: number;
11
11
  pendingRestart: boolean;
12
12
  restartAttempts: number;
13
+ lastFailureStage: string | null;
14
+ lastFailureAt: number;
15
+ rollbackPending: boolean;
13
16
  }
14
17
  export interface UpdateCheckResult {
15
18
  updateAvailable: boolean;
@@ -33,6 +36,10 @@ export declare function classifyUpdate(current: string, candidate: string): {
33
36
  isMajor: boolean;
34
37
  };
35
38
  export declare function loadUpdateState(): UpdateState;
39
+ export declare function preflightAutoUpdateEnvironment(): {
40
+ ok: boolean;
41
+ missing: string[];
42
+ };
36
43
  export declare function saveUpdateState(state: UpdateState): void;
37
44
  export declare function checkNpmVersion(): string | null;
38
45
  export declare function checkForUpdate(overrideInterval?: number): UpdateCheckResult | null;
@@ -37,6 +37,7 @@ exports.parseSemVer = parseSemVer;
37
37
  exports.isNewerVersion = isNewerVersion;
38
38
  exports.classifyUpdate = classifyUpdate;
39
39
  exports.loadUpdateState = loadUpdateState;
40
+ exports.preflightAutoUpdateEnvironment = preflightAutoUpdateEnvironment;
40
41
  exports.saveUpdateState = saveUpdateState;
41
42
  exports.checkNpmVersion = checkNpmVersion;
42
43
  exports.checkForUpdate = checkForUpdate;
@@ -117,12 +118,43 @@ function loadUpdateState() {
117
118
  consecutiveFailures: 0,
118
119
  pendingRestart: false,
119
120
  restartAttempts: 0,
121
+ lastFailureStage: null,
122
+ lastFailureAt: 0,
123
+ rollbackPending: false,
120
124
  };
121
125
  if (!(0, fs_1.existsSync)(UPDATE_STATE_FILE))
122
126
  return defaults;
123
127
  const loaded = (0, safe_io_1.readJsonSafe)(UPDATE_STATE_FILE, {}, 'update-state');
124
128
  return { ...defaults, ...loaded };
125
129
  }
130
+ function recordUpdateFailure(state, stage, error, extra) {
131
+ state.lastFailureStage = stage;
132
+ state.lastFailureAt = Date.now();
133
+ state.lastError = error;
134
+ Object.assign(state, extra ?? {});
135
+ saveUpdateState(state);
136
+ }
137
+ function preflightAutoUpdateEnvironment() {
138
+ const checks = [
139
+ { name: 'npm', command: 'npm --version' },
140
+ { name: 'tar', command: 'tar --version' },
141
+ { name: 'openclaw', command: 'openclaw --version' },
142
+ ];
143
+ const missing = [];
144
+ for (const check of checks) {
145
+ try {
146
+ (0, child_process_1.execSync)(check.command, {
147
+ encoding: 'utf-8',
148
+ timeout: 10_000,
149
+ stdio: ['pipe', 'pipe', 'pipe'],
150
+ });
151
+ }
152
+ catch {
153
+ missing.push(check.name);
154
+ }
155
+ }
156
+ return { ok: missing.length === 0, missing };
157
+ }
126
158
  function saveUpdateState(state) {
127
159
  ensureDir((0, path_1.dirname)(UPDATE_STATE_FILE));
128
160
  (0, safe_io_1.writeJsonSafe)(UPDATE_STATE_FILE, state);
@@ -166,9 +198,8 @@ function checkForUpdate(overrideInterval) {
166
198
  state.lastCheckAt = now;
167
199
  state.currentVersion = version_1.VERSION;
168
200
  if (!latestVersion) {
169
- state.lastError = 'Failed to query npm registry';
170
201
  state.consecutiveFailures = (state.consecutiveFailures ?? 0) + 1;
171
- saveUpdateState(state);
202
+ recordUpdateFailure(state, 'check', 'Failed to query npm registry');
172
203
  const FAILURE_THRESHOLD = 3;
173
204
  if (state.consecutiveFailures >= FAILURE_THRESHOLD) {
174
205
  const nextRetryAt = new Date(now + CHECK_INTERVAL_MS).toISOString();
@@ -189,6 +220,8 @@ function checkForUpdate(overrideInterval) {
189
220
  state.updateAvailable = true;
190
221
  state.lastError = null;
191
222
  state.consecutiveFailures = 0;
223
+ state.lastFailureStage = null;
224
+ state.lastFailureAt = 0;
192
225
  saveUpdateState(state);
193
226
  const classification = classifyUpdate(version_1.VERSION, latestVersion);
194
227
  log.info('updater', `Update available: ${version_1.VERSION} → ${latestVersion} (${classification.isPatch ? 'patch' : classification.isMinor ? 'minor' : 'major'})`);
@@ -202,6 +235,8 @@ function checkForUpdate(overrideInterval) {
202
235
  state.updateAvailable = false;
203
236
  state.lastError = null;
204
237
  state.consecutiveFailures = 0;
238
+ state.lastFailureStage = null;
239
+ state.lastFailureAt = 0;
205
240
  saveUpdateState(state);
206
241
  return null;
207
242
  }
@@ -472,6 +507,8 @@ function performAutoUpdate(mode, checkIntervalMs) {
472
507
  log.info('updater', `Clearing stale pendingRestart — running version matches stored currentVersion (${version_1.VERSION})`);
473
508
  st.pendingRestart = false;
474
509
  st.restartAttempts = 0;
510
+ st.rollbackPending = false;
511
+ st.lastFailureStage = null;
475
512
  saveUpdateState(st);
476
513
  }
477
514
  }
@@ -499,6 +536,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
499
536
  }
500
537
  state.pendingRestart = false;
501
538
  state.restartAttempts = 0;
539
+ state.rollbackPending = false;
502
540
  saveUpdateState(state);
503
541
  }
504
542
  else {
@@ -507,10 +545,14 @@ function performAutoUpdate(mode, checkIntervalMs) {
507
545
  if (restarted) {
508
546
  state.pendingRestart = false;
509
547
  state.restartAttempts = 0;
548
+ state.rollbackPending = false;
549
+ state.lastFailureStage = null;
510
550
  saveUpdateState(state);
511
551
  }
512
552
  else {
513
553
  state.restartAttempts = (state.restartAttempts ?? 0) + 1;
554
+ state.lastFailureStage = 'restart';
555
+ state.lastFailureAt = Date.now();
514
556
  saveUpdateState(state);
515
557
  }
516
558
  }
@@ -550,11 +592,22 @@ function performAutoUpdate(mode, checkIntervalMs) {
550
592
  }
551
593
  const state = loadUpdateState();
552
594
  log.info('updater', `Auto-updating: ${version_1.VERSION} → ${check.latestVersion}`);
595
+ const preflight = preflightAutoUpdateEnvironment();
596
+ if (!preflight.ok) {
597
+ state.consecutiveFailures++;
598
+ recordUpdateFailure(state, 'preflight', `Missing required commands for auto-update: ${preflight.missing.join(', ')}`);
599
+ return {
600
+ action: 'error',
601
+ fromVersion: version_1.VERSION,
602
+ toVersion: check.latestVersion,
603
+ message: `Auto-update aborted: missing required commands (${preflight.missing.join(', ')})`,
604
+ requiresRestart: false,
605
+ };
606
+ }
553
607
  const backupPath = backupCurrentVersion();
554
608
  if (!backupPath) {
555
609
  state.consecutiveFailures++;
556
- state.lastError = 'Backup failed — update aborted';
557
- saveUpdateState(state);
610
+ recordUpdateFailure(state, 'backup', 'Backup failed — update aborted');
558
611
  return {
559
612
  action: 'error',
560
613
  fromVersion: version_1.VERSION,
@@ -568,8 +621,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
568
621
  log.warn('updater', 'Install failed — rolling back...');
569
622
  const restored = restoreFromBackup(backupPath);
570
623
  state.consecutiveFailures++;
571
- state.lastError = 'Install failed — rolled back';
572
- saveUpdateState(state);
624
+ recordUpdateFailure(state, restored ? 'install' : 'rollback', restored ? 'Install failed — rolled back' : 'Install failed and rollback failed', { rollbackPending: !restored });
573
625
  return {
574
626
  action: restored ? 'rollback' : 'error',
575
627
  fromVersion: version_1.VERSION,
@@ -591,8 +643,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
591
643
  log.warn('updater', 'Rolling back...');
592
644
  restoreFromBackup(backupPath);
593
645
  state.consecutiveFailures++;
594
- state.lastError = 'Post-install validation failed — rolled back';
595
- saveUpdateState(state);
646
+ recordUpdateFailure(state, 'validation', 'Post-install validation failed — rolled back');
596
647
  return {
597
648
  action: 'rollback',
598
649
  fromVersion: version_1.VERSION,
@@ -614,8 +665,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
614
665
  }
615
666
  catch { }
616
667
  state.consecutiveFailures++;
617
- state.lastError = 'Integrity check failed — rolled back';
618
- saveUpdateState(state);
668
+ recordUpdateFailure(state, 'integrity', 'Integrity check failed — rolled back');
619
669
  return noOp;
620
670
  }
621
671
  const restarted = requestGatewayRestart();
@@ -626,6 +676,9 @@ function performAutoUpdate(mode, checkIntervalMs) {
626
676
  state.lastUpdateAt = Date.now();
627
677
  state.consecutiveFailures = 0;
628
678
  state.lastError = null;
679
+ state.lastFailureStage = 'restart';
680
+ state.lastFailureAt = Date.now();
681
+ state.rollbackPending = false;
629
682
  saveUpdateState(state);
630
683
  log.warn('updater', `Gateway restart failed after update to ${check.latestVersion} — will retry on next poll cycle.`);
631
684
  return {
@@ -644,6 +697,9 @@ function performAutoUpdate(mode, checkIntervalMs) {
644
697
  state.currentVersion = check.latestVersion;
645
698
  state.consecutiveFailures = 0;
646
699
  state.lastError = null;
700
+ state.lastFailureStage = null;
701
+ state.lastFailureAt = 0;
702
+ state.rollbackPending = false;
647
703
  saveUpdateState(state);
648
704
  log.info('updater', `✅ Auto-updated: ${version_1.VERSION} → ${check.latestVersion}. Gateway restart initiated.`);
649
705
  return {
@@ -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.7.13",
5
+ "version": "0.8.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
@@ -47,7 +47,7 @@
47
47
  }
48
48
  ],
49
49
  "default": true,
50
- "description": "Auto-update mode: true (auto-update patch versions), false (disabled), or 'notify-only' (log available updates without installing)."
50
+ "description": "Auto-update mode: true (auto-update patch and minor versions with rollback safety), false (disabled), or 'notify-only' (log available updates without installing)."
51
51
  },
52
52
  "debugLog": {
53
53
  "type": "boolean",
@@ -78,7 +78,7 @@
78
78
  },
79
79
  "autoUpdate": {
80
80
  "label": "Auto-update mode",
81
- "description": "true = auto-install patch updates, 'notify-only' = log only, false = disabled"
81
+ "description": "true = auto-install patch and minor updates, 'notify-only' = log only, false = disabled"
82
82
  },
83
83
  "debugLog": {
84
84
  "label": "Debug logging",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upx-us/shield",
3
- "version": "0.7.13",
3
+ "version": "0.8.0",
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",