@upx-us/shield 0.7.12 → 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
  }
@@ -342,7 +377,7 @@ function restoreFromBackup(backupPath) {
342
377
  return false;
343
378
  }
344
379
  }
345
- function updateOpenClawPluginMetadata(newVersion, shasum) {
380
+ function updateOpenClawPluginMetadata(newVersion, shasum, tarballBuffer) {
346
381
  const configPath = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'openclaw.json');
347
382
  try {
348
383
  if (!(0, fs_1.existsSync)(configPath))
@@ -355,7 +390,13 @@ function updateOpenClawPluginMetadata(newVersion, shasum) {
355
390
  install.version = newVersion;
356
391
  install.resolvedVersion = newVersion;
357
392
  install.resolvedSpec = `${PACKAGE_NAME}@${newVersion}`;
358
- delete install.integrity;
393
+ if (tarballBuffer) {
394
+ const integrityHash = (0, crypto_1.createHash)('sha512').update(tarballBuffer).digest('base64');
395
+ install.integrity = `sha512-${integrityHash}`;
396
+ }
397
+ else {
398
+ delete install.integrity;
399
+ }
359
400
  if (shasum) {
360
401
  install.shasum = shasum;
361
402
  }
@@ -407,7 +448,8 @@ function downloadAndInstall(targetVersion) {
407
448
  return false;
408
449
  }
409
450
  const tarball = (0, path_1.join)(tmpDir, tarballs[0]);
410
- const tarballShasum = (0, crypto_1.createHash)('sha1').update((0, fs_1.readFileSync)(tarball)).digest('hex');
451
+ const tarballBuffer = (0, fs_1.readFileSync)(tarball);
452
+ const tarballShasum = (0, crypto_1.createHash)('sha1').update(tarballBuffer).digest('hex');
411
453
  const stagingDir = (0, path_1.join)(tmpDir, 'staging');
412
454
  (0, fs_1.mkdirSync)(stagingDir, { recursive: true });
413
455
  (0, child_process_1.execSync)(`tar xzf "${tarball}" -C "${stagingDir}" --strip-components=1`, {
@@ -442,7 +484,7 @@ function downloadAndInstall(targetVersion) {
442
484
  (0, fs_1.rmSync)((0, path_1.join)(PLUGIN_DIR, entry), { recursive: true, force: true });
443
485
  }
444
486
  copyRecursive(stagingDir, PLUGIN_DIR);
445
- updateOpenClawPluginMetadata(targetVersion, tarballShasum);
487
+ updateOpenClawPluginMetadata(targetVersion, tarballShasum, tarballBuffer);
446
488
  log.info('updater', `Installed ${PACKAGE_NAME}@${targetVersion} successfully`);
447
489
  return true;
448
490
  }
@@ -465,6 +507,8 @@ function performAutoUpdate(mode, checkIntervalMs) {
465
507
  log.info('updater', `Clearing stale pendingRestart — running version matches stored currentVersion (${version_1.VERSION})`);
466
508
  st.pendingRestart = false;
467
509
  st.restartAttempts = 0;
510
+ st.rollbackPending = false;
511
+ st.lastFailureStage = null;
468
512
  saveUpdateState(st);
469
513
  }
470
514
  }
@@ -492,6 +536,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
492
536
  }
493
537
  state.pendingRestart = false;
494
538
  state.restartAttempts = 0;
539
+ state.rollbackPending = false;
495
540
  saveUpdateState(state);
496
541
  }
497
542
  else {
@@ -500,10 +545,14 @@ function performAutoUpdate(mode, checkIntervalMs) {
500
545
  if (restarted) {
501
546
  state.pendingRestart = false;
502
547
  state.restartAttempts = 0;
548
+ state.rollbackPending = false;
549
+ state.lastFailureStage = null;
503
550
  saveUpdateState(state);
504
551
  }
505
552
  else {
506
553
  state.restartAttempts = (state.restartAttempts ?? 0) + 1;
554
+ state.lastFailureStage = 'restart';
555
+ state.lastFailureAt = Date.now();
507
556
  saveUpdateState(state);
508
557
  }
509
558
  }
@@ -543,11 +592,22 @@ function performAutoUpdate(mode, checkIntervalMs) {
543
592
  }
544
593
  const state = loadUpdateState();
545
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
+ }
546
607
  const backupPath = backupCurrentVersion();
547
608
  if (!backupPath) {
548
609
  state.consecutiveFailures++;
549
- state.lastError = 'Backup failed — update aborted';
550
- saveUpdateState(state);
610
+ recordUpdateFailure(state, 'backup', 'Backup failed — update aborted');
551
611
  return {
552
612
  action: 'error',
553
613
  fromVersion: version_1.VERSION,
@@ -561,8 +621,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
561
621
  log.warn('updater', 'Install failed — rolling back...');
562
622
  const restored = restoreFromBackup(backupPath);
563
623
  state.consecutiveFailures++;
564
- state.lastError = 'Install failed — rolled back';
565
- saveUpdateState(state);
624
+ recordUpdateFailure(state, restored ? 'install' : 'rollback', restored ? 'Install failed — rolled back' : 'Install failed and rollback failed', { rollbackPending: !restored });
566
625
  return {
567
626
  action: restored ? 'rollback' : 'error',
568
627
  fromVersion: version_1.VERSION,
@@ -584,8 +643,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
584
643
  log.warn('updater', 'Rolling back...');
585
644
  restoreFromBackup(backupPath);
586
645
  state.consecutiveFailures++;
587
- state.lastError = 'Post-install validation failed — rolled back';
588
- saveUpdateState(state);
646
+ recordUpdateFailure(state, 'validation', 'Post-install validation failed — rolled back');
589
647
  return {
590
648
  action: 'rollback',
591
649
  fromVersion: version_1.VERSION,
@@ -607,8 +665,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
607
665
  }
608
666
  catch { }
609
667
  state.consecutiveFailures++;
610
- state.lastError = 'Integrity check failed — rolled back';
611
- saveUpdateState(state);
668
+ recordUpdateFailure(state, 'integrity', 'Integrity check failed — rolled back');
612
669
  return noOp;
613
670
  }
614
671
  const restarted = requestGatewayRestart();
@@ -619,6 +676,9 @@ function performAutoUpdate(mode, checkIntervalMs) {
619
676
  state.lastUpdateAt = Date.now();
620
677
  state.consecutiveFailures = 0;
621
678
  state.lastError = null;
679
+ state.lastFailureStage = 'restart';
680
+ state.lastFailureAt = Date.now();
681
+ state.rollbackPending = false;
622
682
  saveUpdateState(state);
623
683
  log.warn('updater', `Gateway restart failed after update to ${check.latestVersion} — will retry on next poll cycle.`);
624
684
  return {
@@ -637,6 +697,9 @@ function performAutoUpdate(mode, checkIntervalMs) {
637
697
  state.currentVersion = check.latestVersion;
638
698
  state.consecutiveFailures = 0;
639
699
  state.lastError = null;
700
+ state.lastFailureStage = null;
701
+ state.lastFailureAt = 0;
702
+ state.rollbackPending = false;
640
703
  saveUpdateState(state);
641
704
  log.info('updater', `✅ Auto-updated: ${version_1.VERSION} → ${check.latestVersion}. Gateway restart initiated.`);
642
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.12",
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.12",
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",