@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.
- package/README.md +178 -53
- package/dist/index.d.ts +13 -15
- package/dist/index.js +593 -245
- package/dist/src/config.d.ts +1 -15
- package/dist/src/config.js +3 -39
- package/dist/src/counters.d.ts +14 -0
- package/dist/src/counters.js +96 -0
- package/dist/src/events/base.d.ts +0 -25
- package/dist/src/events/base.js +0 -15
- package/dist/src/events/browser/enrich.js +1 -1
- package/dist/src/events/exec/enrich.js +0 -2
- package/dist/src/events/exec/redactions.d.ts +0 -1
- package/dist/src/events/exec/redactions.js +0 -1
- package/dist/src/events/file/enrich.js +0 -3
- package/dist/src/events/generic/index.d.ts +0 -1
- package/dist/src/events/generic/index.js +0 -1
- package/dist/src/events/index.d.ts +0 -13
- package/dist/src/events/index.js +1 -13
- package/dist/src/events/message/validations.js +0 -3
- package/dist/src/events/sessions-spawn/enrich.js +0 -1
- package/dist/src/events/sessions-spawn/event.d.ts +0 -1
- package/dist/src/events/tool-result/enrich.js +0 -1
- package/dist/src/events/tool-result/redactions.js +0 -1
- package/dist/src/events/web/enrich.d.ts +0 -4
- package/dist/src/events/web/enrich.js +6 -14
- package/dist/src/events/web/redactions.js +1 -3
- package/dist/src/fetcher.d.ts +1 -0
- package/dist/src/fetcher.js +28 -19
- package/dist/src/index.js +31 -16
- package/dist/src/log.d.ts +0 -26
- package/dist/src/log.js +1 -27
- package/dist/src/redactor/base.d.ts +0 -23
- package/dist/src/redactor/base.js +0 -7
- package/dist/src/redactor/index.d.ts +0 -15
- package/dist/src/redactor/index.js +8 -27
- package/dist/src/redactor/strategies/command.js +0 -3
- package/dist/src/redactor/strategies/hostname.js +0 -1
- package/dist/src/redactor/strategies/index.d.ts +0 -5
- package/dist/src/redactor/strategies/index.js +0 -5
- package/dist/src/redactor/strategies/path.js +3 -3
- package/dist/src/redactor/strategies/secret-key.js +33 -9
- package/dist/src/redactor/vault.d.ts +0 -19
- package/dist/src/redactor/vault.js +7 -35
- package/dist/src/sender.d.ts +12 -20
- package/dist/src/sender.js +40 -36
- package/dist/src/setup.d.ts +11 -9
- package/dist/src/setup.js +33 -32
- package/dist/src/transformer.d.ts +1 -12
- package/dist/src/transformer.js +73 -48
- package/dist/src/validator.d.ts +0 -11
- package/dist/src/validator.js +19 -25
- package/dist/src/version.js +1 -2
- package/openclaw.plugin.json +10 -2
- package/package.json +8 -3
- package/dist/src/host-collector.d.ts +0 -1
- package/dist/src/host-collector.js +0 -200
- 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
|
-
|
|
59
|
-
|
|
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 {
|
|
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;
|
|
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
|
-
|
|
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.
|
|
127
|
-
|
|
128
|
-
if (
|
|
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
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
flushRedactor();
|
|
307
|
-
state.eventsProcessed += accepted;
|
|
308
|
-
state.consecutiveFailures = 0;
|
|
617
|
+
if (telemetryHandle) {
|
|
618
|
+
clearInterval(telemetryHandle);
|
|
619
|
+
telemetryHandle = null;
|
|
309
620
|
}
|
|
310
|
-
|
|
311
|
-
|
|
621
|
+
try {
|
|
622
|
+
const { flush: fr } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
|
|
623
|
+
fr();
|
|
312
624
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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;
|
|
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 {
|
|
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
|
};
|