@upx-us/shield 0.2.16-beta → 0.3.4

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.
Files changed (57) hide show
  1. package/README.md +178 -53
  2. package/dist/index.d.ts +13 -15
  3. package/dist/index.js +593 -245
  4. package/dist/src/config.d.ts +1 -15
  5. package/dist/src/config.js +3 -39
  6. package/dist/src/counters.d.ts +14 -0
  7. package/dist/src/counters.js +96 -0
  8. package/dist/src/events/base.d.ts +0 -25
  9. package/dist/src/events/base.js +0 -15
  10. package/dist/src/events/browser/enrich.js +1 -1
  11. package/dist/src/events/exec/enrich.js +0 -2
  12. package/dist/src/events/exec/redactions.d.ts +0 -1
  13. package/dist/src/events/exec/redactions.js +0 -1
  14. package/dist/src/events/file/enrich.js +0 -3
  15. package/dist/src/events/generic/index.d.ts +0 -1
  16. package/dist/src/events/generic/index.js +0 -1
  17. package/dist/src/events/index.d.ts +0 -13
  18. package/dist/src/events/index.js +1 -13
  19. package/dist/src/events/message/validations.js +0 -3
  20. package/dist/src/events/sessions-spawn/enrich.js +0 -1
  21. package/dist/src/events/sessions-spawn/event.d.ts +0 -1
  22. package/dist/src/events/tool-result/enrich.js +0 -1
  23. package/dist/src/events/tool-result/redactions.js +0 -1
  24. package/dist/src/events/web/enrich.d.ts +0 -4
  25. package/dist/src/events/web/enrich.js +6 -14
  26. package/dist/src/events/web/redactions.js +1 -3
  27. package/dist/src/fetcher.d.ts +1 -0
  28. package/dist/src/fetcher.js +28 -19
  29. package/dist/src/index.js +31 -16
  30. package/dist/src/log.d.ts +0 -26
  31. package/dist/src/log.js +1 -27
  32. package/dist/src/redactor/base.d.ts +0 -23
  33. package/dist/src/redactor/base.js +0 -7
  34. package/dist/src/redactor/index.d.ts +0 -15
  35. package/dist/src/redactor/index.js +8 -27
  36. package/dist/src/redactor/strategies/command.js +0 -3
  37. package/dist/src/redactor/strategies/hostname.js +0 -1
  38. package/dist/src/redactor/strategies/index.d.ts +0 -5
  39. package/dist/src/redactor/strategies/index.js +0 -5
  40. package/dist/src/redactor/strategies/path.js +3 -3
  41. package/dist/src/redactor/strategies/secret-key.js +33 -9
  42. package/dist/src/redactor/vault.d.ts +0 -19
  43. package/dist/src/redactor/vault.js +7 -35
  44. package/dist/src/sender.d.ts +12 -20
  45. package/dist/src/sender.js +40 -36
  46. package/dist/src/setup.d.ts +11 -9
  47. package/dist/src/setup.js +33 -32
  48. package/dist/src/transformer.d.ts +1 -12
  49. package/dist/src/transformer.js +73 -48
  50. package/dist/src/validator.d.ts +0 -11
  51. package/dist/src/validator.js +19 -25
  52. package/dist/src/version.js +1 -2
  53. package/openclaw.plugin.json +10 -2
  54. package/package.json +8 -3
  55. package/dist/src/host-collector.d.ts +0 -1
  56. package/dist/src/host-collector.js +0 -200
  57. package/skills/shield/SKILL.md +0 -38
package/dist/index.js CHANGED
@@ -1,19 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * OpenClaw Shield — Plugin Entry Point
4
- *
5
- * This file is the OpenClaw plugin entry point, declared in package.json
6
- * under `openclaw.extensions`. It registers the Shield plugin with the
7
- * OpenClaw Gateway and starts the monitoring bridge as a managed service.
8
- *
9
- * The monitoring bridge runs as a background service within the Gateway
10
- * process, polling session files and forwarding enriched security events
11
- * to the Shield detection platform.
12
- *
13
- * Dual-mode design:
14
- * - Plugin mode: this file, registered via api.registerService()
15
- * - Standalone mode: src/index.ts, runs directly via `shield-bridge`
16
- */
17
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
3
  if (k2 === undefined) k2 = k;
19
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -48,6 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
48
33
  };
49
34
  })();
50
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.performAutoRegistration = performAutoRegistration;
37
+ exports.resolveInstallationKey = resolveInstallationKey;
38
+ exports.maskPluginConfigForLogs = maskPluginConfigForLogs;
39
+ exports.createSingleflightRunner = createSingleflightRunner;
40
+ exports.createStartGuard = createStartGuard;
51
41
  const config_1 = require("./src/config");
52
42
  const log_1 = require("./src/log");
53
43
  const log = __importStar(require("./src/log"));
@@ -55,22 +45,208 @@ const version_1 = require("./src/version");
55
45
  const fs_1 = require("fs");
56
46
  const path_1 = require("path");
57
47
  const os_1 = require("os");
58
- // ─── Persistent status file ──────────────────────────────────────────────────
59
- // Written by the daemon after each poll; read by the CLI status command.
48
+ const crypto_1 = require("crypto");
49
+ const counters_1 = require("./src/counters");
50
+ const SHIELD_API_URL = 'https://openclaw-shield.upx.com';
51
+ async function performAutoRegistration(installationKey) {
52
+ try {
53
+ const fingerprint = (0, crypto_1.createHash)('sha256')
54
+ .update(`${(0, os_1.hostname)()}-${(0, os_1.userInfo)().username}-${Date.now()}`)
55
+ .digest('hex');
56
+ const payload = {
57
+ installation_code: installationKey,
58
+ fingerprint,
59
+ machine: {
60
+ hostname: (0, os_1.hostname)(),
61
+ user: (0, os_1.userInfo)().username,
62
+ os: process.platform,
63
+ arch: process.arch,
64
+ node_version: process.version,
65
+ },
66
+ software: {
67
+ plugin_version: version_1.VERSION,
68
+ instance_name: `openclaw-${(0, os_1.hostname)()}`,
69
+ },
70
+ };
71
+ const response = await fetch(`${SHIELD_API_URL}/v1/register`, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'OpenClaw-Shield-Plugin/auto-register' },
74
+ body: JSON.stringify(payload),
75
+ signal: AbortSignal.timeout(15_000),
76
+ });
77
+ if (!response.ok) {
78
+ const body = await response.text().catch(() => '');
79
+ let msg = `HTTP ${response.status}`;
80
+ try {
81
+ const p = JSON.parse(body);
82
+ msg = p.message || p.error || msg;
83
+ }
84
+ catch { }
85
+ log.error('shield', `Auto-registration failed: ${msg}`);
86
+ return null;
87
+ }
88
+ const data = await response.json();
89
+ const hmacSecret = data.hmacSecret;
90
+ if (!hmacSecret) {
91
+ log.error('shield', 'Auto-registration failed: no credentials returned from platform');
92
+ return null;
93
+ }
94
+ const configDir = (0, path_1.join)(config_1.SHIELD_CONFIG_PATH, '..');
95
+ if (!(0, fs_1.existsSync)(configDir))
96
+ (0, fs_1.mkdirSync)(configDir, { recursive: true });
97
+ const envContent = [
98
+ `# Shield API Configuration — auto-generated by OpenClaw plugin on ${new Date().toISOString()}`,
99
+ `SHIELD_API_URL=${SHIELD_API_URL}`,
100
+ `SHIELD_INSTANCE_ID=${fingerprint}`,
101
+ `SHIELD_HMAC_SECRET=${hmacSecret}`,
102
+ `INSTANCE_NAME=openclaw-${(0, os_1.hostname)()}`,
103
+ ].join('\n') + '\n';
104
+ (0, fs_1.writeFileSync)(config_1.SHIELD_CONFIG_PATH, envContent, { encoding: 'utf-8', mode: 0o600 });
105
+ return { apiUrl: SHIELD_API_URL, instanceId: fingerprint, hmacSecret, shieldEnv: '' };
106
+ }
107
+ catch (err) {
108
+ log.error('shield', `Auto-registration error: ${err instanceof Error ? err.message : String(err)}`);
109
+ return null;
110
+ }
111
+ }
112
+ function resolveInstallationKey(pluginConfig) {
113
+ if (typeof pluginConfig.installationKey !== 'string')
114
+ return null;
115
+ const key = pluginConfig.installationKey.trim();
116
+ log.debug('shield', `installationKey lookup: found=${!!key}`);
117
+ return key || null;
118
+ }
119
+ const PLACEHOLDER_VALUES = new Set(['saved-secret-123', 'placeholder', 'replace_me', '']);
120
+ function hasValidCredentials(creds) {
121
+ const isPlaceholder = (v) => !v || PLACEHOLDER_VALUES.has(v.trim().toLowerCase());
122
+ return !isPlaceholder(creds.apiUrl) && !isPlaceholder(creds.instanceId) && !isPlaceholder(creds.hmacSecret);
123
+ }
124
+ const SENSITIVE_KEY_RE = /(installationkey|secret|token|password|apikey|api_key|hmac)/i;
125
+ function sanitizeConfigValue(value) {
126
+ if (Array.isArray(value))
127
+ return value.map(sanitizeConfigValue);
128
+ if (value && typeof value === 'object') {
129
+ const out = {};
130
+ for (const [k, v] of Object.entries(value)) {
131
+ out[k] = SENSITIVE_KEY_RE.test(k) ? '[REDACTED]' : sanitizeConfigValue(v);
132
+ }
133
+ return out;
134
+ }
135
+ return value;
136
+ }
137
+ function maskPluginConfigForLogs(pluginConfig) {
138
+ return sanitizeConfigValue(pluginConfig);
139
+ }
140
+ function createSingleflightRunner(task) {
141
+ let inFlight = null;
142
+ return async () => {
143
+ if (inFlight)
144
+ return inFlight;
145
+ inFlight = (async () => {
146
+ try {
147
+ await task();
148
+ }
149
+ finally {
150
+ inFlight = null;
151
+ }
152
+ })();
153
+ return inFlight;
154
+ };
155
+ }
156
+ function createStartGuard() {
157
+ let started = false;
158
+ let starting = false;
159
+ return {
160
+ begin() {
161
+ if (started || starting)
162
+ return false;
163
+ starting = true;
164
+ return true;
165
+ },
166
+ endSuccess() {
167
+ starting = false;
168
+ started = true;
169
+ },
170
+ endFailure() {
171
+ starting = false;
172
+ },
173
+ reset() {
174
+ started = false;
175
+ starting = false;
176
+ },
177
+ };
178
+ }
60
179
  const STATUS_FILE = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data', 'status.json');
180
+ const STATS_FILE = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data', 'stats.json');
181
+ let _allTimeStats = null;
182
+ let _allTimeStatsDirty = false;
183
+ function readAllTimeStats() {
184
+ if (_allTimeStats)
185
+ return _allTimeStats;
186
+ try {
187
+ if (!(0, fs_1.existsSync)(STATS_FILE)) {
188
+ _allTimeStats = { eventsProcessed: 0, quarantineCount: 0 };
189
+ return _allTimeStats;
190
+ }
191
+ _allTimeStats = JSON.parse((0, fs_1.readFileSync)(STATS_FILE, 'utf8'));
192
+ return _allTimeStats;
193
+ }
194
+ catch {
195
+ _allTimeStats = { eventsProcessed: 0, quarantineCount: 0 };
196
+ return _allTimeStats;
197
+ }
198
+ }
199
+ function writeAllTimeStats(delta) {
200
+ const current = readAllTimeStats();
201
+ if (delta.eventsProcessed)
202
+ current.eventsProcessed += delta.eventsProcessed;
203
+ if (delta.quarantineCount)
204
+ current.quarantineCount += delta.quarantineCount;
205
+ _allTimeStatsDirty = true;
206
+ }
207
+ function flushAllTimeStats() {
208
+ if (!_allTimeStatsDirty || !_allTimeStats)
209
+ return;
210
+ try {
211
+ const dir = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data');
212
+ if (!(0, fs_1.existsSync)(dir))
213
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
214
+ (0, fs_1.writeFileSync)(STATS_FILE, JSON.stringify(_allTimeStats, null, 2));
215
+ _allTimeStatsDirty = false;
216
+ }
217
+ catch { }
218
+ }
219
+ let _stateDirty = true;
220
+ function markStateDirty() { _stateDirty = true; }
61
221
  function persistState(extra = {}) {
222
+ flushAllTimeStats();
223
+ if (!_stateDirty)
224
+ return;
62
225
  try {
63
226
  const dir = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data');
64
227
  if (!(0, fs_1.existsSync)(dir))
65
228
  (0, fs_1.mkdirSync)(dir, { recursive: true });
229
+ let countersSnapshot = {};
230
+ try {
231
+ countersSnapshot = {
232
+ totalEvents: (0, counters_1.getTotalEventCount)(),
233
+ eventTypes: Object.fromEntries((0, counters_1.getEventTypeCounts)().map(({ type, count }) => [type, count])),
234
+ totalRedactions: (0, counters_1.getTotalRedactionCount)(),
235
+ redactionCategories: Object.fromEntries((0, counters_1.getRedactionCounts)().map(({ category, count }) => [category, count])),
236
+ };
237
+ }
238
+ catch { }
66
239
  (0, fs_1.writeFileSync)(STATUS_FILE, JSON.stringify({
67
240
  ...state, ...extra,
68
241
  version: version_1.VERSION,
69
242
  updatedAt: Date.now(),
70
243
  pid: process.pid,
244
+ counters: countersSnapshot,
245
+ allTime: readAllTimeStats(),
71
246
  }, null, 2));
247
+ _stateDirty = false;
72
248
  }
73
- catch { /* non-fatal */ }
249
+ catch { }
74
250
  }
75
251
  function readPersistedState() {
76
252
  try {
@@ -79,22 +255,23 @@ function readPersistedState() {
79
255
  const d = JSON.parse((0, fs_1.readFileSync)(STATUS_FILE, 'utf8'));
80
256
  const age = Date.now() - (d.updatedAt || 0);
81
257
  if (age > 10 * 60 * 1000)
82
- return null; // stale if >10min
258
+ return null;
83
259
  return d;
84
260
  }
85
261
  catch {
86
262
  return null;
87
263
  }
88
264
  }
89
- // ---------------------------------------------------------------------------
90
- // Bridge state — shared between service lifecycle, RPC, and CLI
91
- // ---------------------------------------------------------------------------
92
265
  const state = {
266
+ activated: false,
93
267
  running: false,
268
+ startedAt: 0,
94
269
  lastPollAt: 0,
95
270
  eventsProcessed: 0,
96
271
  quarantineCount: 0,
97
272
  consecutiveFailures: 0,
273
+ instanceId: '',
274
+ lastSync: null,
98
275
  };
99
276
  const MAX_BACKOFF_MS = 5 * 60 * 1000;
100
277
  const TELEMETRY_INTERVAL_MS = 5 * 60 * 1000;
@@ -104,14 +281,122 @@ function getBackoffInterval(baseMs) {
104
281
  const backoff = baseMs * Math.pow(2, Math.min(state.consecutiveFailures, 10));
105
282
  return Math.min(backoff, MAX_BACKOFF_MS);
106
283
  }
107
- // ---------------------------------------------------------------------------
108
- // Plugin export
109
- // ---------------------------------------------------------------------------
284
+ function printNotActivatedStatus() {
285
+ console.log(`OpenClaw Shield — v${version_1.VERSION}`);
286
+ console.log('');
287
+ console.log(' Status: Loaded (not activated)');
288
+ console.log('');
289
+ console.log(' To activate, provide your Installation Key:');
290
+ console.log(' 1. openclaw shield activate <YOUR_KEY>');
291
+ console.log(' 2. Add to openclaw.json:');
292
+ console.log(' plugins.entries.shield.config.installationKey = "<YOUR_KEY>"');
293
+ console.log(' Then: openclaw gateway restart');
294
+ console.log('');
295
+ console.log(' Get your key at: https://uss.upx.com → APPS → OpenClaw Shield');
296
+ }
297
+ function printActivatedStatus() {
298
+ const s = readPersistedState() ?? state;
299
+ const isRunning = Boolean(s.running);
300
+ const ageMs = s.updatedAt ? Date.now() - s.updatedAt : null;
301
+ const ageLabel = ageMs != null
302
+ ? (ageMs < 60_000 ? `${Math.round(ageMs / 1000)}s ago`
303
+ : ageMs < 3_600_000 ? `${Math.floor(ageMs / 60_000)}m ago`
304
+ : `${(ageMs / 3_600_000).toFixed(1)}h ago`)
305
+ : '';
306
+ const lastPollMs = s.lastPollAt ? Date.now() - s.lastPollAt : null;
307
+ const lastPollLabel = s.lastPollAt
308
+ ? (lastPollMs < 60_000 ? `${Math.round(lastPollMs / 1000)}s ago`
309
+ : lastPollMs < 3_600_000 ? `${Math.floor(lastPollMs / 60_000)}m ago`
310
+ : `${(lastPollMs / 3_600_000).toFixed(1)}h ago`)
311
+ : 'never';
312
+ const instanceId = s.instanceId;
313
+ const shortId = instanceId ? `${instanceId.slice(0, 8)}…` : '';
314
+ console.log(`OpenClaw Shield — v${s.version ?? version_1.VERSION}${ageLabel ? ` (${ageLabel})` : ''}`);
315
+ console.log('');
316
+ console.log('── Plugin Health ─────────────────────────────');
317
+ console.log(` Connection: ${isRunning ? '✅ Connected' : '❌ Disconnected'}`);
318
+ console.log(` Version: ${s.version ?? version_1.VERSION}`);
319
+ if (shortId)
320
+ console.log(` Instance: ${shortId}`);
321
+ console.log(` Last poll: ${lastPollLabel}`);
322
+ const allTime = (s.allTime ?? readAllTimeStats());
323
+ console.log(` Events sent: ${allTime.eventsProcessed.toLocaleString()} (all-time)`);
324
+ console.log(` Quarantine: ${allTime.quarantineCount.toLocaleString()} (all-time)`);
325
+ console.log(` Failures: ${s.consecutiveFailures ?? 0} (consecutive)`);
326
+ if (s.pid)
327
+ console.log(` Daemon PID: ${s.pid}`);
328
+ const startedAt = s.startedAt;
329
+ if (startedAt) {
330
+ const uptimeMs = Date.now() - startedAt;
331
+ const uptimeLabel = uptimeMs < 3_600_000
332
+ ? `${Math.floor(uptimeMs / 60_000)}m`
333
+ : `${(uptimeMs / 3_600_000).toFixed(1)}h`;
334
+ console.log(` Session: ${uptimeLabel}`);
335
+ }
336
+ console.log('');
337
+ console.log('── Activity ──────────────────────────────────');
338
+ const BAR_CHARS = '████████████████████';
339
+ const BAR_MAX = 8;
340
+ const bar = (count, max) => {
341
+ if (max === 0)
342
+ return '';
343
+ const filled = Math.max(1, Math.round((count / max) * BAR_MAX));
344
+ return BAR_CHARS.slice(0, filled);
345
+ };
346
+ const fmtTime = (ms) => ms < 60_000 ? `${Math.round(ms / 1000)}s ago`
347
+ : ms < 3_600_000 ? `${Math.floor(ms / 60_000)}m ago`
348
+ : `${(ms / 3_600_000).toFixed(1)}h ago`;
349
+ const lastSync = s.lastSync;
350
+ console.log('');
351
+ if (lastSync && lastSync.at) {
352
+ const syncRows = Object.entries(lastSync.eventTypes).sort(([, a], [, b]) => b - a);
353
+ const syncMax = syncRows[0]?.[1] ?? 0;
354
+ console.log(`📡 Last sync (${fmtTime(Date.now() - lastSync.at)} — ${lastSync.eventCount} event${lastSync.eventCount !== 1 ? 's' : ''})`);
355
+ for (const [type, count] of syncRows) {
356
+ console.log(` ${type.padEnd(20)} ${bar(count, syncMax).padEnd(BAR_MAX + 1)} ${count}`);
357
+ }
358
+ }
359
+ else {
360
+ console.log('📡 Last sync');
361
+ console.log(' No sync yet. Bridge will send on the next poll cycle.');
362
+ }
363
+ const counters = (s.counters ?? {});
364
+ const sessionEvents = counters.totalEvents ?? 0;
365
+ const sessionTypes = (counters.eventTypes ?? {});
366
+ const sessionRows = Object.entries(sessionTypes).sort(([, a], [, b]) => b - a);
367
+ const sessionMax = sessionRows[0]?.[1] ?? 0;
368
+ console.log('');
369
+ const sessionLabel = startedAt
370
+ ? `since restart ${fmtTime(Date.now() - startedAt)}`
371
+ : 'this session';
372
+ console.log(`📊 This session (${sessionLabel} — ${sessionEvents} event${sessionEvents !== 1 ? 's' : ''})`);
373
+ if (sessionRows.length === 0) {
374
+ console.log(' No events recorded yet.');
375
+ }
376
+ else {
377
+ for (const [type, count] of sessionRows) {
378
+ console.log(` ${type.padEnd(20)} ${bar(count, sessionMax).padEnd(BAR_MAX + 1)} ${count}`);
379
+ }
380
+ }
381
+ const totalRedactions = counters.totalRedactions ?? 0;
382
+ const redactionCategories = (counters.redactionCategories ?? {});
383
+ const redactionRows = Object.entries(redactionCategories).sort(([, a], [, b]) => b - a);
384
+ console.log('');
385
+ console.log(`🔒 Redactions (${totalRedactions > 0 ? `${totalRedactions}x this session` : 'none this session'})`);
386
+ if (redactionRows.length === 0) {
387
+ console.log(' (none)');
388
+ }
389
+ else {
390
+ for (const [category, count] of redactionRows) {
391
+ console.log(` ${`${category.toLowerCase()} data`.padEnd(24)} redacted ${count}x`);
392
+ }
393
+ console.log(' (original values never stored or transmitted)');
394
+ }
395
+ }
110
396
  exports.default = {
111
397
  id: 'shield',
112
398
  name: 'OpenClaw Shield',
113
399
  register(api) {
114
- // Wire up the Gateway logger as the log backend
115
400
  const gatewayAdapter = {
116
401
  debug(tag, msg, data) {
117
402
  api.logger.debug(`[${tag}] ${msg}${data !== undefined ? ' ' + JSON.stringify(data) : ''}`);
@@ -123,250 +408,238 @@ exports.default = {
123
408
  },
124
409
  };
125
410
  (0, log_1.setAdapter)(gatewayAdapter);
126
- const pluginConfig = (api.config ?? {});
127
- const enabled = pluginConfig.enabled !== false;
128
- if (!enabled) {
411
+ const pluginConfig = (api.pluginConfig ?? {});
412
+ log.debug('shield', 'Plugin config received', maskPluginConfigForLogs(pluginConfig));
413
+ if (pluginConfig.enabled === false) {
129
414
  log.info('shield', 'Monitoring disabled via config (enabled: false)');
130
415
  return;
131
416
  }
132
- // Build credentials from plugin config + config.env fallback (no process.env mutation)
133
- const credentials = (0, config_1.loadCredentialsFromPluginConfig)(pluginConfig);
134
- if (!credentials.apiUrl || !credentials.hmacSecret) {
135
- log.warn('shield', 'Not configured. Credentials can come from:');
136
- log.warn('shield', ' 1. Run setup wizard: npx shield-setup');
137
- log.warn('shield', ' 2. Set in plugins.entries.shield.config (openclaw.json)');
138
- log.warn('shield', ' 3. Set env vars: SHIELD_API_URL + SHIELD_HMAC_SECRET');
139
- return;
140
- }
141
- const config = (0, config_1.loadConfig)({
142
- credentials,
143
- dryRun: typeof pluginConfig.dryRun === 'boolean' ? pluginConfig.dryRun : undefined,
144
- redactionEnabled: typeof pluginConfig.redactionEnabled === 'boolean' ? pluginConfig.redactionEnabled : undefined,
145
- pollIntervalMs: typeof pluginConfig.pollIntervalMs === 'number' ? pluginConfig.pollIntervalMs : undefined,
146
- collectHostMetrics: typeof pluginConfig.collectHostMetrics === 'boolean' ? pluginConfig.collectHostMetrics : undefined,
147
- });
148
- log.info('shield', `Starting monitoring bridge v${version_1.VERSION} (poll: ${config.pollIntervalMs}ms, dryRun: ${config.dryRun})`);
149
- // -----------------------------------------------------------------------
150
- // RPC methods
151
- // -----------------------------------------------------------------------
417
+ const installationKey = resolveInstallationKey(pluginConfig);
418
+ const dryRunVal = typeof pluginConfig.dryRun === 'boolean' ? pluginConfig.dryRun : undefined;
419
+ const redactionVal = typeof pluginConfig.redactionEnabled === 'boolean' ? pluginConfig.redactionEnabled : undefined;
420
+ const pollVal = typeof pluginConfig.pollIntervalMs === 'number' ? pluginConfig.pollIntervalMs : undefined;
421
+ const hostMetricsVal = typeof pluginConfig.collectHostMetrics === 'boolean' ? pluginConfig.collectHostMetrics : undefined;
152
422
  let pollFn = null;
153
- api.registerGatewayMethod('shield.status', ({ respond }) => {
154
- respond(true, {
155
- running: state.running,
156
- lastPollAt: state.lastPollAt,
157
- eventsProcessed: state.eventsProcessed,
158
- quarantineCount: state.quarantineCount,
159
- consecutiveFailures: state.consecutiveFailures,
160
- version: version_1.VERSION,
161
- });
162
- });
163
- api.registerGatewayMethod('shield.flush', ({ respond }) => {
164
- if (!pollFn) {
165
- respond(false, { error: 'Bridge not started' });
166
- return;
167
- }
168
- pollFn()
169
- .then(() => respond(true, { flushed: true }))
170
- .catch((err) => respond(false, { error: err instanceof Error ? err.message : String(err) }));
171
- });
172
- // -----------------------------------------------------------------------
173
- // CLI commands
174
- // -----------------------------------------------------------------------
175
- api.registerCli(({ program }) => {
176
- const shield = program.command('shield');
177
- shield.command('status')
178
- .description('Show Shield monitoring status')
179
- .action(async () => {
180
- // Prefer persisted state written by the daemon; fall back to local state
181
- const s = readPersistedState() ?? state;
182
- const lastPoll = s.lastPollAt ? new Date(s.lastPollAt).toISOString() : 'never';
183
- const updatedAt = s.updatedAt ? ` (${Math.round((Date.now() - s.updatedAt) / 1000)}s ago)` : '';
184
- console.log(`Shield v${s.version ?? version_1.VERSION}${updatedAt}`);
185
- console.log(` Running: ${s.running}`);
186
- console.log(` Last poll: ${lastPoll}`);
187
- console.log(` Events: ${s.eventsProcessed}`);
188
- console.log(` Quarantine: ${s.quarantineCount}`);
189
- console.log(` Failures: ${s.consecutiveFailures}`);
190
- if (s.pid)
191
- console.log(` Daemon PID: ${s.pid}`);
192
- });
193
- shield.command('flush')
194
- .description('Trigger an immediate poll cycle')
195
- .action(async () => {
196
- if (!pollFn) {
197
- console.error('Bridge not started');
198
- return;
199
- }
200
- console.log('Flushing...');
201
- await pollFn();
202
- console.log('Done');
203
- });
204
- }, { commands: ['shield'] });
205
- // -----------------------------------------------------------------------
206
- // Register background service
207
- // -----------------------------------------------------------------------
208
423
  let pollHandle = null;
209
424
  let telemetryHandle = null;
425
+ const startGuard = createStartGuard();
426
+ let onSignalHandler = null;
210
427
  api.registerService({
211
428
  id: 'shield-monitor',
212
429
  async start() {
213
- const { fetchNewEntries, commitCursors } = await Promise.resolve().then(() => __importStar(require('./src/fetcher')));
214
- const { transformEntries, generateHostTelemetry, resolveOpenClawVersion, resolveAgentLabel } = await Promise.resolve().then(() => __importStar(require('./src/transformer')));
215
- const { sendEvents, reportInstance } = await Promise.resolve().then(() => __importStar(require('./src/sender')));
216
- const { init: initRedactor, flush: flushRedactor, redactEvent } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
217
- const { validate } = await Promise.resolve().then(() => __importStar(require('./src/validator')));
218
- if (config.redactionEnabled)
219
- initRedactor();
220
- state.running = true;
221
- persistState(); // mark daemon as running immediately
222
- // -- Telemetry --------------------------------------------------
223
- const runTelemetry = async () => {
224
- if (!state.running)
225
- return;
226
- if (config.collectHostMetrics) {
227
- const hostEvent = generateHostTelemetry();
228
- if (hostEvent) {
229
- const batch = config.redactionEnabled
230
- ? [redactEvent(hostEvent)]
231
- : [hostEvent];
232
- const results = await sendEvents(batch, config);
233
- const ok = results.every(r => r.success);
234
- log.info('shield', `Host telemetry → Chronicle: success=${ok}`);
430
+ if (!startGuard.begin()) {
431
+ log.debug('shield', 'Start requested while service is already started or in progress');
432
+ return;
433
+ }
434
+ try {
435
+ let credentials = (0, config_1.loadCredentials)();
436
+ let validCreds = hasValidCredentials(credentials);
437
+ if (!validCreds && installationKey) {
438
+ log.info('shield', 'Installation key found activating Shield (first-time setup)...');
439
+ const autoCreds = await performAutoRegistration(installationKey);
440
+ if (!autoCreds) {
441
+ log.error('shield', 'Activation failed. Verify your Installation Key and try again.');
442
+ startGuard.endFailure();
443
+ return;
235
444
  }
445
+ log.info('shield', '✅ Shield activated! Starting monitoring bridge...');
446
+ log.info('shield', ` Credentials saved to ${config_1.SHIELD_CONFIG_PATH}`);
447
+ log.info('shield', ' Tip: you can remove installationKey from config after first activation.');
448
+ credentials = autoCreds;
449
+ validCreds = true;
236
450
  }
237
- const instancePayload = {
238
- machine: {
239
- hostname: config.hostname,
240
- os: process.platform,
241
- arch: process.arch,
242
- node_version: process.version,
243
- },
244
- software: {
245
- plugin_version: version_1.VERSION,
246
- openclaw_version: resolveOpenClawVersion(),
247
- agent_label: resolveAgentLabel('main'),
248
- },
249
- };
250
- const ok = await reportInstance(instancePayload, config.credentials);
251
- log.info('shield', `Instance report → Platform: success=${ok}`);
252
- };
253
- runTelemetry().catch((err) => log.error('shield', `Telemetry error: ${err instanceof Error ? err.message : String(err)}`));
254
- telemetryHandle = setInterval(() => {
255
- runTelemetry().catch((err) => log.error('shield', `Telemetry error: ${err instanceof Error ? err.message : String(err)}`));
256
- }, TELEMETRY_INTERVAL_MS);
257
- // -- Poll -------------------------------------------------------
258
- const poll = async () => {
259
- if (!state.running)
451
+ if (!validCreds) {
452
+ log.warn('shield', 'Shield is not activated.');
453
+ log.warn('shield', ' Activate via CLI: openclaw shield activate <YOUR_KEY>');
454
+ log.warn('shield', ' Or set plugins.entries.shield.config.installationKey in openclaw.json and restart.');
455
+ log.warn('shield', ' Get your key at: https://uss.upx.com → APPS → OpenClaw Shield');
456
+ startGuard.endFailure();
260
457
  return;
261
- try {
262
- const entries = await fetchNewEntries(config);
263
- if (entries.length === 0) {
264
- // Still commit cursors so initCursorsForDir positions
265
- // (set to current file sizes) are persisted on disk.
266
- // Without this, each poll re-initialises cursors to the
267
- // NEW file size and silently skips events between polls.
268
- commitCursors(config, []);
269
- state.consecutiveFailures = 0;
458
+ }
459
+ state.activated = true;
460
+ state.startedAt = Date.now();
461
+ const config = (0, config_1.loadConfig)({
462
+ credentials,
463
+ dryRun: dryRunVal,
464
+ redactionEnabled: redactionVal,
465
+ pollIntervalMs: pollVal,
466
+ collectHostMetrics: hostMetricsVal,
467
+ });
468
+ state.instanceId = config.credentials.instanceId ?? '';
469
+ const persistedStats = readAllTimeStats();
470
+ if (persistedStats.lastSync)
471
+ state.lastSync = persistedStats.lastSync;
472
+ log.info('shield', `Starting monitoring bridge v${version_1.VERSION} (poll: ${config.pollIntervalMs}ms, dryRun: ${config.dryRun})`);
473
+ const { fetchNewEntries, commitCursors } = await Promise.resolve().then(() => __importStar(require('./src/fetcher')));
474
+ const { transformEntries, generateHostTelemetry, resolveOpenClawVersion, resolveAgentLabel } = await Promise.resolve().then(() => __importStar(require('./src/transformer')));
475
+ const { sendEvents, reportInstance } = await Promise.resolve().then(() => __importStar(require('./src/sender')));
476
+ const { init: initRedactor, flush: flushRedactor, redactEvent } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
477
+ const { validate } = await Promise.resolve().then(() => __importStar(require('./src/validator')));
478
+ if (config.redactionEnabled)
479
+ initRedactor();
480
+ state.running = true;
481
+ persistState();
482
+ const runTelemetry = async () => {
483
+ if (!state.running)
484
+ return;
485
+ const hostSnapshot = config.collectHostMetrics ? generateHostTelemetry() : null;
486
+ const hostMeta = hostSnapshot?.event?.tool_metadata;
487
+ const instancePayload = {
488
+ machine: {
489
+ hostname: config.hostname,
490
+ os: process.platform,
491
+ arch: process.arch,
492
+ node_version: process.version,
493
+ },
494
+ software: {
495
+ plugin_version: version_1.VERSION,
496
+ openclaw_version: resolveOpenClawVersion(),
497
+ agent_label: resolveAgentLabel('main'),
498
+ ...(hostMeta && {
499
+ gateway_bind: hostMeta['openclaw.gateway_bind'],
500
+ webhook_configured: hostMeta['openclaw.webhook_configured'],
501
+ browser_auth_required: hostMeta['openclaw.browser_auth_required'],
502
+ }),
503
+ },
504
+ };
505
+ const result = await reportInstance(instancePayload, config.credentials);
506
+ log.info('shield', `Instance report → Platform: success=${result.ok}`);
507
+ };
508
+ const runTelemetrySingleflight = createSingleflightRunner(runTelemetry);
509
+ runTelemetrySingleflight().catch((err) => log.error('shield', `Telemetry error: ${err instanceof Error ? err.message : String(err)}`));
510
+ telemetryHandle = setInterval(() => {
511
+ runTelemetrySingleflight().catch((err) => log.error('shield', `Telemetry error: ${err instanceof Error ? err.message : String(err)}`));
512
+ }, TELEMETRY_INTERVAL_MS);
513
+ const poll = async () => {
514
+ if (!state.running)
515
+ return;
516
+ try {
517
+ const entries = await fetchNewEntries(config);
518
+ if (entries.length === 0) {
519
+ commitCursors(config, []);
520
+ state.consecutiveFailures = 0;
521
+ state.lastPollAt = Date.now();
522
+ markStateDirty();
523
+ persistState();
524
+ return;
525
+ }
526
+ let envelopes = transformEntries(entries);
527
+ const { valid: validEvents, quarantined } = validate(envelopes.map(e => e.event));
528
+ if (quarantined > 0) {
529
+ state.quarantineCount += quarantined;
530
+ markStateDirty();
531
+ writeAllTimeStats({ quarantineCount: quarantined });
532
+ log.warn('shield', `${quarantined} events quarantined (see ~/.openclaw/shield/data/quarantine.jsonl)`);
533
+ }
534
+ envelopes = envelopes.filter(e => validEvents.includes(e.event));
535
+ if (config.redactionEnabled) {
536
+ envelopes = envelopes.map(e => redactEvent(e));
537
+ }
538
+ const results = await sendEvents(envelopes, config);
539
+ const needsReg = results.some(r => r.needsRegistration);
540
+ if (needsReg) {
541
+ log.error('shield', 'Instance not registered on platform — Shield deactivated.');
542
+ state.running = false;
543
+ markStateDirty();
544
+ return;
545
+ }
546
+ const pending = results.find(r => r.pendingNamespace);
547
+ if (pending) {
548
+ const waitMs = Math.min(pending.retryAfterMs ?? 300_000, MAX_BACKOFF_MS);
549
+ log.warn('shield', `Namespace allocation in progress — holding events, backing off ${Math.round(waitMs / 1000)}s`);
550
+ state.lastPollAt = Date.now();
551
+ markStateDirty();
552
+ persistState();
553
+ await new Promise(r => setTimeout(r, waitMs));
554
+ return;
555
+ }
556
+ const accepted = results.reduce((sum, r) => sum + (r.success ? r.eventCount : 0), 0);
557
+ if (accepted > 0) {
558
+ commitCursors(config, entries);
559
+ flushRedactor();
560
+ state.eventsProcessed += accepted;
561
+ state.consecutiveFailures = 0;
562
+ markStateDirty();
563
+ const syncEventTypes = {};
564
+ for (const env of envelopes) {
565
+ const t = env.event.tool_category ?? 'UNKNOWN';
566
+ syncEventTypes[t] = (syncEventTypes[t] ?? 0) + 1;
567
+ }
568
+ const lastSync = { at: Date.now(), eventCount: accepted, eventTypes: syncEventTypes };
569
+ state.lastSync = lastSync;
570
+ writeAllTimeStats({ eventsProcessed: accepted, lastSync });
571
+ }
572
+ else {
573
+ state.consecutiveFailures++;
574
+ markStateDirty();
575
+ }
270
576
  state.lastPollAt = Date.now();
577
+ markStateDirty();
271
578
  persistState();
272
- return;
273
579
  }
274
- let envelopes = transformEntries(entries);
275
- const { valid: validEvents, quarantined } = validate(envelopes.map(e => e.event));
276
- if (quarantined > 0) {
277
- state.quarantineCount += quarantined;
278
- log.warn('shield', `${quarantined} events quarantined (see ~/.openclaw/shield/data/quarantine.jsonl)`);
279
- }
280
- envelopes = envelopes.filter(e => validEvents.includes(e.event));
281
- if (config.redactionEnabled) {
282
- envelopes = envelopes.map(e => redactEvent(e));
580
+ catch (err) {
581
+ state.consecutiveFailures++;
582
+ markStateDirty();
583
+ log.error('shield', `Poll error: ${err instanceof Error ? err.message : String(err)}`);
283
584
  }
284
- const results = await sendEvents(envelopes, config);
285
- // 403 needs_registration → self-halt cleanly (no point retrying)
286
- const needsReg = results.some(r => r.needsRegistration);
287
- if (needsReg) {
288
- log.error('shield', 'Instance not registered on platform — Shield deactivated. Re-run: npx shield-setup');
289
- state.running = false;
585
+ };
586
+ const runPollSingleflight = createSingleflightRunner(poll);
587
+ pollFn = runPollSingleflight;
588
+ await runPollSingleflight();
589
+ const schedulePoll = () => {
590
+ if (!state.running)
290
591
  return;
592
+ const interval = getBackoffInterval(config.pollIntervalMs);
593
+ if (interval !== config.pollIntervalMs) {
594
+ log.warn('shield', `Backing off: next poll in ${Math.round(interval / 1000)}s (${state.consecutiveFailures} consecutive failures)`);
291
595
  }
292
- // 202 pending_namespace hold cursors, don't count as failure, use retry_after
293
- const pending = results.find(r => r.pendingNamespace);
294
- if (pending) {
295
- const waitMs = pending.retryAfterMs ?? 300_000;
296
- log.warn('shield', `Namespace allocation in progress — holding events, backing off ${Math.round(waitMs / 1000)}s`);
297
- state.lastPollAt = Date.now();
298
- persistState();
299
- // Override next poll interval by sleeping here (schedulePoll will use normal backoff after)
300
- await new Promise(r => setTimeout(r, waitMs));
596
+ pollHandle = setTimeout(() => {
597
+ runPollSingleflight().catch((err) => {
598
+ state.consecutiveFailures++;
599
+ log.error('shield', `Poll error (unhandled): ${err instanceof Error ? err.message : String(err)}`);
600
+ }).finally(() => {
601
+ schedulePoll();
602
+ });
603
+ }, interval);
604
+ };
605
+ schedulePoll();
606
+ onSignalHandler = async () => {
607
+ if (!state.running)
301
608
  return;
609
+ state.running = false;
610
+ startGuard.reset();
611
+ markStateDirty();
612
+ persistState();
613
+ if (pollHandle) {
614
+ clearTimeout(pollHandle);
615
+ pollHandle = null;
302
616
  }
303
- const accepted = results.reduce((sum, r) => sum + (r.success ? r.eventCount : 0), 0);
304
- if (accepted > 0) {
305
- commitCursors(config, entries);
306
- flushRedactor();
307
- state.eventsProcessed += accepted;
308
- state.consecutiveFailures = 0;
617
+ if (telemetryHandle) {
618
+ clearInterval(telemetryHandle);
619
+ telemetryHandle = null;
309
620
  }
310
- else {
311
- state.consecutiveFailures++;
621
+ try {
622
+ const { flush: fr } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
623
+ fr();
312
624
  }
313
- state.lastPollAt = Date.now();
314
- persistState();
315
- }
316
- catch (err) {
317
- state.consecutiveFailures++;
318
- log.error('shield', `Poll error: ${err instanceof Error ? err.message : String(err)}`);
319
- }
320
- };
321
- pollFn = poll;
322
- await poll();
323
- const schedulePoll = () => {
324
- if (!state.running)
325
- return;
326
- const interval = getBackoffInterval(config.pollIntervalMs);
327
- if (interval !== config.pollIntervalMs) {
328
- log.warn('shield', `Backing off: next poll in ${Math.round(interval / 1000)}s (${state.consecutiveFailures} consecutive failures)`);
329
- }
330
- pollHandle = setTimeout(() => {
331
- poll().catch((err) => {
332
- state.consecutiveFailures++;
333
- log.error('shield', `Poll error (unhandled): ${err instanceof Error ? err.message : String(err)}`);
334
- }).finally(() => {
335
- schedulePoll();
336
- });
337
- }, interval);
338
- };
339
- schedulePoll();
340
- // ── Graceful shutdown on process signals ──────────────────────────
341
- // Handles SIGTERM (gateway graceful stop) and SIGINT (Ctrl-C / dev).
342
- // Uses once() so the handler self-removes after first signal.
343
- const onSignal = async () => {
344
- if (!state.running)
345
- return; // already stopped
346
- state.running = false;
347
- persistState();
348
- if (pollHandle) {
349
- clearTimeout(pollHandle);
350
- pollHandle = null;
351
- }
352
- if (telemetryHandle) {
353
- clearInterval(telemetryHandle);
354
- telemetryHandle = null;
355
- }
356
- try {
357
- const { flush: fr } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
358
- fr();
359
- }
360
- catch { }
361
- log.info('shield', 'Service stopped (signal)');
362
- };
363
- process.once('SIGTERM', onSignal);
364
- process.once('SIGINT', onSignal);
625
+ catch { }
626
+ log.info('shield', 'Service stopped (signal)');
627
+ };
628
+ process.once('SIGTERM', onSignalHandler);
629
+ process.once('SIGINT', onSignalHandler);
630
+ startGuard.endSuccess();
631
+ }
632
+ catch (err) {
633
+ startGuard.endFailure();
634
+ throw err;
635
+ }
365
636
  },
366
637
  async stop() {
367
638
  if (!state.running)
368
- return; // already stopped by signal handler
639
+ return;
369
640
  state.running = false;
641
+ startGuard.reset();
642
+ markStateDirty();
370
643
  persistState();
371
644
  if (pollHandle) {
372
645
  clearTimeout(pollHandle);
@@ -376,13 +649,88 @@ exports.default = {
376
649
  clearInterval(telemetryHandle);
377
650
  telemetryHandle = null;
378
651
  }
652
+ if (onSignalHandler) {
653
+ process.off('SIGTERM', onSignalHandler);
654
+ process.off('SIGINT', onSignalHandler);
655
+ onSignalHandler = null;
656
+ }
379
657
  try {
380
658
  const { flush: flushRedactor } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
381
659
  flushRedactor();
382
660
  }
383
- catch { /* ignore if module not loaded */ }
661
+ catch { }
384
662
  log.info('shield', 'Service stopped');
385
663
  },
386
664
  });
665
+ api.registerGatewayMethod('shield.status', ({ respond }) => {
666
+ const creds = (0, config_1.loadCredentials)();
667
+ const activated = state.activated || hasValidCredentials(creds);
668
+ respond(true, {
669
+ activated,
670
+ running: state.running,
671
+ lastPollAt: state.lastPollAt,
672
+ eventsProcessed: state.eventsProcessed,
673
+ quarantineCount: state.quarantineCount,
674
+ consecutiveFailures: state.consecutiveFailures,
675
+ version: version_1.VERSION,
676
+ });
677
+ });
678
+ api.registerGatewayMethod('shield.flush', ({ respond }) => {
679
+ if (!pollFn) {
680
+ respond(false, { error: 'Bridge not started' });
681
+ return;
682
+ }
683
+ pollFn()
684
+ .then(() => respond(true, { flushed: true }))
685
+ .catch((err) => respond(false, { error: err instanceof Error ? err.message : String(err) }));
686
+ });
687
+ api.registerCli(({ program }) => {
688
+ const shield = program.command('shield');
689
+ shield.command('status')
690
+ .description('Show Shield monitoring status and activity')
691
+ .action(async () => {
692
+ const creds = (0, config_1.loadCredentials)();
693
+ const activated = hasValidCredentials(creds);
694
+ if (!activated) {
695
+ printNotActivatedStatus();
696
+ }
697
+ else {
698
+ printActivatedStatus();
699
+ }
700
+ });
701
+ shield.command('flush')
702
+ .description('Trigger an immediate poll cycle')
703
+ .action(async () => {
704
+ if (!pollFn) {
705
+ console.error('Bridge not started');
706
+ return;
707
+ }
708
+ console.log('Flushing...');
709
+ await pollFn();
710
+ console.log('Done');
711
+ });
712
+ shield.command('activate')
713
+ .description('Activate Shield with an Installation Key')
714
+ .argument('<key>', 'Installation Key from the Shield portal')
715
+ .action(async (key) => {
716
+ if (state.activated) {
717
+ console.log('Shield is already activated.');
718
+ return;
719
+ }
720
+ console.log('Activating Shield...');
721
+ const creds = await performAutoRegistration(key.trim());
722
+ if (creds) {
723
+ console.log('');
724
+ console.log('✅ Shield activated! Credentials saved.');
725
+ console.log(' Restart the gateway to start monitoring:');
726
+ console.log(' openclaw gateway restart');
727
+ }
728
+ else {
729
+ console.error('');
730
+ console.error('❌ Activation failed. Verify your Installation Key and try again.');
731
+ console.error(' Get your key at: https://uss.upx.com → APPS → OpenClaw Shield');
732
+ }
733
+ });
734
+ }, { commands: ['shield'] });
387
735
  },
388
736
  };