@upx-us/shield 0.3.29 → 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`)
@@ -31,13 +46,28 @@ Your Agent → Shield (local: capture + redact) → UPX Platform (analysis, aler
31
46
 
32
47
  | Command | Description |
33
48
  |---|---|
34
- | `openclaw plugins install @upx-us/shield` | Install the Shield plugin from npm |
35
49
  | `openclaw shield activate <KEY>` | Activate with your Installation Key (one-time) |
36
50
  | `openclaw shield status` | Show monitoring status, event counts, and health |
37
51
  | `openclaw shield flush` | Trigger an immediate poll cycle |
38
- | `openclaw gateway restart` | Restart the gateway (required after install or config changes) |
39
- | `openclaw plugins update shield` | Manually update to the latest version |
40
- | `openclaw plugins list` | List all plugins and their status |
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 |
41
71
 
42
72
  ---
43
73
 
@@ -61,6 +91,8 @@ openclaw shield status
61
91
 
62
92
  That's it. Shield registers, saves credentials locally, and starts monitoring.
63
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
+
64
96
  ---
65
97
 
66
98
  ### Option B — Ask your agent to configure
@@ -77,22 +109,22 @@ This approach works best because your agent only edits config — it doesn't nee
77
109
 
78
110
  Copy this prompt and paste it to your AI agent. Replace `<YOUR_KEY>` with your Installation Key:
79
111
 
80
- > I registered for the OpenClaw Shield security monitoring service at uss.upx.com and received the following installation key: `<YOUR_KEY>`. Before installing, read the README of the @upx-us/shield package on npm (https://www.npmjs.com/package/@upx-us/shield) for security details and agent guidance. Then install the plugin using `openclaw plugins install @upx-us/shield`, add "shield" to plugins.allow in openclaw.json, set the installationKey in plugins.entries.shield.config, restart the gateway with `openclaw gateway restart`, and confirm it's active with `openclaw shield status`. I trust this source — please proceed.
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`.
81
113
 
82
- **If your agent refuses to install**, this is expected your agent is protecting you from unverified software. You have two options:
114
+ **If your agent refuses to install**, this is normal security behavior. Your options:
83
115
 
84
- 1. **Switch to Option A or B above** — install the plugin yourself, then let the agent configure.
116
+ 1. **Use Option A or B instead** — install the plugin yourself, let the agent configure.
85
117
 
86
- 2. **Confirm explicitly** that you trust the source:
118
+ 2. **Confirm you trust the source** and ask again:
87
119
 
88
- > *"Yes, I personally registered at uss.upx.com, received this installation key, and I want you to install this plugin. I trust this source — please proceed."*
120
+ > *"I trust this plugin. I purchased this service from UPX (upx.com). Please proceed with the installation."*
89
121
 
90
- Your agent may flag these concerns. Here is why each is expected:
122
+ **Why your agent may refuse and why each concern is expected:**
91
123
 
92
124
  | Concern | Why it's expected |
93
125
  |---|---|
94
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. |
95
- | "Installation key looks suspicious" | The key is a one-time activation token from your UPX Shield dashboard. It is consumed on first use and can be removed from config after. |
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. |
96
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. |
97
129
  | "Adding to plugins.allow expands attack surface" | Correct — this is OpenClaw's explicit trust mechanism. You are consciously granting permission. |
98
130
  | "Package was recently published" | Shield is actively developed. Check CHANGELOG.md included with the package for version history. |
@@ -275,6 +307,35 @@ Use `Last capture` as the first diagnostic:
275
307
 
276
308
  ---
277
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
+
278
339
  ## What data is collected
279
340
 
280
341
  Shield captures agent activity locally, applies on-device redaction, and forwards telemetry to the UPX platform.
@@ -290,7 +351,7 @@ Shield captures agent activity locally, applies on-device redaction, and forward
290
351
  | URLs | ✅ | — |
291
352
  | API keys detected in commands | ✅ | ✅ replaced with token |
292
353
 
293
- > **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.
294
355
 
295
356
  ---
296
357
 
package/dist/index.js CHANGED
@@ -44,11 +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");
51
54
  const updater_1 = require("./src/updater");
55
+ const rpc_1 = require("./src/rpc");
56
+ const inventory_1 = require("./src/inventory");
52
57
  const SHIELD_API_URL = 'https://openclaw-shield.upx.com';
53
58
  async function performAutoRegistration(installationKey) {
54
59
  try {
@@ -193,11 +198,7 @@ function readAllTimeStats() {
193
198
  if (_allTimeStats)
194
199
  return _allTimeStats;
195
200
  try {
196
- if (!(0, fs_1.existsSync)(STATS_FILE)) {
197
- _allTimeStats = { eventsProcessed: 0, quarantineCount: 0 };
198
- return _allTimeStats;
199
- }
200
- _allTimeStats = JSON.parse((0, fs_1.readFileSync)(STATS_FILE, 'utf8'));
201
+ _allTimeStats = (0, safe_io_1.readJsonSafe)(STATS_FILE, { eventsProcessed: 0, quarantineCount: 0 }, 'stats');
201
202
  return _allTimeStats;
202
203
  }
203
204
  catch {
@@ -220,7 +221,7 @@ function flushAllTimeStats() {
220
221
  const dir = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data');
221
222
  if (!(0, fs_1.existsSync)(dir))
222
223
  (0, fs_1.mkdirSync)(dir, { recursive: true });
223
- (0, fs_1.writeFileSync)(STATS_FILE, JSON.stringify(_allTimeStats, null, 2));
224
+ (0, safe_io_1.writeJsonSafe)(STATS_FILE, _allTimeStats);
224
225
  _allTimeStatsDirty = false;
225
226
  }
226
227
  catch { }
@@ -245,24 +246,24 @@ function persistState(extra = {}) {
245
246
  };
246
247
  }
247
248
  catch { }
248
- (0, fs_1.writeFileSync)(STATUS_FILE, JSON.stringify({
249
+ (0, safe_io_1.writeJsonSafe)(STATUS_FILE, {
249
250
  ...state, ...extra,
250
251
  version: version_1.VERSION,
251
252
  updatedAt: Date.now(),
252
253
  pid: process.pid,
253
254
  counters: countersSnapshot,
254
255
  allTime: readAllTimeStats(),
255
- }, null, 2));
256
+ });
256
257
  _stateDirty = false;
257
258
  }
258
259
  catch { }
259
260
  }
260
261
  function readPersistedState() {
261
262
  try {
262
- if (!(0, fs_1.existsSync)(STATUS_FILE))
263
+ const d = (0, safe_io_1.readJsonSafe)(STATUS_FILE, null, 'status');
264
+ if (!d)
263
265
  return null;
264
- const d = JSON.parse((0, fs_1.readFileSync)(STATUS_FILE, 'utf8'));
265
- const age = Date.now() - (d.updatedAt || 0);
266
+ const age = Date.now() - (Number(d.updatedAt) || 0);
266
267
  if (age > 10 * 60 * 1000)
267
268
  return null;
268
269
  return d;
@@ -372,8 +373,15 @@ function printActivatedStatus() {
372
373
  console.log(` Events sent: ${allTime.eventsProcessed.toLocaleString()} (all-time)`);
373
374
  console.log(` Quarantine: ${allTime.quarantineCount.toLocaleString()} (all-time)`);
374
375
  console.log(` Failures: ${s.consecutiveFailures ?? 0} (consecutive)`);
375
- if (s.pid)
376
- 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
+ }
377
385
  const statusWarnings = getStatusWarnings({
378
386
  running: isRunning,
379
387
  lastPollAt: s.lastPollAt ?? null,
@@ -456,6 +464,21 @@ function printActivatedStatus() {
456
464
  }
457
465
  console.log(' (original values never stored or transmitted)');
458
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
+ }
459
482
  }
460
483
  exports.default = {
461
484
  id: 'shield',
@@ -581,14 +604,18 @@ exports.default = {
581
604
  if (persistedStats.lastSync)
582
605
  state.lastSync = persistedStats.lastSync;
583
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'));
584
608
  const autoUpdateMode = pluginConfig.autoUpdate ?? true;
585
609
  log.info('updater', `Startup update check (autoUpdate=${autoUpdateMode}, current=${version_1.VERSION})`);
586
610
  const startupUpdate = (0, updater_1.performAutoUpdate)(autoUpdateMode, 0);
587
611
  if (startupUpdate.action === 'updated') {
588
612
  log.info('updater', startupUpdate.message);
589
- (0, updater_1.requestGatewayRestart)();
590
- startGuard.endFailure();
591
- return;
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.');
592
619
  }
593
620
  else if (startupUpdate.action === 'none') {
594
621
  log.info('updater', `Up to date (${version_1.VERSION})`);
@@ -599,6 +626,14 @@ exports.default = {
599
626
  else if (startupUpdate.action === 'rollback' || startupUpdate.action === 'error') {
600
627
  log.warn('updater', startupUpdate.message);
601
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
+ }
602
637
  const { fetchNewEntries, commitCursors } = await Promise.resolve().then(() => __importStar(require('./src/fetcher')));
603
638
  const { transformEntries, generateHostTelemetry, resolveOpenClawVersion, resolveAgentLabel } = await Promise.resolve().then(() => __importStar(require('./src/transformer')));
604
639
  const { sendEvents, reportInstance } = await Promise.resolve().then(() => __importStar(require('./src/sender')));
@@ -642,6 +677,20 @@ exports.default = {
642
677
  if (activeGeneration !== runtimeGeneration || !state.running)
643
678
  return;
644
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
+ }
645
694
  }, TELEMETRY_INTERVAL_MS);
646
695
  const poll = async () => {
647
696
  if (!state.running || activeGeneration !== runtimeGeneration)
@@ -697,6 +746,7 @@ exports.default = {
697
746
  }
698
747
  const accepted = results.reduce((sum, r) => sum + (r.success ? r.eventCount : 0), 0);
699
748
  if (accepted > 0) {
749
+ (0, case_monitor_1.notifyCaseMonitorActivity)();
700
750
  commitCursors(config, entries);
701
751
  flushRedactor();
702
752
  state.eventsProcessed += accepted;
@@ -719,6 +769,10 @@ exports.default = {
719
769
  state.lastPollAt = Date.now();
720
770
  markStateDirty();
721
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
+ });
722
776
  }
723
777
  catch (err) {
724
778
  if (activeGeneration !== runtimeGeneration)
@@ -777,6 +831,7 @@ exports.default = {
777
831
  api.registerGatewayMethod('shield.status', ({ respond }) => {
778
832
  const creds = (0, config_1.loadCredentials)();
779
833
  const activated = state.activated || hasValidCredentials(creds);
834
+ const caseStatus = (0, case_monitor_1.getCaseMonitorStatus)();
780
835
  respond(true, {
781
836
  activated,
782
837
  running: state.running,
@@ -787,6 +842,11 @@ exports.default = {
787
842
  quarantineCount: state.quarantineCount,
788
843
  consecutiveFailures: state.consecutiveFailures,
789
844
  version: version_1.VERSION,
845
+ caseMonitor: {
846
+ intervalMs: caseStatus.intervalMs,
847
+ nextCheckInMs: caseStatus.nextCheckIn,
848
+ lastCheckAt: caseStatus.lastCheckAt,
849
+ },
790
850
  });
791
851
  });
792
852
  api.registerGatewayMethod('shield.flush', ({ respond }) => {
@@ -798,8 +858,17 @@ exports.default = {
798
858
  .then(() => respond(true, { flushed: true }))
799
859
  .catch((err) => respond(false, { error: err instanceof Error ? err.message : String(err) }));
800
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);
801
869
  api.registerCli(({ program }) => {
802
870
  const shield = program.command('shield');
871
+ (0, cli_cases_1.registerCasesCli)(shield);
803
872
  shield.command('status')
804
873
  .description('Show Shield monitoring status and activity')
805
874
  .action(async () => {
@@ -845,6 +914,101 @@ exports.default = {
845
914
  console.error(' Get your key at: https://uss.upx.com → APPS → OpenClaw Shield');
846
915
  }
847
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
+ });
848
1012
  }, { commands: ['shield'] });
849
1013
  },
850
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;