@upx-us/shield 0.7.13 โ 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -220
- package/README.md +47 -30
- package/dist/index.d.ts +64 -1
- package/dist/index.js +495 -133
- package/dist/src/case-monitor.d.ts +10 -1
- package/dist/src/case-monitor.js +50 -10
- package/dist/src/config.js +1 -1
- package/dist/src/events/browser/validations.js +8 -4
- package/dist/src/sender.d.ts +4 -0
- package/dist/src/sender.js +17 -12
- package/dist/src/updater.d.ts +7 -0
- package/dist/src/updater.js +66 -10
- package/openclaw.plugin.json +3 -3
- package/package.json +2 -2
- package/skills/shield/SKILL.md +14 -6
|
@@ -19,6 +19,11 @@ export declare function setDirectSendDispatcher(opts: {
|
|
|
19
19
|
to: string;
|
|
20
20
|
channel: string;
|
|
21
21
|
}): void;
|
|
22
|
+
declare const SEVERITY_ORDER: Record<string, number>;
|
|
23
|
+
declare function formatCaseTimestamp(isoDate: string): string;
|
|
24
|
+
declare function buildCaseUrl(c: CaseSummary | CaseDetail): string;
|
|
25
|
+
declare function formatBatchCaseBlock(c: CaseSummary | CaseDetail): string;
|
|
26
|
+
declare function dispatchCaseNotifications(cases: (CaseSummary | CaseDetail)[]): void;
|
|
22
27
|
export interface CaseSummary {
|
|
23
28
|
id: string;
|
|
24
29
|
rule_id: string;
|
|
@@ -77,4 +82,8 @@ export declare function formatCaseNotification(c: CaseSummary | CaseDetail): str
|
|
|
77
82
|
export declare function _resetForTesting(): void;
|
|
78
83
|
export declare function _simulateFailuresForTesting(count: number, firstFailureAgeMs: number): void;
|
|
79
84
|
export declare function _simulateSuccessForTesting(): void;
|
|
80
|
-
export {};
|
|
85
|
+
export { SEVERITY_ORDER as _SEVERITY_ORDER };
|
|
86
|
+
export declare const _formatCaseTimestamp: typeof formatCaseTimestamp;
|
|
87
|
+
export declare const _buildCaseUrl: typeof buildCaseUrl;
|
|
88
|
+
export declare const _formatBatchCaseBlock: typeof formatBatchCaseBlock;
|
|
89
|
+
export declare const _dispatchCaseNotifications: typeof dispatchCaseNotifications;
|
package/dist/src/case-monitor.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports._dispatchCaseNotifications = exports._formatBatchCaseBlock = exports._buildCaseUrl = exports._formatCaseTimestamp = exports._SEVERITY_ORDER = void 0;
|
|
36
37
|
exports.setCaseNotificationDispatcher = setCaseNotificationDispatcher;
|
|
37
38
|
exports.setDirectSendDispatcher = setDirectSendDispatcher;
|
|
38
39
|
exports.initCaseMonitor = initCaseMonitor;
|
|
@@ -96,6 +97,42 @@ function setDirectSendDispatcher(opts) {
|
|
|
96
97
|
_directSendChannel = opts.channel;
|
|
97
98
|
log.info('case-monitor', `Direct send configured (channel: ${opts.channel}, to: ${opts.to})`);
|
|
98
99
|
}
|
|
100
|
+
const SEVERITY_ORDER = {
|
|
101
|
+
CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3, INFO: 4,
|
|
102
|
+
};
|
|
103
|
+
exports._SEVERITY_ORDER = SEVERITY_ORDER;
|
|
104
|
+
function formatCaseTimestamp(isoDate) {
|
|
105
|
+
const d = new Date(isoDate);
|
|
106
|
+
if (isNaN(d.getTime()))
|
|
107
|
+
return isoDate;
|
|
108
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
109
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
|
|
110
|
+
}
|
|
111
|
+
function buildCaseUrl(c) {
|
|
112
|
+
if (isCaseDetail(c) && c.url)
|
|
113
|
+
return c.url;
|
|
114
|
+
return `https://uss.upx.com/cases/${c.id}`;
|
|
115
|
+
}
|
|
116
|
+
function formatBatchCaseBlock(c) {
|
|
117
|
+
const severityEmoji = {
|
|
118
|
+
CRITICAL: '๐ด', HIGH: '๐ ', MEDIUM: '๐ก', LOW: '๐ต', INFO: 'โน๏ธ',
|
|
119
|
+
};
|
|
120
|
+
const emoji = severityEmoji[c.severity] || 'โ ๏ธ';
|
|
121
|
+
const sev = c.severity || '';
|
|
122
|
+
const description = (isCaseDetail(c) && c.rule?.description)
|
|
123
|
+
? c.rule.description
|
|
124
|
+
: (c.description || c.summary || '');
|
|
125
|
+
const ts = formatCaseTimestamp(c.created_at);
|
|
126
|
+
const url = buildCaseUrl(c);
|
|
127
|
+
const lines = [
|
|
128
|
+
`${emoji} ${c.rule_title} ยท ${sev}`,
|
|
129
|
+
];
|
|
130
|
+
if (description)
|
|
131
|
+
lines.push(description);
|
|
132
|
+
lines.push(`๐ ${ts}`);
|
|
133
|
+
lines.push(`๐ View case: ${url}`);
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
99
136
|
function dispatchCaseNotifications(cases) {
|
|
100
137
|
if (cases.length === 0)
|
|
101
138
|
return;
|
|
@@ -104,19 +141,18 @@ function dispatchCaseNotifications(cases) {
|
|
|
104
141
|
text = formatCaseNotification(cases[0]);
|
|
105
142
|
}
|
|
106
143
|
else {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
return
|
|
144
|
+
const sorted = [...cases].sort((a, b) => {
|
|
145
|
+
const oa = SEVERITY_ORDER[a.severity] ?? 99;
|
|
146
|
+
const ob = SEVERITY_ORDER[b.severity] ?? 99;
|
|
147
|
+
return oa - ob;
|
|
111
148
|
});
|
|
149
|
+
const agentLabel = _agentId || 'agent';
|
|
150
|
+
const separator = 'โโโโโโโโโโโโโโโโโโโโโ';
|
|
151
|
+
const blocks = sorted.map(c => `${separator}\n${formatBatchCaseBlock(c)}`);
|
|
112
152
|
text = [
|
|
113
|
-
`โ ๏ธ
|
|
114
|
-
'',
|
|
115
|
-
...lines,
|
|
116
|
-
'',
|
|
153
|
+
`โ ๏ธ Shield โ ${cases.length} new cases ยท ${agentLabel}`,
|
|
117
154
|
'',
|
|
118
|
-
|
|
119
|
-
...cases.map(c => ` โข _"Investigate case ${c.id}"_`),
|
|
155
|
+
...blocks,
|
|
120
156
|
].join('\n');
|
|
121
157
|
}
|
|
122
158
|
if (_directSendFn && _directSendTo) {
|
|
@@ -456,3 +492,7 @@ function _simulateSuccessForTesting() {
|
|
|
456
492
|
_degradedSinceMs = null;
|
|
457
493
|
}
|
|
458
494
|
}
|
|
495
|
+
exports._formatCaseTimestamp = formatCaseTimestamp;
|
|
496
|
+
exports._buildCaseUrl = buildCaseUrl;
|
|
497
|
+
exports._formatBatchCaseBlock = formatBatchCaseBlock;
|
|
498
|
+
exports._dispatchCaseNotifications = dispatchCaseNotifications;
|
package/dist/src/config.js
CHANGED
|
@@ -163,7 +163,7 @@ function loadConfig(overrides) {
|
|
|
163
163
|
sessionDirs = discoverSessionDirs();
|
|
164
164
|
}
|
|
165
165
|
if (sessionDirs.length === 0) {
|
|
166
|
-
log.warn('config',
|
|
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 {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validate = validate;
|
|
4
|
+
const URL_REQUIRED_ACTIONS = new Set(['navigate', 'open']);
|
|
4
5
|
function validate(event) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const action = event.tool_metadata?.browser_action ?? null;
|
|
7
|
+
if (URL_REQUIRED_ACTIONS.has(action)) {
|
|
8
|
+
if (!event.url)
|
|
9
|
+
return { valid: false, field: 'url', error: 'missing url' };
|
|
10
|
+
if (!event.target?.url)
|
|
11
|
+
return { valid: false, field: 'target.url', error: 'missing target.url' };
|
|
12
|
+
}
|
|
9
13
|
return { valid: true };
|
|
10
14
|
}
|
package/dist/src/sender.d.ts
CHANGED
|
@@ -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>;
|
package/dist/src/sender.js
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
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) {
|
package/dist/src/updater.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/updater.js
CHANGED
|
@@ -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
|
-
|
|
202
|
+
recordUpdateFailure(state, 'check', 'Failed to query npm registry');
|
|
172
203
|
const FAILURE_THRESHOLD = 3;
|
|
173
204
|
if (state.consecutiveFailures >= FAILURE_THRESHOLD) {
|
|
174
205
|
const nextRetryAt = new Date(now + CHECK_INTERVAL_MS).toISOString();
|
|
@@ -189,6 +220,8 @@ function checkForUpdate(overrideInterval) {
|
|
|
189
220
|
state.updateAvailable = true;
|
|
190
221
|
state.lastError = null;
|
|
191
222
|
state.consecutiveFailures = 0;
|
|
223
|
+
state.lastFailureStage = null;
|
|
224
|
+
state.lastFailureAt = 0;
|
|
192
225
|
saveUpdateState(state);
|
|
193
226
|
const classification = classifyUpdate(version_1.VERSION, latestVersion);
|
|
194
227
|
log.info('updater', `Update available: ${version_1.VERSION} โ ${latestVersion} (${classification.isPatch ? 'patch' : classification.isMinor ? 'minor' : 'major'})`);
|
|
@@ -202,6 +235,8 @@ function checkForUpdate(overrideInterval) {
|
|
|
202
235
|
state.updateAvailable = false;
|
|
203
236
|
state.lastError = null;
|
|
204
237
|
state.consecutiveFailures = 0;
|
|
238
|
+
state.lastFailureStage = null;
|
|
239
|
+
state.lastFailureAt = 0;
|
|
205
240
|
saveUpdateState(state);
|
|
206
241
|
return null;
|
|
207
242
|
}
|
|
@@ -472,6 +507,8 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
472
507
|
log.info('updater', `Clearing stale pendingRestart โ running version matches stored currentVersion (${version_1.VERSION})`);
|
|
473
508
|
st.pendingRestart = false;
|
|
474
509
|
st.restartAttempts = 0;
|
|
510
|
+
st.rollbackPending = false;
|
|
511
|
+
st.lastFailureStage = null;
|
|
475
512
|
saveUpdateState(st);
|
|
476
513
|
}
|
|
477
514
|
}
|
|
@@ -499,6 +536,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
499
536
|
}
|
|
500
537
|
state.pendingRestart = false;
|
|
501
538
|
state.restartAttempts = 0;
|
|
539
|
+
state.rollbackPending = false;
|
|
502
540
|
saveUpdateState(state);
|
|
503
541
|
}
|
|
504
542
|
else {
|
|
@@ -507,10 +545,14 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
507
545
|
if (restarted) {
|
|
508
546
|
state.pendingRestart = false;
|
|
509
547
|
state.restartAttempts = 0;
|
|
548
|
+
state.rollbackPending = false;
|
|
549
|
+
state.lastFailureStage = null;
|
|
510
550
|
saveUpdateState(state);
|
|
511
551
|
}
|
|
512
552
|
else {
|
|
513
553
|
state.restartAttempts = (state.restartAttempts ?? 0) + 1;
|
|
554
|
+
state.lastFailureStage = 'restart';
|
|
555
|
+
state.lastFailureAt = Date.now();
|
|
514
556
|
saveUpdateState(state);
|
|
515
557
|
}
|
|
516
558
|
}
|
|
@@ -550,11 +592,22 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
550
592
|
}
|
|
551
593
|
const state = loadUpdateState();
|
|
552
594
|
log.info('updater', `Auto-updating: ${version_1.VERSION} โ ${check.latestVersion}`);
|
|
595
|
+
const preflight = preflightAutoUpdateEnvironment();
|
|
596
|
+
if (!preflight.ok) {
|
|
597
|
+
state.consecutiveFailures++;
|
|
598
|
+
recordUpdateFailure(state, 'preflight', `Missing required commands for auto-update: ${preflight.missing.join(', ')}`);
|
|
599
|
+
return {
|
|
600
|
+
action: 'error',
|
|
601
|
+
fromVersion: version_1.VERSION,
|
|
602
|
+
toVersion: check.latestVersion,
|
|
603
|
+
message: `Auto-update aborted: missing required commands (${preflight.missing.join(', ')})`,
|
|
604
|
+
requiresRestart: false,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
553
607
|
const backupPath = backupCurrentVersion();
|
|
554
608
|
if (!backupPath) {
|
|
555
609
|
state.consecutiveFailures++;
|
|
556
|
-
state
|
|
557
|
-
saveUpdateState(state);
|
|
610
|
+
recordUpdateFailure(state, 'backup', 'Backup failed โ update aborted');
|
|
558
611
|
return {
|
|
559
612
|
action: 'error',
|
|
560
613
|
fromVersion: version_1.VERSION,
|
|
@@ -568,8 +621,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
568
621
|
log.warn('updater', 'Install failed โ rolling back...');
|
|
569
622
|
const restored = restoreFromBackup(backupPath);
|
|
570
623
|
state.consecutiveFailures++;
|
|
571
|
-
state
|
|
572
|
-
saveUpdateState(state);
|
|
624
|
+
recordUpdateFailure(state, restored ? 'install' : 'rollback', restored ? 'Install failed โ rolled back' : 'Install failed and rollback failed', { rollbackPending: !restored });
|
|
573
625
|
return {
|
|
574
626
|
action: restored ? 'rollback' : 'error',
|
|
575
627
|
fromVersion: version_1.VERSION,
|
|
@@ -591,8 +643,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
591
643
|
log.warn('updater', 'Rolling back...');
|
|
592
644
|
restoreFromBackup(backupPath);
|
|
593
645
|
state.consecutiveFailures++;
|
|
594
|
-
state
|
|
595
|
-
saveUpdateState(state);
|
|
646
|
+
recordUpdateFailure(state, 'validation', 'Post-install validation failed โ rolled back');
|
|
596
647
|
return {
|
|
597
648
|
action: 'rollback',
|
|
598
649
|
fromVersion: version_1.VERSION,
|
|
@@ -614,8 +665,7 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
614
665
|
}
|
|
615
666
|
catch { }
|
|
616
667
|
state.consecutiveFailures++;
|
|
617
|
-
state
|
|
618
|
-
saveUpdateState(state);
|
|
668
|
+
recordUpdateFailure(state, 'integrity', 'Integrity check failed โ rolled back');
|
|
619
669
|
return noOp;
|
|
620
670
|
}
|
|
621
671
|
const restarted = requestGatewayRestart();
|
|
@@ -626,6 +676,9 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
626
676
|
state.lastUpdateAt = Date.now();
|
|
627
677
|
state.consecutiveFailures = 0;
|
|
628
678
|
state.lastError = null;
|
|
679
|
+
state.lastFailureStage = 'restart';
|
|
680
|
+
state.lastFailureAt = Date.now();
|
|
681
|
+
state.rollbackPending = false;
|
|
629
682
|
saveUpdateState(state);
|
|
630
683
|
log.warn('updater', `Gateway restart failed after update to ${check.latestVersion} โ will retry on next poll cycle.`);
|
|
631
684
|
return {
|
|
@@ -644,6 +697,9 @@ function performAutoUpdate(mode, checkIntervalMs) {
|
|
|
644
697
|
state.currentVersion = check.latestVersion;
|
|
645
698
|
state.consecutiveFailures = 0;
|
|
646
699
|
state.lastError = null;
|
|
700
|
+
state.lastFailureStage = null;
|
|
701
|
+
state.lastFailureAt = 0;
|
|
702
|
+
state.rollbackPending = false;
|
|
647
703
|
saveUpdateState(state);
|
|
648
704
|
log.info('updater', `โ
Auto-updated: ${version_1.VERSION} โ ${check.latestVersion}. Gateway restart initiated.`);
|
|
649
705
|
return {
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.8.1",
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upx-us/shield",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Security monitoring
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "Security monitoring and SIEM integration for OpenClaw agents โ behavioral detection, case generation, and forensic audit trail via Google SecOps (Chronicle).",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
package/skills/shield/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: openclaw-shield-upx
|
|
3
|
-
description: "Security monitoring and threat detection for OpenClaw agents โ
|
|
3
|
+
description: "Security monitoring and threat detection for OpenClaw agents โ powered by Google SecOps (Chronicle). Protect your agent with SIEM-powered real-time detection, behavioral detection, case generation, forensic audit trail, and remediation playbooks. Use when: user asks about security status, Shield health, event logs, redaction vault, setting up agent protection, enabling SIEM, detecting threats, monitoring agent activity, or auditing agent actions. NOT for: general OS hardening, firewall config, or network security unrelated to OpenClaw agents."
|
|
4
4
|
homepage: https://www.upx.com/en/lp/openclaw-shield-upx
|
|
5
5
|
source: https://www.npmjs.com/package/@upx-us/shield
|
|
6
6
|
license: "Proprietary โ UPX Technologies, Inc. All rights reserved."
|
|
@@ -16,7 +16,7 @@ Security monitoring for OpenClaw agents by [UPX](https://www.upx.com). Shield ru
|
|
|
16
16
|
Shield requires the `@upx-us/shield` plugin and an active subscription.
|
|
17
17
|
|
|
18
18
|
- **Plugin**: [@upx-us/shield](https://www.npmjs.com/package/@upx-us/shield)
|
|
19
|
-
- **Subscribe / Free
|
|
19
|
+
- **Subscribe / Free 60-day trial (no credit card required)**: [upx.com/en/lp/openclaw-shield-upx](https://www.upx.com/en/lp/openclaw-shield-upx)
|
|
20
20
|
- **Dashboard**: [uss.upx.com](https://uss.upx.com)
|
|
21
21
|
|
|
22
22
|
## Commands
|
|
@@ -43,7 +43,7 @@ Before responding to ANY Shield-related request, run `openclaw shield status` an
|
|
|
43
43
|
### State A โ Plugin not installed
|
|
44
44
|
(command not found, or exit code non-zero with plugin_not_found)
|
|
45
45
|
Respond with:
|
|
46
|
-
> Shield is not installed yet. Start your free
|
|
46
|
+
> Shield is not installed yet. Start your free 60-day trial (no credit card required) at:
|
|
47
47
|
> https://www.upx.com/en/lp/openclaw-shield-upx
|
|
48
48
|
>
|
|
49
49
|
> Then install the plugin:
|
|
@@ -79,6 +79,10 @@ Proceed normally. No onboarding message needed.
|
|
|
79
79
|
|
|
80
80
|
**Data flow disclosure**: Shield captures agent activity locally and sends redacted telemetry to the UPX detection platform for security monitoring. No credentials are handled by this skill โ authentication is managed by the plugin using the installation key configured during setup. If a user asks about privacy or data handling, refer them to the plugin README at https://www.npmjs.com/package/@upx-us/shield for full details.
|
|
81
81
|
|
|
82
|
+
## Presentation Language
|
|
83
|
+
|
|
84
|
+
Always present Shield information, alerts, and case summaries to the user in the language they use to communicate. Translate descriptions, summaries, severity labels, and recommendations โ but never translate raw command output or technical identifiers (rule names, case IDs, version numbers, field names, resolution/root-cause enum values). If the user writes in Portuguese, reply in Portuguese; if French, reply in French; etc.
|
|
85
|
+
|
|
82
86
|
## Responding to Security Cases
|
|
83
87
|
|
|
84
88
|
When a Shield case fires or the user asks about an alert: use `openclaw shield cases` to list open cases and `openclaw shield cases --id <id>` for full detail (timeline, matched events, playbook). Severity guidance: **CRITICAL/HIGH** โ surface immediately and ask if they want to investigate; **MEDIUM** โ present and offer a playbook walkthrough; **LOW/INFO** โ mention without interrupting the current task. Always include: rule name, what it detects, when it fired, and the first recommended remediation step. Confirm with the user before resolving โ never resolve autonomously.
|
|
@@ -109,6 +113,7 @@ When asked "is my agent secure?", "am I protected?", or "what's being detected?"
|
|
|
109
113
|
|
|
110
114
|
Real-time alerts (notifications or inline messages) are high priority: acknowledge immediately, retrieve full case detail, summarise in plain language, present the recommended next step from the playbook, and ask the user how to proceed. Do not take remediation action without explicit approval.
|
|
111
115
|
|
|
116
|
+
|
|
112
117
|
## When to use this skill
|
|
113
118
|
|
|
114
119
|
- "Is Shield running?" โ `openclaw shield status`
|
|
@@ -123,10 +128,13 @@ Real-time alerts (notifications or inline messages) are high priority: acknowled
|
|
|
123
128
|
|
|
124
129
|
After running `openclaw shield status`, check:
|
|
125
130
|
|
|
126
|
-
-
|
|
127
|
-
- **
|
|
128
|
-
-
|
|
131
|
+
- **โ
Running ยท Connected** โ healthy, nothing to do
|
|
132
|
+
- **โ ๏ธ Degraded ยท Connected** โ capturing but sync issues; try `openclaw shield flush`
|
|
133
|
+
- **โ Disconnected** โ gateway may need a restart
|
|
134
|
+
- **Failures: N poll** โ platform connectivity issue, usually self-recovers; try `openclaw shield flush`
|
|
135
|
+
- **Failures: N telemetry** โ instance reporting failing, monitoring still active
|
|
129
136
|
- **Rising quarantine** โ possible version mismatch, suggest checking for plugin updates
|
|
137
|
+
- **Last data: capture Xm ago** (stale) โ agent may be idle, or capture pipeline issue
|
|
130
138
|
|
|
131
139
|
## RPCs
|
|
132
140
|
|