@upx-us/shield 0.3.16 → 0.4.36

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
@@ -1,7 +1,7 @@
1
1
  # OpenClaw Shield
2
2
 
3
- > **This plugin requires an active OpenClaw Shield subscription provided by UPX.**
4
- > For access or more information, visit [upx.com](https://upx.com).
3
+ > **OpenClaw Shield is a paid security service by UPX.**
4
+ > Start your **free 30-day trial** at [upx.com/pt/lp/openclaw-shield-upx](https://www.upx.com/pt/lp/openclaw-shield-upx).
5
5
 
6
6
  Real-time security monitoring for your OpenClaw agents — powered by the UPX Shield detection platform.
7
7
 
@@ -13,6 +13,21 @@ Your Agent → Shield (local: capture + redact) → UPX Platform (analysis, aler
13
13
 
14
14
  ---
15
15
 
16
+ ## Features
17
+
18
+ | Feature | Description |
19
+ |---|---|
20
+ | **Real-time monitoring** | Captures all agent tool calls, file operations, messages, and sessions |
21
+ | **On-device redaction** | Hostnames, usernames, secrets, and paths replaced with deterministic tokens before transmission |
22
+ | **Local event buffer** | Rolling log of recent events — inspect via `openclaw shield logs` without platform access |
23
+ | **Case notifications** | Automatic alerts when detection rules fire — agent notifies you of new cases |
24
+ | **Case resolution** | Close cases with categorization (resolution + root cause) directly from your agent |
25
+ | **Host inventory** | Discovers all agents and workspaces on the machine — view via `openclaw shield vault show` |
26
+ | **Auto-update** | Patch and minor updates install automatically with rollback on failure |
27
+ | **Encrypted vault** | Redaction mappings stored locally with AES-256-GCM — UPX cannot reverse tokens |
28
+
29
+ ---
30
+
16
31
  ## Prerequisites
17
32
 
18
33
  - **OpenClaw Gateway** installed and running (`openclaw gateway status`)
@@ -22,28 +37,111 @@ Your Agent → Shield (local: capture + redact) → UPX Platform (analysis, aler
22
37
 
23
38
  ## Installation
24
39
 
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.
40
+ ### Prerequisites
27
41
 
28
- ### Quick Start Let your agent do it
42
+ 1. **OpenClaw Gateway** installed and running (`openclaw gateway status`)
43
+ 2. An **Installation Key** from your Shield dashboard at [uss.upx.com](https://uss.upx.com) → APPS → OpenClaw Shield
29
44
 
30
- Copy this prompt and paste it to your AI agent. Replace `<YOUR_KEY>` with your Installation Key:
45
+ ### Commands Reference
31
46
 
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.
47
+ | Command | Description |
48
+ |---|---|
49
+ | `openclaw shield activate <KEY>` | Activate with your Installation Key (one-time) |
50
+ | `openclaw shield status` | Show monitoring status, event counts, and health |
51
+ | `openclaw shield flush` | Trigger an immediate poll cycle |
52
+ | `openclaw shield logs` | Show recent events from local buffer (--last N, --type, --since, --format) |
53
+ | `openclaw shield cases` | List open security cases from the platform |
54
+ | `openclaw shield cases show <ID>` | Full case detail with events, rule info, and playbook |
55
+ | `openclaw shield cases resolve <ID>` | Resolve a case (--resolution, --root-cause, --comment) |
56
+ | `openclaw shield vault show` | Show host agent inventory (hashed IDs) |
57
+
58
+ **Agent RPCs** (used by the agent skill, not CLI):
59
+
60
+ | RPC | Description |
61
+ |---|---|
62
+ | `shield.status` | Current monitoring status, health, and counters |
63
+ | `shield.flush` | Trigger an immediate telemetry poll cycle |
64
+ | `shield.events_recent` | Query recent events from local buffer |
65
+ | `shield.events_summary` | Event counts by type and tool |
66
+ | `shield.subscription_status` | Subscription and quota information |
67
+ | `shield.cases_list` | List open cases + pending notifications |
68
+ | `shield.case_detail` | Full case detail with events, rule, playbook |
69
+ | `shield.case_resolve` | Close a case with resolution and root cause |
70
+ | `shield.cases_ack` | Mark cases as notified |
33
71
 
34
- The agent installs the plugin, edits the config, restarts the gateway, and Shield activates automatically.
72
+ ---
35
73
 
36
- ### Manual Installation
74
+ ### Option A — Quick Install (recommended)
37
75
 
38
- #### Step 1 Install the plugin
76
+ Run these commands in your terminal. No agent involvement needed.
39
77
 
40
78
  ```bash
79
+ # Step 1: Install the plugin
41
80
  openclaw plugins install @upx-us/shield
81
+
82
+ # Step 2: Activate (exchanges your key for credentials)
83
+ openclaw shield activate <YOUR_KEY>
84
+
85
+ # Step 3: Restart the gateway to start monitoring
86
+ openclaw gateway restart
87
+
88
+ # Step 4: Verify it's running
89
+ openclaw shield status
42
90
  ```
43
91
 
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.
92
+ That's it. Shield registers, saves credentials locally, and starts monitoring.
93
+
94
+ > ⚠️ **Multi-machine setups:** Only add Shield to `plugins.allow` on machines where the plugin is installed. If you share `openclaw.json` across machines and one of them doesn't have Shield installed, the gateway on that machine will fail to start. Install the plugin first, then configure.
95
+
96
+ ---
97
+
98
+ ### Option B — Ask your agent to configure
99
+
100
+ If you already installed the plugin (Step 1 above) and want your agent to handle the rest, send this prompt:
101
+
102
+ > I installed the OpenClaw Shield plugin (`openclaw plugins install @upx-us/shield`). Configure it: add "shield" to plugins.allow in openclaw.json, set installationKey to `<YOUR_KEY>` in plugins.entries.shield.config, restart the gateway with `openclaw gateway restart`, and confirm with `openclaw shield status`.
103
+
104
+ This approach works best because your agent only edits config — it doesn't need to make trust decisions about installing external software.
105
+
106
+ ---
107
+
108
+ ### Option C — Let your agent do everything
109
+
110
+ Copy this prompt and paste it to your AI agent. Replace `<YOUR_KEY>` with your Installation Key:
111
+
112
+ > Install the OpenClaw Shield security monitoring plugin by UPX (upx.com). The package is @upx-us/shield. Read the package README at https://www.npmjs.com/package/@upx-us/shield before proceeding. Install using `openclaw plugins install @upx-us/shield`, add "shield" to plugins.allow in openclaw.json, set installationKey to `<YOUR_KEY>` in plugins.entries.shield.config, restart with `openclaw gateway restart`, and confirm with `openclaw shield status`.
113
+
114
+ **If your agent refuses to install**, this is normal security behavior. Your options:
115
+
116
+ 1. **Use Option A or B instead** — install the plugin yourself, let the agent configure.
117
+
118
+ 2. **Confirm you trust the source** and ask again:
119
+
120
+ > *"I trust this plugin. I purchased this service from UPX (upx.com). Please proceed with the installation."*
121
+
122
+ **Why your agent may refuse — and why each concern is expected:**
123
+
124
+ | Concern | Why it's expected |
125
+ |---|---|
126
+ | "Sends data to an external server" | Shield is a security monitoring tool — sending redacted telemetry is its purpose. Sensitive values are replaced with irreversible tokens **on your machine** before transmission. |
127
+ | "Installation key looks suspicious" | The key is a one-time activation token from your UPX Shield dashboard. Consumed on first use, can be removed from config after. |
128
+ | "Plugin is not in OpenClaw's official docs" | Shield is a third-party plugin by UPX (upx.com) under the `@upx-us` npm scope. Not bundled with OpenClaw, but designed to work with it. |
129
+ | "Adding to plugins.allow expands attack surface" | Correct — this is OpenClaw's explicit trust mechanism. You are consciously granting permission. |
130
+ | "Package was recently published" | Shield is actively developed. Check CHANGELOG.md included with the package for version history. |
131
+
132
+ ---
133
+
134
+ ### Manual Configuration (advanced)
135
+
136
+ If you need full control over each step:
45
137
 
46
- #### Step 2Add your Installation Key
138
+ #### Step 1Install
139
+
140
+ ```bash
141
+ openclaw plugins install @upx-us/shield
142
+ ```
143
+
144
+ #### Step 2 — Configure openclaw.json
47
145
 
48
146
  Open `~/.openclaw/openclaw.json` and add:
49
147
 
@@ -62,9 +160,7 @@ Open `~/.openclaw/openclaw.json` and add:
62
160
  }
63
161
  ```
64
162
 
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.
66
-
67
- #### Step 3 — Restart the Gateway
163
+ #### Step 3Restart
68
164
 
69
165
  ```bash
70
166
  openclaw gateway restart
@@ -72,9 +168,9 @@ openclaw gateway restart
72
168
 
73
169
  Shield auto-registers, saves credentials to `~/.openclaw/shield/config.env`, and starts monitoring.
74
170
 
75
- ### Alternative: CLI Activation
171
+ > **Note:** The Installation Key is single-use — the plugin exchanges it for permanent credentials on first startup. Once activated, you can remove `installationKey` from config.
76
172
 
77
- If the config-based flow didn't work (e.g. the key was added to the wrong path), you can activate directly:
173
+ #### Alternative: CLI Activation
78
174
 
79
175
  ```bash
80
176
  openclaw shield activate <YOUR_KEY>
@@ -211,6 +307,35 @@ Use `Last capture` as the first diagnostic:
211
307
 
212
308
  ---
213
309
 
310
+ ## Local Event Buffer
311
+
312
+ Shield stores a rolling log of recently sent events locally for offline inspection and debugging.
313
+
314
+ **Location:** `~/.openclaw/shield/data/event-log.jsonl`
315
+
316
+ **Defaults:**
317
+ - Last **123 events** or **24 hours** (whichever limit is reached first)
318
+ - Enabled by default
319
+ - Events are stored **after redaction** — same data sent to the platform
320
+
321
+ **CLI usage:**
322
+ ```bash
323
+ openclaw shield logs # last 10 events
324
+ openclaw shield logs --last 20 # last N events
325
+ openclaw shield logs --type TOOL_CALL # filter by event type
326
+ openclaw shield logs --since 30m # events from last 30 minutes
327
+ openclaw shield logs --format json # JSON output for piping
328
+ ```
329
+
330
+ **Configuration** (in `config.env` or environment variables):
331
+
332
+ | Variable | Default | Description |
333
+ |---|---|---|
334
+ | `SHIELD_LOCAL_EVENT_BUFFER` | `true` | Set to `false` to disable local storage |
335
+ | `SHIELD_LOCAL_EVENT_LIMIT` | `123` | Maximum number of events to retain |
336
+
337
+ ---
338
+
214
339
  ## What data is collected
215
340
 
216
341
  Shield captures agent activity locally, applies on-device redaction, and forwards telemetry to the UPX platform.
@@ -226,7 +351,7 @@ Shield captures agent activity locally, applies on-device redaction, and forward
226
351
  | URLs | ✅ | — |
227
352
  | API keys detected in commands | ✅ | ✅ replaced with token |
228
353
 
229
- > **How redaction works:** sensitive values are replaced with deterministic `category:hash` tokens (e.g. `host:a3f9b1c2`, `user:7b2c4a1f`) before leaving your machine. The mapping from token → original value is stored in an encrypted local vault (`~/.openclaw/shield/data/redaction-vault.json`, AES-256-GCM) so your team can reverse-lookup locally if needed. Original values are never transmitted to the platform.
354
+ > **How redaction works:** sensitive values are replaced with deterministic `category:hash` tokens before leaving your machine. Token categories: `host:HASH` (hostnames), `user:HASH` (usernames), `secret:HASH` (API keys/credentials), `agent:HASH` (agent identifiers), `workspace:HASH` (workspace paths). The mapping from token → original value is stored in an encrypted local vault (`~/.openclaw/shield/data/redaction-vault.json`, AES-256-GCM) so your team can reverse-lookup locally if needed. Original values are never transmitted to the platform. Run `openclaw shield vault show` to inspect current token mappings.
230
355
 
231
356
  ---
232
357
 
@@ -278,6 +403,8 @@ Sent every poll cycle over HTTPS. This is the data stream subject to the full re
278
403
 
279
404
  ## Upgrading
280
405
 
406
+ ### Manual upgrade
407
+
281
408
  ```bash
282
409
  openclaw plugins update shield
283
410
  openclaw shield status
@@ -287,6 +414,34 @@ Cursors and credentials are preserved across upgrades. See the CHANGELOG (availa
287
414
 
288
415
  > **"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.
289
416
 
417
+ ### Auto-update
418
+
419
+ Shield can check for and install newer versions automatically.
420
+
421
+ Add to `~/.openclaw/openclaw.json`:
422
+
423
+ ```json
424
+ {
425
+ "plugins": {
426
+ "entries": {
427
+ "shield": {
428
+ "config": {
429
+ "autoUpdate": "notify-only"
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+ ```
436
+
437
+ | Mode | Behavior |
438
+ |---|---|
439
+ | `false` | Disabled. No version checks. |
440
+ | `"notify-only"` | Logs available updates but does not install. |
441
+ | `true` | **(default)** Automatically installs **patch and minor** versions. Major updates require manual upgrade. |
442
+
443
+ When `autoUpdate` is `true`, Shield backs up the current version before installing and automatically rolls back if anything fails. Gateway is restarted after a successful update.
444
+
290
445
  ---
291
446
 
292
447
  ## Troubleshooting
@@ -330,7 +485,7 @@ Yes. Shield runs as a passive observer — it hooks into the event stream and do
330
485
  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
486
 
332
487
  **Where is the changelog?**
333
- the CHANGELOG, available on the Shield portal at uss.upx.com
488
+ See CHANGELOG.md included with the plugin.
334
489
 
335
490
  ---
336
491
 
package/dist/index.js CHANGED
@@ -44,10 +44,16 @@ const log_1 = require("./src/log");
44
44
  const log = __importStar(require("./src/log"));
45
45
  const version_1 = require("./src/version");
46
46
  const fs_1 = require("fs");
47
+ const safe_io_1 = require("./src/safe-io");
47
48
  const path_1 = require("path");
48
49
  const os_1 = require("os");
49
50
  const crypto_1 = require("crypto");
50
51
  const counters_1 = require("./src/counters");
52
+ const cli_cases_1 = require("./src/cli-cases");
53
+ const case_monitor_1 = require("./src/case-monitor");
54
+ const updater_1 = require("./src/updater");
55
+ const rpc_1 = require("./src/rpc");
56
+ const inventory_1 = require("./src/inventory");
51
57
  const SHIELD_API_URL = 'https://openclaw-shield.upx.com';
52
58
  async function performAutoRegistration(installationKey) {
53
59
  try {
@@ -192,11 +198,7 @@ function readAllTimeStats() {
192
198
  if (_allTimeStats)
193
199
  return _allTimeStats;
194
200
  try {
195
- if (!(0, fs_1.existsSync)(STATS_FILE)) {
196
- _allTimeStats = { eventsProcessed: 0, quarantineCount: 0 };
197
- return _allTimeStats;
198
- }
199
- _allTimeStats = JSON.parse((0, fs_1.readFileSync)(STATS_FILE, 'utf8'));
201
+ _allTimeStats = (0, safe_io_1.readJsonSafe)(STATS_FILE, { eventsProcessed: 0, quarantineCount: 0 }, 'stats');
200
202
  return _allTimeStats;
201
203
  }
202
204
  catch {
@@ -219,7 +221,7 @@ function flushAllTimeStats() {
219
221
  const dir = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data');
220
222
  if (!(0, fs_1.existsSync)(dir))
221
223
  (0, fs_1.mkdirSync)(dir, { recursive: true });
222
- (0, fs_1.writeFileSync)(STATS_FILE, JSON.stringify(_allTimeStats, null, 2));
224
+ (0, safe_io_1.writeJsonSafe)(STATS_FILE, _allTimeStats);
223
225
  _allTimeStatsDirty = false;
224
226
  }
225
227
  catch { }
@@ -244,24 +246,24 @@ function persistState(extra = {}) {
244
246
  };
245
247
  }
246
248
  catch { }
247
- (0, fs_1.writeFileSync)(STATUS_FILE, JSON.stringify({
249
+ (0, safe_io_1.writeJsonSafe)(STATUS_FILE, {
248
250
  ...state, ...extra,
249
251
  version: version_1.VERSION,
250
252
  updatedAt: Date.now(),
251
253
  pid: process.pid,
252
254
  counters: countersSnapshot,
253
255
  allTime: readAllTimeStats(),
254
- }, null, 2));
256
+ });
255
257
  _stateDirty = false;
256
258
  }
257
259
  catch { }
258
260
  }
259
261
  function readPersistedState() {
260
262
  try {
261
- if (!(0, fs_1.existsSync)(STATUS_FILE))
263
+ const d = (0, safe_io_1.readJsonSafe)(STATUS_FILE, null, 'status');
264
+ if (!d)
262
265
  return null;
263
- const d = JSON.parse((0, fs_1.readFileSync)(STATUS_FILE, 'utf8'));
264
- const age = Date.now() - (d.updatedAt || 0);
266
+ const age = Date.now() - (Number(d.updatedAt) || 0);
265
267
  if (age > 10 * 60 * 1000)
266
268
  return null;
267
269
  return d;
@@ -371,8 +373,15 @@ function printActivatedStatus() {
371
373
  console.log(` Events sent: ${allTime.eventsProcessed.toLocaleString()} (all-time)`);
372
374
  console.log(` Quarantine: ${allTime.quarantineCount.toLocaleString()} (all-time)`);
373
375
  console.log(` Failures: ${s.consecutiveFailures ?? 0} (consecutive)`);
374
- if (s.pid)
375
- console.log(` Daemon PID: ${s.pid}`);
376
+ if (s.pid) {
377
+ let pidAlive = false;
378
+ try {
379
+ process.kill(s.pid, 0);
380
+ pidAlive = true;
381
+ }
382
+ catch { }
383
+ console.log(` Daemon PID: ${s.pid}${pidAlive ? '' : ' ⚠️ stale (process not running)'}`);
384
+ }
376
385
  const statusWarnings = getStatusWarnings({
377
386
  running: isRunning,
378
387
  lastPollAt: s.lastPollAt ?? null,
@@ -455,6 +464,21 @@ function printActivatedStatus() {
455
464
  }
456
465
  console.log(' (original values never stored or transmitted)');
457
466
  }
467
+ const cmStatus = (0, case_monitor_1.getCaseMonitorStatus)();
468
+ {
469
+ console.log('');
470
+ console.log('── Case Monitor ──────────────────────────────');
471
+ const intervalSec = Math.round(cmStatus.intervalMs / 1000);
472
+ const nextSec = Math.round(cmStatus.nextCheckIn / 1000);
473
+ const lastCheckLabel = cmStatus.lastCheckAt > 0
474
+ ? (Date.now() - cmStatus.lastCheckAt < 60_000
475
+ ? `${Math.round((Date.now() - cmStatus.lastCheckAt) / 1000)}s ago`
476
+ : `${Math.floor((Date.now() - cmStatus.lastCheckAt) / 60_000)}m ago`)
477
+ : 'pending';
478
+ console.log(` Interval: ${intervalSec}s (adaptive: 60s active → 900s idle)`);
479
+ console.log(` Last check: ${lastCheckLabel}`);
480
+ console.log(` Next check: ~${nextSec}s`);
481
+ }
458
482
  }
459
483
  exports.default = {
460
484
  id: 'shield',
@@ -580,6 +604,36 @@ exports.default = {
580
604
  if (persistedStats.lastSync)
581
605
  state.lastSync = persistedStats.lastSync;
582
606
  log.info('shield', `Starting monitoring bridge v${version_1.VERSION} (poll: ${config.pollIntervalMs}ms, dryRun: ${config.dryRun})`);
607
+ (0, case_monitor_1.initCaseMonitor)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'));
608
+ const autoUpdateMode = pluginConfig.autoUpdate ?? true;
609
+ log.info('updater', `Startup update check (autoUpdate=${autoUpdateMode}, current=${version_1.VERSION})`);
610
+ const startupUpdate = (0, updater_1.performAutoUpdate)(autoUpdateMode, 0);
611
+ if (startupUpdate.action === 'updated') {
612
+ log.info('updater', startupUpdate.message);
613
+ const restarted = (0, updater_1.requestGatewayRestart)();
614
+ if (restarted) {
615
+ startGuard.endFailure();
616
+ return;
617
+ }
618
+ log.warn('updater', 'Gateway restart failed — continuing with current version in memory. Restart the gateway manually to load the updated plugin.');
619
+ }
620
+ else if (startupUpdate.action === 'none') {
621
+ log.info('updater', `Up to date (${version_1.VERSION})`);
622
+ }
623
+ else if (startupUpdate.action === 'notify') {
624
+ log.info('updater', startupUpdate.message);
625
+ }
626
+ else if (startupUpdate.action === 'rollback' || startupUpdate.action === 'error') {
627
+ log.warn('updater', startupUpdate.message);
628
+ }
629
+ try {
630
+ const inv = (0, inventory_1.collectInventory)();
631
+ const { setCachedInventory } = await Promise.resolve().then(() => __importStar(require('./src/inventory')));
632
+ setCachedInventory(inv);
633
+ }
634
+ catch (err) {
635
+ log.warn('shield', `Inventory collection failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
636
+ }
583
637
  const { fetchNewEntries, commitCursors } = await Promise.resolve().then(() => __importStar(require('./src/fetcher')));
584
638
  const { transformEntries, generateHostTelemetry, resolveOpenClawVersion, resolveAgentLabel } = await Promise.resolve().then(() => __importStar(require('./src/transformer')));
585
639
  const { sendEvents, reportInstance } = await Promise.resolve().then(() => __importStar(require('./src/sender')));
@@ -623,6 +677,20 @@ exports.default = {
623
677
  if (activeGeneration !== runtimeGeneration || !state.running)
624
678
  return;
625
679
  runTelemetrySingleflight().catch((err) => log.error('shield', `Telemetry error: ${err instanceof Error ? err.message : String(err)}`));
680
+ try {
681
+ const updateResult = (0, updater_1.performAutoUpdate)(autoUpdateMode);
682
+ if (updateResult.action !== 'none') {
683
+ log.info('updater', updateResult.message);
684
+ if (updateResult.action === 'updated') {
685
+ if (!(0, updater_1.requestGatewayRestart)()) {
686
+ log.warn('updater', 'Restart not available — new version loads on next manual restart');
687
+ }
688
+ }
689
+ }
690
+ }
691
+ catch (err) {
692
+ log.warn('updater', `Periodic update check failed: ${err instanceof Error ? err.message : String(err)}`);
693
+ }
626
694
  }, TELEMETRY_INTERVAL_MS);
627
695
  const poll = async () => {
628
696
  if (!state.running || activeGeneration !== runtimeGeneration)
@@ -678,6 +746,7 @@ exports.default = {
678
746
  }
679
747
  const accepted = results.reduce((sum, r) => sum + (r.success ? r.eventCount : 0), 0);
680
748
  if (accepted > 0) {
749
+ (0, case_monitor_1.notifyCaseMonitorActivity)();
681
750
  commitCursors(config, entries);
682
751
  flushRedactor();
683
752
  state.eventsProcessed += accepted;
@@ -700,6 +769,10 @@ exports.default = {
700
769
  state.lastPollAt = Date.now();
701
770
  markStateDirty();
702
771
  persistState();
772
+ const platformConfig = { apiUrl: config.credentials.apiUrl, instanceId: config.credentials.instanceId, hmacSecret: config.credentials.hmacSecret };
773
+ await (0, case_monitor_1.checkForNewCases)(platformConfig).catch(err => {
774
+ log.debug('case-monitor', `Check error: ${err instanceof Error ? err.message : String(err)}`);
775
+ });
703
776
  }
704
777
  catch (err) {
705
778
  if (activeGeneration !== runtimeGeneration)
@@ -758,6 +831,7 @@ exports.default = {
758
831
  api.registerGatewayMethod('shield.status', ({ respond }) => {
759
832
  const creds = (0, config_1.loadCredentials)();
760
833
  const activated = state.activated || hasValidCredentials(creds);
834
+ const caseStatus = (0, case_monitor_1.getCaseMonitorStatus)();
761
835
  respond(true, {
762
836
  activated,
763
837
  running: state.running,
@@ -768,6 +842,11 @@ exports.default = {
768
842
  quarantineCount: state.quarantineCount,
769
843
  consecutiveFailures: state.consecutiveFailures,
770
844
  version: version_1.VERSION,
845
+ caseMonitor: {
846
+ intervalMs: caseStatus.intervalMs,
847
+ nextCheckInMs: caseStatus.nextCheckIn,
848
+ lastCheckAt: caseStatus.lastCheckAt,
849
+ },
771
850
  });
772
851
  });
773
852
  api.registerGatewayMethod('shield.flush', ({ respond }) => {
@@ -779,8 +858,17 @@ exports.default = {
779
858
  .then(() => respond(true, { flushed: true }))
780
859
  .catch((err) => respond(false, { error: err instanceof Error ? err.message : String(err) }));
781
860
  });
861
+ const rpcCreds = (0, config_1.loadCredentials)();
862
+ const rpcConfig = (0, config_1.loadConfig)({});
863
+ const platformApiConfig = {
864
+ apiUrl: rpcConfig.platformApiUrl ?? null,
865
+ instanceId: state.instanceId || rpcCreds?.instanceId || '',
866
+ hmacSecret: rpcCreds?.hmacSecret || '',
867
+ };
868
+ (0, rpc_1.registerAllRpcs)(api, platformApiConfig);
782
869
  api.registerCli(({ program }) => {
783
870
  const shield = program.command('shield');
871
+ (0, cli_cases_1.registerCasesCli)(shield);
784
872
  shield.command('status')
785
873
  .description('Show Shield monitoring status and activity')
786
874
  .action(async () => {
@@ -826,6 +914,101 @@ exports.default = {
826
914
  console.error(' Get your key at: https://uss.upx.com → APPS → OpenClaw Shield');
827
915
  }
828
916
  });
917
+ const vault = shield.command('vault');
918
+ vault.command('show')
919
+ .description('Show Shield vault — agent inventory and redaction summary')
920
+ .action(async () => {
921
+ const vaultData = (0, inventory_1.readVault)();
922
+ const inventory = vaultData.host_inventory;
923
+ if (!inventory || !inventory.agents || inventory.agents.length === 0) {
924
+ console.log('No host inventory collected yet.');
925
+ console.log('Inventory is collected on gateway startup when Shield is activated.');
926
+ return;
927
+ }
928
+ console.log(`Host Inventory (collected: ${inventory.collected_at})`);
929
+ console.log('');
930
+ console.log(` Agents: ${inventory.agent_count}`);
931
+ console.log(` Workspaces: ${inventory.workspace_count}`);
932
+ console.log('');
933
+ console.log('── Agents ────────────────────────────────────');
934
+ for (const agent of inventory.agents) {
935
+ const agentHash = (0, crypto_1.createHash)('sha256').update(agent.id).digest('hex').slice(0, 8);
936
+ const wsHash = (0, crypto_1.createHash)('sha256').update(agent.workspace).digest('hex').slice(0, 8);
937
+ const bootstrapLabel = agent.is_bootstrapped ? '✅' : '⏳ pending';
938
+ const identityLabel = agent.has_identity ? '✅' : '—';
939
+ console.log(` agent:${agentHash} workspace:${wsHash} identity=${identityLabel} bootstrapped=${bootstrapLabel}`);
940
+ }
941
+ try {
942
+ const { initVault, getAllMappings } = await Promise.resolve().then(() => __importStar(require('./src/redactor/vault')));
943
+ initVault();
944
+ const mappings = getAllMappings();
945
+ const tokens = Object.keys(mappings);
946
+ if (tokens.length > 0) {
947
+ const counts = {};
948
+ for (const token of tokens) {
949
+ const colonIdx = token.indexOf(':');
950
+ const category = colonIdx > 0 ? token.slice(0, colonIdx) : 'unknown';
951
+ counts[category] = (counts[category] || 0) + 1;
952
+ }
953
+ console.log('');
954
+ console.log(`── Redactions (${tokens.length} tokens) ─────────────────────`);
955
+ for (const [category, count] of Object.entries(counts)) {
956
+ const label = count === 1 ? 'unique value' : 'unique values';
957
+ console.log(` ${(category + ':HASH').padEnd(20)} ${count} ${label}`);
958
+ }
959
+ console.log('');
960
+ console.log(' (original values encrypted locally — never transmitted)');
961
+ }
962
+ }
963
+ catch {
964
+ }
965
+ });
966
+ shield.command('logs')
967
+ .description('Show recent Shield events from local buffer')
968
+ .option('--last <n>', 'Number of events to show', '10')
969
+ .option('--type <type>', 'Filter by event type (TOOL_CALL, FILE_WRITE, etc.)')
970
+ .option('--since <duration>', 'Show events since duration (e.g. 30m, 2h, 1d)')
971
+ .option('--format <fmt>', 'Output format: table or json', 'table')
972
+ .action(async (opts) => {
973
+ const { queryEvents: qe, initEventStore: ie } = require('./src/event-store');
974
+ const os = require('os');
975
+ const path = require('path');
976
+ ie(path.join(os.homedir(), '.openclaw', 'shield', 'data'));
977
+ const limit = parseInt(opts.last, 10) || 10;
978
+ let sinceMs;
979
+ if (opts.since) {
980
+ const match = opts.since.match(/^(\d+)(m|h|d)$/);
981
+ if (match) {
982
+ const val = parseInt(match[1], 10);
983
+ const unit = match[2];
984
+ sinceMs = val * (unit === 'm' ? 60000 : unit === 'h' ? 3600000 : 86400000);
985
+ }
986
+ else {
987
+ console.error(`Invalid duration: ${opts.since}. Use format: 30m, 2h, 1d`);
988
+ return;
989
+ }
990
+ }
991
+ const events = qe({ limit, type: opts.type, sinceMs });
992
+ if (events.length === 0) {
993
+ console.log('No events found.');
994
+ console.log('Events are stored after successful transmission to the platform.');
995
+ return;
996
+ }
997
+ if (opts.format === 'json') {
998
+ console.log(JSON.stringify(events, null, 2));
999
+ return;
1000
+ }
1001
+ console.log(`Recent Events (${events.length}):`);
1002
+ console.log('');
1003
+ for (const e of events) {
1004
+ const time = new Date(e.ts).toLocaleTimeString();
1005
+ const flag = e.redacted ? ' 🔒' : '';
1006
+ const summaryTrunc = e.summary.length > 60 ? e.summary.slice(0, 57) + '...' : e.summary;
1007
+ console.log(` ${time} ${e.type.padEnd(12)} ${e.tool.padEnd(10)} ${summaryTrunc}${flag}`);
1008
+ }
1009
+ console.log('');
1010
+ console.log(` Showing ${events.length} events. Use --last N for more, --format json for details.`);
1011
+ });
829
1012
  }, { commands: ['shield'] });
830
1013
  },
831
1014
  };
@@ -0,0 +1,24 @@
1
+ import { type PlatformApiConfig } from './rpc/client';
2
+ export interface CaseSummary {
3
+ id: string;
4
+ rule_id: string;
5
+ rule_title: string;
6
+ severity: string;
7
+ status: string;
8
+ created_at: string;
9
+ summary: string;
10
+ event_count: number;
11
+ agent_id?: string;
12
+ }
13
+ export declare function initCaseMonitor(dataDir: string): void;
14
+ export declare function notifyCaseMonitorActivity(): void;
15
+ export declare function getCaseMonitorStatus(): {
16
+ intervalMs: number;
17
+ nextCheckIn: number;
18
+ lastCheckAt: number;
19
+ };
20
+ export declare function checkForNewCases(config: PlatformApiConfig): Promise<void>;
21
+ export declare function getPendingCases(): CaseSummary[];
22
+ export declare function acknowledgeCases(caseIds: string[]): void;
23
+ export declare function formatCaseNotification(c: CaseSummary): string;
24
+ export declare function _resetForTesting(): void;