@upx-us/shield 0.3.6 → 0.3.16

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 CHANGED
@@ -20,29 +20,32 @@ Your Agent → Shield (local: capture + redact) → UPX Platform (analysis, aler
20
20
 
21
21
  ---
22
22
 
23
- ## Quick Start — Suggested bot prompt
23
+ ## Installation
24
24
 
25
- Copy this prompt and paste it to your AI agent. Replace `<YOUR_KEY>` with your Installation Key:
25
+ **If you have an OpenClaw agent running**, use Quick Start paste a single prompt and you're done.
26
+ **If you prefer to configure manually**, follow Manual Installation below.
26
27
 
27
- > Run `openclaw plugins install @upx-us/shield` to install the Shield plugin. Then update `~/.openclaw/openclaw.json`: add `"shield"` to `plugins.allow`, and under `plugins.entries.shield.config` set `"installationKey": "<YOUR_KEY>"`. Then run `openclaw gateway restart`. Confirm Shield is active with `openclaw shield status`.
28
+ ### Quick Start Let your agent do it
28
29
 
29
- That's it. The bot installs the plugin, configures the key, restarts the gateway, and Shield activates automatically.
30
+ Copy this prompt and paste it to your AI agent. Replace `<YOUR_KEY>` with your Installation Key:
30
31
 
31
- ---
32
+ > Install the OpenClaw Shield plugin (@upx-us/shield) with installation key `<YOUR_KEY>`. Add "shield" to plugins.allow and set installationKey in plugins.entries.shield.config. Restart the gateway and confirm Shield is active with openclaw shield status.
32
33
 
33
- ## Manual Installation
34
+ The agent installs the plugin, edits the config, restarts the gateway, and Shield activates automatically.
34
35
 
35
- ### Step 1 — Install the plugin
36
+ ### Manual Installation
37
+
38
+ #### Step 1 — Install the plugin
36
39
 
37
40
  ```bash
38
41
  openclaw plugins install @upx-us/shield
39
42
  ```
40
43
 
41
- > OpenClaw will show a security warning about environment variable access and network calls this is **expected** for a security monitoring plugin. Add `"shield"` to `plugins.allow` in `openclaw.json` to suppress it.
44
+ > **Security prompt:** OpenClaw will warn about environment variable access combined with network calls. This is **expected** Shield reads signing credentials and sends telemetry to the Shield API. To acknowledge, add `"shield"` to `plugins.allow` in `~/.openclaw/openclaw.json` (shown in Step 2). If automating via a script or agent, pre-populate `plugins.allow` before running the install command to avoid interactive prompts.
42
45
 
43
- ### Step 2 — Add your Installation Key
46
+ #### Step 2 — Add your Installation Key
44
47
 
45
- Open `~/.openclaw/openclaw.json` and add the following:
48
+ Open `~/.openclaw/openclaw.json` and add:
46
49
 
47
50
  ```json
48
51
  {
@@ -59,9 +62,9 @@ Open `~/.openclaw/openclaw.json` and add the following:
59
62
  }
60
63
  ```
61
64
 
62
- > **Note:** Each Installation Key is single-use. The plugin exchanges it for credentials on first startup and saves them locally you can remove `installationKey` from the config after activation.
65
+ > **Installation Keys are single-use** the plugin exchanges your key for permanent credentials on first startup and saves them locally. The key is consumed on the first *successful* activation; if the first attempt fails (network issue, config typo), the key is not burned and you can retry. Once activation succeeds, you can safely remove `installationKey` from the config.
63
66
 
64
- ### Step 3 — Restart the Gateway
67
+ #### Step 3 — Restart the Gateway
65
68
 
66
69
  ```bash
67
70
  openclaw gateway restart
@@ -69,9 +72,7 @@ openclaw gateway restart
69
72
 
70
73
  Shield auto-registers, saves credentials to `~/.openclaw/shield/config.env`, and starts monitoring.
71
74
 
72
- ---
73
-
74
- ## Alternative: CLI Activation
75
+ ### Alternative: CLI Activation
75
76
 
76
77
  If the config-based flow didn't work (e.g. the key was added to the wrong path), you can activate directly:
77
78
 
@@ -82,22 +83,45 @@ openclaw gateway restart
82
83
 
83
84
  ---
84
85
 
85
- ## Verify it's running
86
+ ## What to expect after activation
86
87
 
87
- ```bash
88
- openclaw shield status
88
+ After restart, Shield exchanges your key for permanent credentials — this takes a few seconds. You should see your first events within the first poll cycle (~30 seconds). Within 1–2 minutes, those events will appear on the platform at [uss.upx.com](https://uss.upx.com).
89
+
90
+ Run `openclaw shield status` to confirm:
91
+
92
+ **Just activated (first minute):**
93
+
94
+ ```
95
+ OpenClaw Shield — v0.3.x (5s ago)
96
+
97
+ ── Plugin Health ─────────────────────────────
98
+ Connection: ✅ Connected
99
+ Version: 0.3.x
100
+ Instance: a1b2c3d4…
101
+ Last poll: 5s ago
102
+ Last capture: 5s ago
103
+ Events sent: 3 (all-time)
104
+ Quarantine: 0 (all-time)
105
+ Failures: 0 (consecutive)
106
+ Daemon PID: 12345
107
+ Session: 0m
89
108
  ```
90
109
 
91
- **When activated:**
110
+ A low event count and a recent `Last capture` time means Shield is working correctly. Events accumulate as your agent uses tools.
111
+
112
+ > **Note:** The Activity and Redactions sections appear after your first sync cycle (~30s). If your status looks shorter than the "extended use" example below, that's normal — they'll populate automatically.
113
+
114
+ **After extended use:**
92
115
 
93
116
  ```
94
- OpenClaw Shield — v0.3.0 (5s ago)
117
+ OpenClaw Shield — v0.3.x (5s ago)
95
118
 
96
119
  ── Plugin Health ─────────────────────────────
97
120
  Connection: ✅ Connected
98
- Version: 0.3.0
121
+ Version: 0.3.x
99
122
  Instance: a1b2c3d4…
100
123
  Last poll: 5s ago
124
+ Last capture: 14s ago
101
125
  Events sent: 1,842 (all-time)
102
126
  Quarantine: 0 (all-time)
103
127
  Failures: 0 (consecutive)
@@ -107,8 +131,8 @@ OpenClaw Shield — v0.3.0 (5s ago)
107
131
  ── Activity ──────────────────────────────────
108
132
 
109
133
  📡 Last sync (29s ago — 5 events)
110
- TOOL_RESULT ████████ 3
111
- TOOL_CALL █████ 2
134
+ exec ████████ 3
135
+ file █████ 2
112
136
 
113
137
  📊 This session (since restart 4h 12m ago — 142 events)
114
138
  TOOL_CALL ████████ 78
@@ -123,7 +147,7 @@ OpenClaw Shield — v0.3.0 (5s ago)
123
147
  **When not activated:**
124
148
 
125
149
  ```
126
- OpenClaw Shield — v0.3.0
150
+ OpenClaw Shield — v0.3.x
127
151
 
128
152
  Status: Loaded (not activated)
129
153
 
@@ -142,11 +166,18 @@ OpenClaw Shield — v0.3.0
142
166
 
143
167
  ### Event types
144
168
 
145
- Shield captures what your agent *does* — not what it says. Each line in the Activity section represents a category of tool calls:
169
+ Shield captures what your agent *does* — tool invocations and their results not what it says.
170
+
171
+ The Activity section shows two levels:
172
+
173
+ - **Session-level** (`TOOL_CALL`, `TOOL_RESULT`) — every tool invocation and result, shown in the "This session" summary
174
+ - **Sync-level** (`exec`, `file`, `web`, etc.) — the specific event type after parsing, shown in "Last sync"
175
+
176
+ `exec: 2` in Last sync means 2 of those synced events were shell commands. Those same 2 are also counted within the `TOOL_CALL` total in the session view. They are not separate.
146
177
 
147
178
  | Event type | What it means |
148
179
  |---|---|
149
- | `TOOL_CALL` | A tool was invoked by the agent (exec, read, write, web search, etc.) |
180
+ | `TOOL_CALL` | A tool was invoked by the agent |
150
181
  | `TOOL_RESULT` | The result returned from a tool call |
151
182
  | `exec` | Shell command executed |
152
183
  | `file` | File read, written, or edited |
@@ -156,23 +187,27 @@ Shield captures what your agent *does* — not what it says. Each line in the Ac
156
187
  | `browser` | Browser automation action |
157
188
  | `cron` | A scheduled task fired |
158
189
 
159
- Shield captures what your agent *does* — tool invocations and their results. Each event carries metadata about the operation (tool name, paths, URLs) along with a redacted summary of the result.
160
-
161
190
  ### Quarantine
162
191
 
163
- Quarantine counts events that failed local schema validation and were **held back** rather than forwarded to the platform. They are not lost — they're written to `~/.openclaw/shield/data/quarantine.jsonl` for inspection. A non-zero quarantine count usually means a plugin version mismatch between the collector and the platform schema; upgrading Shield typically resolves it.
192
+ Quarantine counts events that failed local schema validation and were **held back** rather than forwarded to the platform. They are not lost — written to `~/.openclaw/shield/data/quarantine.jsonl` for inspection. A non-zero quarantine count usually means a plugin version mismatch; upgrading Shield typically resolves it.
164
193
 
165
194
  ### Redactions
166
195
 
167
- Redaction runs locally before anything leaves your machine. The counts shown are **occurrences** — how many times that category of sensitive data was detected and replaced with a reversible token (e.g. `host:a3f9b1c2`, `user:7b2c4a1f`). The original values are stored in an encrypted local vault (`~/.openclaw/shield/data/redaction-vault.json`) so they can be recovered for investigation — they are never transmitted to the platform.
196
+ Redaction runs locally before anything leaves your machine. The counts shown are **occurrences** — how many times sensitive data was detected and replaced with a reversible token (e.g. `host:a3f9b1c2`, `user:7b2c4a1f`). Original values are stored in an encrypted local vault and never transmitted.
168
197
 
169
198
  ### All-time vs. session
170
199
 
171
- - **Events sent / Quarantine** in the health section = all-time totals, persisted across gateway restarts
172
- - **This session** in the activity section = since the last gateway restart
200
+ - **Events sent / Quarantine** = all-time totals, persisted across gateway restarts
201
+ - **This session** = since the last gateway restart
173
202
  - **Last sync** = the most recent poll cycle only
174
203
 
175
- To see the full picture — detections, alerts, rules, and cases — visit [uss.upx.com](https://uss.upx.com).
204
+ ### Warnings
205
+
206
+ When status shows `⚠️ Capture health degraded`, the plugin is connected but capture activity is not syncing successfully. This is not triggered by idle sessions.
207
+
208
+ Use `Last capture` as the first diagnostic:
209
+ - Recent `Last capture` + warning present → investigate sync pipeline health
210
+ - Old `Last capture` + no warning → normal idle behavior
176
211
 
177
212
  ---
178
213
 
@@ -195,11 +230,47 @@ Shield captures agent activity locally, applies on-device redaction, and forward
195
230
 
196
231
  ---
197
232
 
233
+ ## What is sent to the platform
234
+
235
+ Shield uses two separate channels with different privacy properties:
236
+
237
+ ### Telemetry (operational identity)
238
+
239
+ Sent every ~5 minutes over HTTPS. This tells the platform *where* the instance is running, not *what* it does. It does **not** pass through the redaction pipeline.
240
+
241
+ ```json
242
+ {
243
+ "machine": {
244
+ "hostname": "openclaw-agent",
245
+ "os": "linux",
246
+ "arch": "x64",
247
+ "node_version": "v22.x.x",
248
+ "public_ip": "1.2.3.4"
249
+ },
250
+ "software": {
251
+ "plugin_version": "0.3.x",
252
+ "openclaw_version": "2026.x.x",
253
+ "agent_label": "main",
254
+ "instance_name": "openclaw-agent"
255
+ }
256
+ }
257
+ ```
258
+
259
+ Telemetry fields like `hostname`, `public_ip`, and `instance_name` are operational identity signals used for geo-correlation in detection rules and instance health monitoring. They travel over HTTPS and are never exposed publicly.
260
+
261
+ ### Events (behavioural data)
262
+
263
+ Sent every poll cycle over HTTPS. This is the data stream subject to the full redaction pipeline. Per-event fields include: tool name, redacted command/path/output, timestamp, session ID, and action type. Sensitive values are replaced with `category:hash` tokens before transmission.
264
+
265
+ > **The distinction:** telemetry = *who/where the instance is* (slim, operational). Events = *what the agent did* (redacted, privacy-protected).
266
+
267
+ ---
268
+
198
269
  ## How your data is protected
199
270
 
200
- **Redaction** runs locally before any data leaves your machine. The redactor detects hostnames, usernames, file paths, API keys, and secrets — replacing them with deterministic `category:hash` tokens. The token→original mapping is stored in an AES-256-GCM encrypted vault (`~/.openclaw/shield/data/redaction-vault.json`, mode 0600). Original values are never transmitted.
271
+ **Redaction** runs locally before any data leaves your machine. The redactor detects hostnames, usernames, file paths, API keys, and secrets — replacing them with deterministic `category:hash` tokens. The token→original mapping is stored in an AES-256-GCM encrypted vault (`~/.openclaw/shield/data/redaction-vault.json`, mode 0600).
201
272
 
202
- **Authentication** uses HMAC-SHA256 with a per-instance signing key. Every request is signed — requests with invalid signatures are rejected by the platform.
273
+ **Authentication** uses HMAC-SHA256 with a per-instance key. Every request is signed — requests with invalid signatures are rejected.
203
274
 
204
275
  **Credentials** are stored locally at `~/.openclaw/shield/config.env` (mode 0600) and are never transmitted.
205
276
 
@@ -207,12 +278,14 @@ Shield captures agent activity locally, applies on-device redaction, and forward
207
278
 
208
279
  ## Upgrading
209
280
 
210
- - Standard upgrade command: `openclaw plugins update shield`
211
- - Cursors and credentials are preserved across upgrades
212
- - After upgrade verify with: `openclaw shield status`
213
- - See CHANGELOG.md for version history
281
+ ```bash
282
+ openclaw plugins update shield
283
+ openclaw shield status
284
+ ```
285
+
286
+ Cursors and credentials are preserved across upgrades. See the CHANGELOG (available on the Shield portal at uss.upx.com) for version history.
214
287
 
215
- > **"Integrity drift detected"** during upgrade is expected. OpenClaw stores the checksum of the installed version and warns when it changes which it always does on a legitimate upgrade. Confirm with `y` to proceed. This prompt only indicates a real problem if you see it without having explicitly upgraded (e.g. the plugin files changed unexpectedly).
288
+ > **"Integrity drift detected"** during upgrade is expected OpenClaw warns when plugin files change, which always happens on a legitimate upgrade. This only indicates a real problem if you see it without having explicitly upgraded.
216
289
 
217
290
  ---
218
291
 
@@ -220,43 +293,79 @@ Shield captures agent activity locally, applies on-device redaction, and forward
220
293
 
221
294
  **Shield shows "not activated"**
222
295
  → Get your Installation Key at [uss.upx.com](https://uss.upx.com) → APPS → OpenClaw Shield → Install
223
- → Add to `openclaw.json`:
224
- ```json
225
- "plugins": { "entries": { "shield": { "config": { "installationKey": "YOUR_KEY" } } } }
226
- ```
227
- → Restart OpenClaw
296
+ → Add to `openclaw.json` and restart the gateway (see Manual Installation above)
228
297
 
229
- **Activated but no events reaching platform**
230
- → Run `openclaw shield status` — check Failures count and Last sync time
231
- Verify network access to the Shield API endpoint (check your config for the URL)
232
- Enable debug logging: set `LOG_LEVEL=debug` in your environment
298
+ **Activated but no events after 5 minutes**
299
+ → Run `openclaw shield status` — check `Failures` count and `Last sync` time
300
+ Check `Last capture`: if it's recent, Shield is capturing but may have a sync issue — restart the gateway
301
+ If `Last capture` is old, your agent may not have used any tools yet — try running a command and check again
302
+ → Enable debug logging: `LOG_LEVEL=debug openclaw start`
233
303
 
234
304
  **High CPU or memory usage**
235
- → Increase poll interval in config: `"pollIntervalMs": 60000` (default is 30000ms)
236
- → Shield processes at most 5000 events per poll cycle regardless of backlog size
305
+ → Increase poll interval: `"pollIntervalMs": 60000` in plugin config (default: 30000ms)
237
306
 
238
307
  **Cursor file issues**
239
- Cursor file location: `~/.openclaw/shield/data/cursors.json`
240
- → To reset: delete the file — Shield will reinitialize on next poll (some events may re-process)
241
-
242
- **Enable debug logging**
243
- → `LOG_LEVEL=debug openclaw start` — verbose output including poll cycles and send results
308
+ To reset: `rm ~/.openclaw/shield/data/cursors.json` — Shield reinitializes on next poll
244
309
 
245
310
  **Dry-run mode (no events sent)**
246
311
  → Add `"dryRun": true` to plugin config — events are processed and logged but never transmitted
247
312
 
248
313
  ---
249
314
 
315
+ ## FAQ
316
+
317
+ **How long until I see my first event on the platform?**
318
+ Within 1–2 minutes of activation. Shield polls every ~30 seconds; the platform processes events within seconds of receipt.
319
+
320
+ **How do I verify it's working end-to-end?**
321
+ Run `openclaw shield status` and check that `Events sent` is increasing and `Last capture` is recent. Then visit [uss.upx.com](https://uss.upx.com) and check your instance — you should see events flowing within 2 minutes.
322
+
323
+ **What if I don't see any events after 5 minutes?**
324
+ Check `openclaw shield status` for elevated `Failures` or a stale `Last sync`. If failures are high, restart the gateway. If `Last capture` is old, your agent hasn't used any tools — run a command to generate activity.
325
+
326
+ **Can I run Shield alongside other plugins?**
327
+ Yes. Shield runs as a passive observer — it hooks into the event stream and does not interfere with other plugins or agent behavior.
328
+
329
+ **Is my Installation Key burned if activation fails?**
330
+ No. The key is consumed only on the first *successful* activation. If the attempt fails (network issue, config error), you can fix the issue and retry with the same key.
331
+
332
+ **Where is the changelog?**
333
+ the CHANGELOG, available on the Shield portal at uss.upx.com
334
+
335
+ ---
336
+
337
+ ## Subscription expiry
338
+
339
+ When a subscription lapses or the monthly event quota is exhausted, Shield detects this on the next ingest call and will:
340
+
341
+ 1. Log a clear warning
342
+ 2. Stop transmitting events — **events are dropped, not queued**
343
+ 3. Show elevated consecutive failures in `openclaw shield status`
344
+
345
+ Events generated during an expiry or quota period are permanently lost. Renew your subscription at [uss.upx.com](https://uss.upx.com) and restart the gateway to resume.
346
+
347
+ ---
348
+
250
349
  ## Uninstalling
251
350
 
252
351
  ```bash
253
352
  openclaw plugins uninstall shield
254
353
  ```
255
354
 
256
- Stops the monitoring bridge and removes the plugin.
355
+ This removes the plugin code. To fully remove all local Shield data:
356
+
357
+ ```bash
358
+ rm -rf ~/.openclaw/shield/
359
+ ```
360
+
361
+ This deletes credentials, cursors, IP cache, and the redaction vault.
362
+
363
+ > **Note:** The redaction vault (`data/redaction-vault.json`) contains the mapping from redaction tokens back to original values. Deleting it means you can no longer reverse-lookup redacted values from past events. Retain this file for as long as your data retention policy requires.
364
+
365
+ Platform-side instance data can be managed via [uss.upx.com](https://uss.upx.com).
257
366
 
258
367
  ---
259
368
 
260
369
  ## Need help?
261
370
 
262
- Visit the Shield portal at [uss.upx.com](https://uss.upx.com) or contact your Shield administrator. For UPX support, reach out at [upx.com](https://upx.com).
371
+ Visit [uss.upx.com](https://uss.upx.com) or contact your Shield administrator.
package/dist/index.d.ts CHANGED
@@ -10,6 +10,16 @@ interface StartGuard {
10
10
  reset: () => void;
11
11
  }
12
12
  export declare function createStartGuard(): StartGuard;
13
+ interface StatusWarningInput {
14
+ running: boolean;
15
+ lastPollAt?: number | null;
16
+ lastSyncAt?: number | null;
17
+ lastCaptureAt?: number | null;
18
+ captureSeenSinceLastSync?: boolean;
19
+ consecutiveFailures?: number | null;
20
+ now?: number;
21
+ }
22
+ export declare function getStatusWarnings(input: StatusWarningInput): string[];
13
23
  interface OpenClawPluginAPI {
14
24
  config?: Record<string, unknown>;
15
25
  pluginConfig?: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ exports.resolveInstallationKey = resolveInstallationKey;
38
38
  exports.maskPluginConfigForLogs = maskPluginConfigForLogs;
39
39
  exports.createSingleflightRunner = createSingleflightRunner;
40
40
  exports.createStartGuard = createStartGuard;
41
+ exports.getStatusWarnings = getStatusWarnings;
41
42
  const config_1 = require("./src/config");
42
43
  const log_1 = require("./src/log");
43
44
  const log = __importStar(require("./src/log"));
@@ -94,6 +95,13 @@ async function performAutoRegistration(installationKey) {
94
95
  const configDir = (0, path_1.join)(config_1.SHIELD_CONFIG_PATH, '..');
95
96
  if (!(0, fs_1.existsSync)(configDir))
96
97
  (0, fs_1.mkdirSync)(configDir, { recursive: true });
98
+ if ((0, fs_1.existsSync)(config_1.SHIELD_CONFIG_PATH)) {
99
+ const existing = (0, config_1.loadCredentials)();
100
+ if (hasValidCredentials(existing)) {
101
+ log.info('shield', 'Existing credentials found and valid — skipping config.env overwrite.');
102
+ return existing;
103
+ }
104
+ }
97
105
  const envContent = [
98
106
  `# Shield API Configuration — auto-generated by OpenClaw plugin on ${new Date().toISOString()}`,
99
107
  `SHIELD_API_URL=${SHIELD_API_URL}`,
@@ -262,6 +270,35 @@ function readPersistedState() {
262
270
  return null;
263
271
  }
264
272
  }
273
+ const STALE_POLL_WARN_MS = 2 * 60 * 1000;
274
+ const STALE_SYNC_WARN_MS = 15 * 60 * 1000;
275
+ const CONSECUTIVE_FAILURES_WARN = 5;
276
+ function getStatusWarnings(input) {
277
+ const warnings = [];
278
+ if (!input.running)
279
+ return warnings;
280
+ const now = input.now ?? Date.now();
281
+ const lastPollAt = input.lastPollAt ?? 0;
282
+ const lastSyncAt = input.lastSyncAt ?? 0;
283
+ const lastCaptureAt = input.lastCaptureAt ?? 0;
284
+ const captureSeenSinceLastSync = input.captureSeenSinceLastSync ?? false;
285
+ const consecutiveFailures = input.consecutiveFailures ?? 0;
286
+ if (!lastPollAt) {
287
+ warnings.push('Hooks may not be initialized: service is running but no poll has executed yet.');
288
+ }
289
+ else if (now - lastPollAt > STALE_POLL_WARN_MS) {
290
+ warnings.push('Polling looks stale while connected; event capture may be paused after reload.');
291
+ }
292
+ const staleSinceSync = Boolean(lastSyncAt) && (now - lastSyncAt > STALE_SYNC_WARN_MS);
293
+ const staleSinceCaptureWithoutSync = !lastSyncAt && Boolean(lastCaptureAt) && (now - lastCaptureAt > STALE_SYNC_WARN_MS);
294
+ if (captureSeenSinceLastSync && (staleSinceSync || staleSinceCaptureWithoutSync)) {
295
+ warnings.push('Capture activity observed, but no successful event sync in a prolonged interval.');
296
+ }
297
+ if (consecutiveFailures >= CONSECUTIVE_FAILURES_WARN) {
298
+ warnings.push(`Consecutive poll failures are elevated (${consecutiveFailures}).`);
299
+ }
300
+ return warnings;
301
+ }
265
302
  const state = {
266
303
  activated: false,
267
304
  running: false,
@@ -271,8 +308,11 @@ const state = {
271
308
  quarantineCount: 0,
272
309
  consecutiveFailures: 0,
273
310
  instanceId: '',
311
+ lastCaptureAt: 0,
312
+ captureSeenSinceLastSync: false,
274
313
  lastSync: null,
275
314
  };
315
+ let teardownPreviousRuntime = null;
276
316
  const MAX_BACKOFF_MS = 5 * 60 * 1000;
277
317
  const TELEMETRY_INTERVAL_MS = 5 * 60 * 1000;
278
318
  function getBackoffInterval(baseMs) {
@@ -319,12 +359,35 @@ function printActivatedStatus() {
319
359
  if (shortId)
320
360
  console.log(` Instance: ${shortId}`);
321
361
  console.log(` Last poll: ${lastPollLabel}`);
362
+ const lastCaptureMs = s.lastCaptureAt ? Date.now() - s.lastCaptureAt : null;
363
+ const lastCaptureLabel = s.lastCaptureAt
364
+ ? (lastCaptureMs < 60_000 ? `${Math.round(lastCaptureMs / 1000)}s ago`
365
+ : lastCaptureMs < 3_600_000 ? `${Math.floor(lastCaptureMs / 60_000)}m ago`
366
+ : `${(lastCaptureMs / 3_600_000).toFixed(1)}h ago`)
367
+ : null;
368
+ if (lastCaptureLabel)
369
+ console.log(` Last capture: ${lastCaptureLabel}`);
322
370
  const allTime = (s.allTime ?? readAllTimeStats());
323
371
  console.log(` Events sent: ${allTime.eventsProcessed.toLocaleString()} (all-time)`);
324
372
  console.log(` Quarantine: ${allTime.quarantineCount.toLocaleString()} (all-time)`);
325
373
  console.log(` Failures: ${s.consecutiveFailures ?? 0} (consecutive)`);
326
374
  if (s.pid)
327
375
  console.log(` Daemon PID: ${s.pid}`);
376
+ const statusWarnings = getStatusWarnings({
377
+ running: isRunning,
378
+ lastPollAt: s.lastPollAt ?? null,
379
+ lastSyncAt: s.lastSync?.at ?? null,
380
+ lastCaptureAt: s.lastCaptureAt ?? null,
381
+ captureSeenSinceLastSync: Boolean(s.captureSeenSinceLastSync ?? false),
382
+ consecutiveFailures: s.consecutiveFailures ?? 0,
383
+ });
384
+ if (statusWarnings.length > 0) {
385
+ console.log(' Warning: ⚠️ Capture health degraded');
386
+ for (const warning of statusWarnings) {
387
+ console.log(` - ${warning}`);
388
+ }
389
+ console.log(' - If this persists, run: openclaw gateway restart');
390
+ }
328
391
  const startedAt = s.startedAt;
329
392
  if (startedAt) {
330
393
  const uptimeMs = Date.now() - startedAt;
@@ -424,6 +487,51 @@ exports.default = {
424
487
  let telemetryHandle = null;
425
488
  const startGuard = createStartGuard();
426
489
  let onSignalHandler = null;
490
+ let runtimeGeneration = 0;
491
+ const clearRuntimeHandles = () => {
492
+ if (pollHandle) {
493
+ clearTimeout(pollHandle);
494
+ pollHandle = null;
495
+ }
496
+ if (telemetryHandle) {
497
+ clearInterval(telemetryHandle);
498
+ telemetryHandle = null;
499
+ }
500
+ };
501
+ const detachSignalHandlers = () => {
502
+ if (!onSignalHandler)
503
+ return;
504
+ process.off('SIGTERM', onSignalHandler);
505
+ process.off('SIGINT', onSignalHandler);
506
+ onSignalHandler = null;
507
+ };
508
+ const cleanupRuntime = async (opts) => {
509
+ runtimeGeneration++;
510
+ pollFn = null;
511
+ clearRuntimeHandles();
512
+ detachSignalHandlers();
513
+ const markStopped = opts?.markStopped ?? true;
514
+ if (markStopped) {
515
+ state.running = false;
516
+ markStateDirty();
517
+ persistState();
518
+ }
519
+ if (opts?.resetGuard) {
520
+ startGuard.reset();
521
+ }
522
+ if (opts?.flushRedactor) {
523
+ try {
524
+ const { flush: flushRedactor } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
525
+ flushRedactor();
526
+ }
527
+ catch { }
528
+ }
529
+ };
530
+ if (teardownPreviousRuntime) {
531
+ void teardownPreviousRuntime()
532
+ .catch((err) => log.warn('shield', `Runtime cleanup before re-register failed: ${err instanceof Error ? err.message : String(err)}`));
533
+ }
534
+ teardownPreviousRuntime = () => cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: false });
427
535
  api.registerService({
428
536
  id: 'shield-monitor',
429
537
  async start() {
@@ -432,6 +540,8 @@ exports.default = {
432
540
  return;
433
541
  }
434
542
  try {
543
+ await cleanupRuntime({ markStopped: false, resetGuard: false, flushRedactor: false });
544
+ const activeGeneration = ++runtimeGeneration;
435
545
  let credentials = (0, config_1.loadCredentials)();
436
546
  let validCreds = hasValidCredentials(credentials);
437
547
  if (!validCreds && installationKey) {
@@ -480,7 +590,7 @@ exports.default = {
480
590
  state.running = true;
481
591
  persistState();
482
592
  const runTelemetry = async () => {
483
- if (!state.running)
593
+ if (!state.running || activeGeneration !== runtimeGeneration)
484
594
  return;
485
595
  const hostSnapshot = config.collectHostMetrics ? generateHostTelemetry() : null;
486
596
  const hostMeta = hostSnapshot?.event?.tool_metadata;
@@ -503,18 +613,24 @@ exports.default = {
503
613
  },
504
614
  };
505
615
  const result = await reportInstance(instancePayload, config.credentials);
616
+ if (activeGeneration !== runtimeGeneration)
617
+ return;
506
618
  log.info('shield', `Instance report → Platform: success=${result.ok}`);
507
619
  };
508
620
  const runTelemetrySingleflight = createSingleflightRunner(runTelemetry);
509
621
  runTelemetrySingleflight().catch((err) => log.error('shield', `Telemetry error: ${err instanceof Error ? err.message : String(err)}`));
510
622
  telemetryHandle = setInterval(() => {
623
+ if (activeGeneration !== runtimeGeneration || !state.running)
624
+ return;
511
625
  runTelemetrySingleflight().catch((err) => log.error('shield', `Telemetry error: ${err instanceof Error ? err.message : String(err)}`));
512
626
  }, TELEMETRY_INTERVAL_MS);
513
627
  const poll = async () => {
514
- if (!state.running)
628
+ if (!state.running || activeGeneration !== runtimeGeneration)
515
629
  return;
516
630
  try {
517
631
  const entries = await fetchNewEntries(config);
632
+ if (activeGeneration !== runtimeGeneration)
633
+ return;
518
634
  if (entries.length === 0) {
519
635
  commitCursors(config, []);
520
636
  state.consecutiveFailures = 0;
@@ -523,6 +639,9 @@ exports.default = {
523
639
  persistState();
524
640
  return;
525
641
  }
642
+ state.lastCaptureAt = Date.now();
643
+ state.captureSeenSinceLastSync = true;
644
+ markStateDirty();
526
645
  let envelopes = transformEntries(entries);
527
646
  const { valid: validEvents, quarantined } = validate(envelopes.map(e => e.event));
528
647
  if (quarantined > 0) {
@@ -536,6 +655,8 @@ exports.default = {
536
655
  envelopes = envelopes.map(e => redactEvent(e));
537
656
  }
538
657
  const results = await sendEvents(envelopes, config);
658
+ if (activeGeneration !== runtimeGeneration)
659
+ return;
539
660
  const needsReg = results.some(r => r.needsRegistration);
540
661
  if (needsReg) {
541
662
  log.error('shield', 'Instance not registered on platform — Shield deactivated.');
@@ -551,6 +672,8 @@ exports.default = {
551
672
  markStateDirty();
552
673
  persistState();
553
674
  await new Promise(r => setTimeout(r, waitMs));
675
+ if (activeGeneration !== runtimeGeneration)
676
+ return;
554
677
  return;
555
678
  }
556
679
  const accepted = results.reduce((sum, r) => sum + (r.success ? r.eventCount : 0), 0);
@@ -567,6 +690,7 @@ exports.default = {
567
690
  }
568
691
  const lastSync = { at: Date.now(), eventCount: accepted, eventTypes: syncEventTypes };
569
692
  state.lastSync = lastSync;
693
+ state.captureSeenSinceLastSync = false;
570
694
  writeAllTimeStats({ eventsProcessed: accepted, lastSync });
571
695
  }
572
696
  else {
@@ -578,6 +702,8 @@ exports.default = {
578
702
  persistState();
579
703
  }
580
704
  catch (err) {
705
+ if (activeGeneration !== runtimeGeneration)
706
+ return;
581
707
  state.consecutiveFailures++;
582
708
  markStateDirty();
583
709
  log.error('shield', `Poll error: ${err instanceof Error ? err.message : String(err)}`);
@@ -587,14 +713,18 @@ exports.default = {
587
713
  pollFn = runPollSingleflight;
588
714
  await runPollSingleflight();
589
715
  const schedulePoll = () => {
590
- if (!state.running)
716
+ if (!state.running || activeGeneration !== runtimeGeneration)
591
717
  return;
592
718
  const interval = getBackoffInterval(config.pollIntervalMs);
593
719
  if (interval !== config.pollIntervalMs) {
594
720
  log.warn('shield', `Backing off: next poll in ${Math.round(interval / 1000)}s (${state.consecutiveFailures} consecutive failures)`);
595
721
  }
596
722
  pollHandle = setTimeout(() => {
723
+ if (activeGeneration !== runtimeGeneration || !state.running)
724
+ return;
597
725
  runPollSingleflight().catch((err) => {
726
+ if (activeGeneration !== runtimeGeneration)
727
+ return;
598
728
  state.consecutiveFailures++;
599
729
  log.error('shield', `Poll error (unhandled): ${err instanceof Error ? err.message : String(err)}`);
600
730
  }).finally(() => {
@@ -606,23 +736,7 @@ exports.default = {
606
736
  onSignalHandler = async () => {
607
737
  if (!state.running)
608
738
  return;
609
- state.running = false;
610
- startGuard.reset();
611
- markStateDirty();
612
- persistState();
613
- if (pollHandle) {
614
- clearTimeout(pollHandle);
615
- pollHandle = null;
616
- }
617
- if (telemetryHandle) {
618
- clearInterval(telemetryHandle);
619
- telemetryHandle = null;
620
- }
621
- try {
622
- const { flush: fr } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
623
- fr();
624
- }
625
- catch { }
739
+ await cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: true });
626
740
  log.info('shield', 'Service stopped (signal)');
627
741
  };
628
742
  process.once('SIGTERM', onSignalHandler);
@@ -635,31 +749,10 @@ exports.default = {
635
749
  }
636
750
  },
637
751
  async stop() {
638
- if (!state.running)
639
- return;
640
- state.running = false;
641
- startGuard.reset();
642
- markStateDirty();
643
- persistState();
644
- if (pollHandle) {
645
- clearTimeout(pollHandle);
646
- pollHandle = null;
647
- }
648
- if (telemetryHandle) {
649
- clearInterval(telemetryHandle);
650
- telemetryHandle = null;
651
- }
652
- if (onSignalHandler) {
653
- process.off('SIGTERM', onSignalHandler);
654
- process.off('SIGINT', onSignalHandler);
655
- onSignalHandler = null;
656
- }
657
- try {
658
- const { flush: flushRedactor } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
659
- flushRedactor();
660
- }
661
- catch { }
662
- log.info('shield', 'Service stopped');
752
+ const wasRunning = state.running;
753
+ await cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: true });
754
+ if (wasRunning)
755
+ log.info('shield', 'Service stopped');
663
756
  },
664
757
  });
665
758
  api.registerGatewayMethod('shield.status', ({ respond }) => {
@@ -669,6 +762,8 @@ exports.default = {
669
762
  activated,
670
763
  running: state.running,
671
764
  lastPollAt: state.lastPollAt,
765
+ lastCaptureAt: state.lastCaptureAt,
766
+ captureSeenSinceLastSync: state.captureSeenSinceLastSync,
672
767
  eventsProcessed: state.eventsProcessed,
673
768
  quarantineCount: state.quarantineCount,
674
769
  consecutiveFailures: state.consecutiveFailures,
@@ -163,6 +163,16 @@ async function sendEvents(events, config) {
163
163
  log.warn('sender', `Batch ${batchNum} attempt ${attempt + 1} — HTTP ${res.status}, retrying...`);
164
164
  continue;
165
165
  }
166
+ let parsedData = null;
167
+ try {
168
+ parsedData = JSON.parse(data);
169
+ }
170
+ catch { }
171
+ if (res.status === 402 || (res.status === 429 && parsedData?.error === 'quota_exceeded')) {
172
+ log.warn('sender', `⚠ Event quota exhausted or subscription inactive — events are being dropped. Renew your subscription to resume delivery.`);
173
+ results.push({ success: false, statusCode: res.status, body: data, eventCount: batch.length });
174
+ break;
175
+ }
166
176
  const safeBody = sanitizeResponseBodyForLog(data);
167
177
  log.error('sender', `Batch ${batchNum} — HTTP ${res.status}: ${safeBody}`);
168
178
  consecutiveBatchFailures++;
@@ -1,60 +1,60 @@
1
1
  {
2
- "id": "shield",
3
- "name": "OpenClaw Shield",
4
- "description": "Real-time security monitoring \u2014 streams enriched, redacted security events to the Shield detection platform.",
5
- "version": "0.3.6",
6
- "skills": [
7
- "./skills"
8
- ],
9
- "configSchema": {
10
- "type": "object",
11
- "additionalProperties": false,
12
- "properties": {
13
- "installationKey": {
14
- "type": "string",
15
- "description": "One-time installation key from the Shield portal. The plugin uses this to register the instance and obtain credentials automatically. Can be removed from config after first activation."
16
- },
17
- "enabled": {
18
- "type": "boolean",
19
- "default": true
20
- },
21
- "dryRun": {
22
- "type": "boolean",
23
- "default": false
24
- },
25
- "redactionEnabled": {
26
- "type": "boolean",
27
- "default": true
28
- },
29
- "pollIntervalMs": {
30
- "type": "number",
31
- "default": 30000
32
- },
33
- "collectHostMetrics": {
34
- "type": "boolean",
35
- "default": false
36
- }
37
- }
38
- },
39
- "uiHints": {
40
- "installationKey": {
41
- "label": "Installation Key",
42
- "description": "One-time key from the Shield portal (https://uss.upx.com). Required for first-time activation only."
43
- },
44
- "enabled": {
45
- "label": "Enable security monitoring"
46
- },
47
- "dryRun": {
48
- "label": "Dry run (log events locally, do not transmit)"
49
- },
50
- "redactionEnabled": {
51
- "label": "Redact sensitive values before transmitting"
52
- },
53
- "pollIntervalMs": {
54
- "label": "Polling interval (milliseconds)"
2
+ "id": "shield",
3
+ "name": "OpenClaw Shield",
4
+ "description": "Real-time security monitoring streams enriched, redacted security events to the Shield detection platform.",
5
+ "version": "0.3.16",
6
+ "skills": [
7
+ "./skills"
8
+ ],
9
+ "configSchema": {
10
+ "type": "object",
11
+ "additionalProperties": false,
12
+ "properties": {
13
+ "installationKey": {
14
+ "type": "string",
15
+ "description": "One-time installation key from the Shield portal. The plugin uses this to register the instance and obtain credentials automatically. Can be removed from config after first activation."
16
+ },
17
+ "enabled": {
18
+ "type": "boolean",
19
+ "default": true
20
+ },
21
+ "dryRun": {
22
+ "type": "boolean",
23
+ "default": false
24
+ },
25
+ "redactionEnabled": {
26
+ "type": "boolean",
27
+ "default": true
28
+ },
29
+ "pollIntervalMs": {
30
+ "type": "number",
31
+ "default": 30000
32
+ },
33
+ "collectHostMetrics": {
34
+ "type": "boolean",
35
+ "default": false
36
+ }
37
+ }
55
38
  },
56
- "collectHostMetrics": {
57
- "label": "Collect host telemetry metrics"
39
+ "uiHints": {
40
+ "installationKey": {
41
+ "label": "Installation Key",
42
+ "description": "One-time key from the Shield portal (https://uss.upx.com). Required for first-time activation only."
43
+ },
44
+ "enabled": {
45
+ "label": "Enable security monitoring"
46
+ },
47
+ "dryRun": {
48
+ "label": "Dry run (log events locally, do not transmit)"
49
+ },
50
+ "redactionEnabled": {
51
+ "label": "Redact sensitive values before transmitting"
52
+ },
53
+ "pollIntervalMs": {
54
+ "label": "Polling interval (milliseconds)"
55
+ },
56
+ "collectHostMetrics": {
57
+ "label": "Collect host telemetry metrics"
58
+ }
58
59
  }
59
- }
60
- }
60
+ }
package/package.json CHANGED
@@ -1,69 +1,72 @@
1
1
  {
2
- "name": "@upx-us/shield",
3
- "version": "0.3.6",
4
- "description": "Security monitoring plugin for OpenClaw agents — streams enriched security events to the Shield detection platform",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "bin": {
8
- "shield-bridge": "dist/src/index.js",
9
- "shield-setup": "dist/src/setup.js"
10
- },
11
- "files": [
12
- "dist/index.js",
13
- "dist/index.d.ts",
14
- "dist/src/**/*.js",
15
- "dist/src/**/*.d.ts",
16
- "openclaw.plugin.json",
17
- "skills/",
18
- "README.md",
19
- "LICENSE"
20
- ],
21
- "scripts": {
22
- "prebuild": "npm run clean",
23
- "build": "tsc",
24
- "start": "node dist/src/index.js",
25
- "setup": "node dist/src/setup.js",
26
- "lint": "tsc --noEmit",
27
- "test": "node --require tsx/cjs --test --test-reporter spec tests/**/*.test.ts tests/*.test.ts",
28
- "test:watch": "node --require tsx/cjs --test --watch tests/**/*.test.ts tests/*.test.ts",
29
- "test:parser": "node tests/run-parser.js",
30
- "test:parser:short": "node tests/run-parser.js --short",
31
- "test:parser:verbose": "node tests/run-parser.js --verbose",
32
- "test:parser:help": "node tests/run-parser.js help",
33
- "dev": "tsx scripts/dev-harness.ts",
34
- "dev:dry": "tsx scripts/dev-harness.ts --dry-run",
35
- "generate:schemas": "tsx scripts/generate-schemas.ts",
36
- "prepublishOnly": "npm run build && npm run generate:schemas && npm run prepublish:check",
37
- "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
38
- "prepublish:check": "node scripts/prepublish-check.js"
39
- },
40
- "keywords": [
41
- "openclaw",
42
- "openclaw-plugin",
43
- "security",
44
- "monitoring",
45
- "detection",
46
- "siem",
47
- "compliance"
48
- ],
49
- "author": "UPX Security Services",
50
- "license": "SEE LICENSE IN LICENSE",
51
- "publishConfig": {
52
- "tag": "latest",
53
- "access": "public"
54
- },
55
- "engines": {
56
- "node": ">=20.0.0"
57
- },
58
- "openclaw": {
59
- "extensions": [
60
- "./dist/index.js"
61
- ]
62
- },
63
- "devDependencies": {
64
- "@types/node": "^25.2.3",
65
- "ts-json-schema-generator": "^2.5.0",
66
- "tsx": "^4.21.0",
67
- "typescript": "^5.9.3"
68
- }
2
+ "name": "@upx-us/shield",
3
+ "version": "0.3.16",
4
+ "description": "Security monitoring plugin for OpenClaw agents — streams enriched security events to the Shield detection platform",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "shield-bridge": "dist/src/index.js",
9
+ "shield-setup": "dist/src/setup.js"
10
+ },
11
+ "files": [
12
+ "dist/index.js",
13
+ "dist/index.d.ts",
14
+ "dist/src/**/*.js",
15
+ "dist/src/**/*.d.ts",
16
+ "openclaw.plugin.json",
17
+ "skills/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "prebuild": "npm run clean",
23
+ "build": "tsc",
24
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
25
+ "lint": "tsc --noEmit",
26
+ "test": "node --require tsx/cjs --test --test-reporter spec tests/**/*.test.ts tests/*.test.ts",
27
+ "test:watch": "node --require tsx/cjs --test --watch tests/**/*.test.ts tests/*.test.ts",
28
+ "test:parser": "node tests/run-parser.js",
29
+ "test:parser:short": "node tests/run-parser.js --short",
30
+ "test:parser:verbose": "node tests/run-parser.js --verbose",
31
+ "test:parser:help": "node tests/run-parser.js help",
32
+ "dev": "tsx scripts/dev-harness.ts",
33
+ "dev:dry": "tsx scripts/dev-harness.ts --dry-run",
34
+ "generate:schemas": "tsx scripts/generate-schemas.ts",
35
+ "package:check": "node scripts/prepublish-check.js",
36
+ "package:build": "npm run build",
37
+ "package:validate": "npm run build && npm run test && npm run package:check",
38
+ "package:pack": "npm pack",
39
+ "package:publish": "npm run package:validate && npm publish --access public",
40
+ "start": "node dist/src/index.js",
41
+ "setup": "node dist/src/setup.js"
42
+ },
43
+ "keywords": [
44
+ "openclaw",
45
+ "openclaw-plugin",
46
+ "security",
47
+ "monitoring",
48
+ "detection",
49
+ "siem",
50
+ "compliance"
51
+ ],
52
+ "author": "UPX Security Services",
53
+ "license": "SEE LICENSE IN LICENSE",
54
+ "publishConfig": {
55
+ "tag": "latest",
56
+ "access": "public"
57
+ },
58
+ "engines": {
59
+ "node": ">=20.0.0"
60
+ },
61
+ "openclaw": {
62
+ "extensions": [
63
+ "./dist/index.js"
64
+ ]
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^25.2.3",
68
+ "ts-json-schema-generator": "^2.5.0",
69
+ "tsx": "^4.21.0",
70
+ "typescript": "^5.9.3"
71
+ }
69
72
  }