@upx-us/shield 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +49 -6
- package/dist/index.js +37 -0
- package/dist/src/attributor.d.ts +15 -0
- package/dist/src/attributor.js +257 -0
- package/dist/src/config.js +1 -1
- package/dist/src/event-store.js +1 -1
- package/dist/src/events/base.d.ts +13 -0
- package/dist/src/events/message/enrich.js +1 -0
- package/dist/src/index.js +1 -1
- package/dist/src/transformer.d.ts +1 -1
- package/dist/src/transformer.js +83 -1
- package/dist/src/validator.js +48 -0
- package/openclaw.plugin.json +4 -4
- package/package.json +4 -1
- package/skills/shield/SKILL.md +68 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.7.0] — 2026-03-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Event authorship & source attribution** — Shield now captures the source of every monitored agent action: who or what triggered it. Each event is tagged with an authorship category — a user via a messaging channel (Telegram, Discord, WhatsApp, etc.), a scheduled job, a periodic health check, a spawned sub-agent, or an autonomous agent action. This context appears alongside every event in your security dashboard, enabling "who caused this?" forensics on any alert or case.
|
|
11
|
+
- **Sender identity captured when available** — when an action is triggered by a user via a messaging channel, the user's display name and channel are captured in plain text for human-readable alerting. The raw user identifier is stored as a reversible token (recoverable locally via `openclaw shield vault show`).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **README updated** — new "Authorship & Event Source" section documents the authorship categories available in the dashboard.
|
|
15
|
+
|
|
16
|
+
### Notes
|
|
17
|
+
- Fully backward compatible. Existing deployments continue to work without any configuration changes. Authorship context is captured automatically on upgrade.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## [0.6.10] — 2026-03-12
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **Local event buffer not populated in plugin mode** — `appendEvents` and `initEventStore` were implemented in the standalone bridge (`src/index.ts`) but never wired into the plugin-mode poll loop (`index.ts`). As a result, `openclaw shield logs` always returned "No events found" even when events were being sent successfully to the platform. Fixed by wiring the event store into the plugin-mode startup and poll cycle (#148).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **Local event buffer default increased to 250** — was 123. Provides better forensic coverage (~24h of normal activity) without significant storage impact. `SHIELD_LOCAL_EVENT_LIMIT` env var still overrides this. README and config updated (#148).
|
|
28
|
+
- **Version corrected from 1.3.0 → 0.6.10** — the 1.3.0 version in `package.json` was premature; this release re-anchors versioning at the correct next increment after 0.6.8.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **`openclaw.message_length_bytes` enrichment field** — message events now include the byte length of the message body as `openclaw.message_length_bytes`. Enables detection rules based on message size thresholds (#152).
|
|
32
|
+
- **Event buffer diagnostics in `shield.status`** — `shield.status` RPC now includes an `eventBuffer` section showing: `enabled`, `path`, `total` events buffered, `oldest`/`newest` timestamps, and `redactedCount`. Makes it immediately diagnosable if the local log is empty or disabled (#148).
|
|
33
|
+
|
|
34
|
+
### Docs
|
|
35
|
+
- **Case investigation workflow in SKILL.md** — documents the standard 4-step forensic procedure: cases + logs + vault correlation for complete incident context. Covers correlating case timestamps, log event windows, and redaction vault lookups (#148, #151).
|
|
36
|
+
- **Expanded uninstall documentation in README** — step-by-step uninstall guide covering plugin removal, local data cleanup (with file-by-file breakdown), and platform instance deactivation. Includes redaction vault retention warning (#151).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
7
40
|
## [0.6.8] — 2026-03-10
|
|
8
41
|
|
|
9
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -309,7 +309,7 @@ Shield stores a rolling log of recently sent events locally for offline inspecti
|
|
|
309
309
|
**Location:** `~/.openclaw/shield/data/event-log.jsonl`
|
|
310
310
|
|
|
311
311
|
**Defaults:**
|
|
312
|
-
- Last **
|
|
312
|
+
- Last **250 events** or **24 hours** (whichever limit is reached first)
|
|
313
313
|
- Enabled by default
|
|
314
314
|
- Events are stored **after redaction** — same data sent to the platform
|
|
315
315
|
|
|
@@ -327,7 +327,7 @@ openclaw shield logs --format json # JSON output for piping
|
|
|
327
327
|
| Variable | Default | Description |
|
|
328
328
|
|---|---|---|
|
|
329
329
|
| `SHIELD_LOCAL_EVENT_BUFFER` | `true` | Set to `false` to disable local storage |
|
|
330
|
-
| `SHIELD_LOCAL_EVENT_LIMIT` | `
|
|
330
|
+
| `SHIELD_LOCAL_EVENT_LIMIT` | `250` | Maximum number of events to retain |
|
|
331
331
|
|
|
332
332
|
---
|
|
333
333
|
|
|
@@ -350,6 +350,24 @@ Shield captures agent activity locally, applies on-device redaction, and forward
|
|
|
350
350
|
|
|
351
351
|
---
|
|
352
352
|
|
|
353
|
+
## Authorship & Event Source
|
|
354
|
+
|
|
355
|
+
Shield captures the **source** of every event — who or what triggered the agent action being monitored.
|
|
356
|
+
|
|
357
|
+
Each event is tagged with an authorship category:
|
|
358
|
+
|
|
359
|
+
| Category | Description |
|
|
360
|
+
|---|---|
|
|
361
|
+
| Communication channel | A user interacted with the agent via a messaging platform (e.g. Telegram, Discord, WhatsApp) |
|
|
362
|
+
| Scheduled job | The action was triggered by a scheduled or automated task |
|
|
363
|
+
| Heartbeat | The action was triggered by a periodic health check |
|
|
364
|
+
| Sub-agent | The action originated from a child agent spawned by a parent |
|
|
365
|
+
| Autonomous | The agent acted on its own initiative without a direct human trigger |
|
|
366
|
+
|
|
367
|
+
This information appears alongside every event in your security dashboard, enabling you to answer **"who caused this?"** for any alert or case.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
353
371
|
## What is sent to the platform
|
|
354
372
|
|
|
355
373
|
Shield uses two separate channels with different privacy properties:
|
|
@@ -498,24 +516,49 @@ Events generated during an expiry or quota period are permanently lost. Renew yo
|
|
|
498
516
|
|
|
499
517
|
## Uninstalling
|
|
500
518
|
|
|
519
|
+
### 1. Remove the plugin
|
|
520
|
+
|
|
501
521
|
```bash
|
|
502
522
|
openclaw plugins uninstall shield
|
|
503
523
|
```
|
|
504
524
|
|
|
505
|
-
|
|
525
|
+
### 2. Remove local Shield data (optional — recommended for privacy hygiene)
|
|
506
526
|
|
|
507
527
|
```bash
|
|
508
528
|
rm -rf ~/.openclaw/shield/
|
|
509
529
|
```
|
|
510
530
|
|
|
511
|
-
This
|
|
531
|
+
This removes all local files Shield writes at runtime:
|
|
512
532
|
|
|
513
|
-
|
|
533
|
+
| File | Contents |
|
|
534
|
+
|---|---|
|
|
535
|
+
| `config.env` | Signing key and instance fingerprint |
|
|
536
|
+
| `data/cursor.json` | Event cursor positions per log source |
|
|
537
|
+
| `data/public-ip.cache` | Cached public IP for telemetry |
|
|
538
|
+
| `data/instance-cache.json` | Cached instance metadata |
|
|
539
|
+
| `data/exclusions.json` | Allowlist entries (suppress specific patterns) |
|
|
540
|
+
| `data/case-monitor-state.json` | Case notification state (acknowledged case IDs) |
|
|
541
|
+
| `data/event-log.jsonl` | Local event ring buffer (24h) |
|
|
542
|
+
| `data/redaction-vault.json` | **Redaction token → original value mapping** |
|
|
543
|
+
|
|
544
|
+
> ⚠️ **Redaction vault**: `data/redaction-vault.json` maps redaction tokens back to original values (secrets, hostnames, file paths). Once deleted, you cannot reverse-lookup redacted values from past events. Retain this file for as long as your data retention policy requires before deleting.
|
|
545
|
+
|
|
546
|
+
### 3. Deactivate the platform instance
|
|
547
|
+
|
|
548
|
+
Removing local files does **not** automatically deactivate your registration on the UPX platform.
|
|
549
|
+
|
|
550
|
+
To fully remove your instance: log in to **[uss.upx.com](https://uss.upx.com)** → Instances → Delete.
|
|
514
551
|
|
|
515
|
-
|
|
552
|
+
Until deactivated, the instance remains registered and may count against your subscription quota.
|
|
516
553
|
|
|
517
554
|
---
|
|
518
555
|
|
|
519
556
|
## Need help?
|
|
520
557
|
|
|
521
558
|
Visit [uss.upx.com](https://uss.upx.com) or contact your Shield administrator.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## License & Distribution
|
|
563
|
+
|
|
564
|
+
The skill wrapper is distributed under MIT-0 on ClaWHub as required by the platform. The underlying plugin and platform remain proprietary UPX Technologies, Inc. IP.
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ const counters_1 = require("./src/counters");
|
|
|
52
52
|
const cli_cases_1 = require("./src/cli-cases");
|
|
53
53
|
const case_monitor_1 = require("./src/case-monitor");
|
|
54
54
|
const exclusions_1 = require("./src/exclusions");
|
|
55
|
+
const event_store_1 = require("./src/event-store");
|
|
55
56
|
const updater_1 = require("./src/updater");
|
|
56
57
|
const rpc_1 = require("./src/rpc");
|
|
57
58
|
const inventory_1 = require("./src/inventory");
|
|
@@ -594,6 +595,9 @@ exports.default = {
|
|
|
594
595
|
log.info('shield', `Starting monitoring bridge v${version_1.VERSION} (poll: ${config.pollIntervalMs}ms, dryRun: ${config.dryRun})`);
|
|
595
596
|
(0, exclusions_1.initExclusions)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'));
|
|
596
597
|
(0, case_monitor_1.initCaseMonitor)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'));
|
|
598
|
+
if (config.localEventBuffer) {
|
|
599
|
+
(0, event_store_1.initEventStore)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'), { maxEvents: config.localEventLimit });
|
|
600
|
+
}
|
|
597
601
|
const runtime = api.runtime;
|
|
598
602
|
if (runtime?.system?.enqueueSystemEvent && runtime?.system?.requestHeartbeatNow) {
|
|
599
603
|
const config = api.config;
|
|
@@ -826,6 +830,18 @@ exports.default = {
|
|
|
826
830
|
state.lastSync = lastSync;
|
|
827
831
|
state.captureSeenSinceLastSync = false;
|
|
828
832
|
writeAllTimeStats({ eventsProcessed: accepted, lastSync });
|
|
833
|
+
if (config.localEventBuffer) {
|
|
834
|
+
const summaries = envelopes.map(env => ({
|
|
835
|
+
ts: env.event.timestamp,
|
|
836
|
+
type: env.event.event_type || 'UNKNOWN',
|
|
837
|
+
tool: env.event.tool_name || 'unknown',
|
|
838
|
+
summary: env.event.tool_metadata?.['openclaw.display_summary'] || env.event.tool_name || 'event',
|
|
839
|
+
session: env.event.session_id || '?',
|
|
840
|
+
model: env.event.model || '?',
|
|
841
|
+
redacted: !!env.source?.plugin?.redaction_applied,
|
|
842
|
+
}));
|
|
843
|
+
(0, event_store_1.appendEvents)(summaries);
|
|
844
|
+
}
|
|
829
845
|
}
|
|
830
846
|
else {
|
|
831
847
|
state.consecutiveFailures++;
|
|
@@ -918,8 +934,11 @@ exports.default = {
|
|
|
918
934
|
vaultSummary = null;
|
|
919
935
|
}
|
|
920
936
|
}
|
|
937
|
+
const hasSecret = !!creds?.hmacSecret && !PLACEHOLDER_VALUES.has((creds.hmacSecret || '').trim().toLowerCase());
|
|
938
|
+
const stateField = hasSecret ? (activated ? 'connected' : 'pending') : 'unconfigured';
|
|
921
939
|
respond(true, {
|
|
922
940
|
activated,
|
|
941
|
+
state: stateField,
|
|
923
942
|
running: state.running,
|
|
924
943
|
lastPollAt: state.lastPollAt,
|
|
925
944
|
lastCaptureAt: state.lastCaptureAt,
|
|
@@ -946,6 +965,24 @@ exports.default = {
|
|
|
946
965
|
redaction: {
|
|
947
966
|
vault: vaultSummary,
|
|
948
967
|
},
|
|
968
|
+
eventBuffer: (() => {
|
|
969
|
+
try {
|
|
970
|
+
const { summarizeEvents: se } = require('./src/event-store');
|
|
971
|
+
const rpcConfig = (0, config_1.loadConfig)({});
|
|
972
|
+
const summary = se();
|
|
973
|
+
return {
|
|
974
|
+
enabled: rpcConfig.localEventBuffer,
|
|
975
|
+
path: (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data', 'event-log.jsonl'),
|
|
976
|
+
total: summary.total,
|
|
977
|
+
oldest: summary.oldest,
|
|
978
|
+
newest: summary.newest,
|
|
979
|
+
redactedCount: summary.redactedCount,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
catch (_) {
|
|
983
|
+
return { enabled: false, path: null, total: 0, oldest: null, newest: null, redactedCount: 0 };
|
|
984
|
+
}
|
|
985
|
+
})(),
|
|
949
986
|
});
|
|
950
987
|
});
|
|
951
988
|
api.registerGatewayMethod('shield.flush', ({ respond }) => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type TriggerType = 'user_message' | 'cron' | 'heartbeat' | 'subagent' | 'autonomous' | 'unknown';
|
|
2
|
+
export declare const TRIGGER_TYPES: readonly TriggerType[];
|
|
3
|
+
export interface AttributionContext {
|
|
4
|
+
trigger_type: TriggerType;
|
|
5
|
+
author_name?: string;
|
|
6
|
+
author_channel?: string;
|
|
7
|
+
author_hash?: string;
|
|
8
|
+
prompt_hash?: string;
|
|
9
|
+
session_label?: string;
|
|
10
|
+
parent_agent_id?: string;
|
|
11
|
+
cron_schedule?: string;
|
|
12
|
+
conversation_depth?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function clearAttributionCache(): void;
|
|
15
|
+
export declare function resolveAttribution(sessionId: string, agentId: string, sessionDir: string): AttributionContext;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TRIGGER_TYPES = void 0;
|
|
37
|
+
exports.clearAttributionCache = clearAttributionCache;
|
|
38
|
+
exports.resolveAttribution = resolveAttribution;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const vault_1 = require("./redactor/vault");
|
|
42
|
+
exports.TRIGGER_TYPES = [
|
|
43
|
+
'user_message', 'cron', 'heartbeat', 'subagent', 'autonomous', 'unknown',
|
|
44
|
+
];
|
|
45
|
+
const _attributionCache = new Map();
|
|
46
|
+
function clearAttributionCache() {
|
|
47
|
+
_attributionCache.clear();
|
|
48
|
+
}
|
|
49
|
+
const MAX_SCAN_LINES = 50;
|
|
50
|
+
const READ_BUF_SIZE = 32768;
|
|
51
|
+
function readFirstLines(filePath, maxLines) {
|
|
52
|
+
if (!fs.existsSync(filePath))
|
|
53
|
+
return [];
|
|
54
|
+
let fd = null;
|
|
55
|
+
try {
|
|
56
|
+
fd = fs.openSync(filePath, 'r');
|
|
57
|
+
const buf = Buffer.alloc(READ_BUF_SIZE);
|
|
58
|
+
const bytesRead = fs.readSync(fd, buf, 0, READ_BUF_SIZE, 0);
|
|
59
|
+
const text = buf.toString('utf8', 0, bytesRead);
|
|
60
|
+
return text
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map(l => l.trim())
|
|
63
|
+
.filter(l => l.length > 0)
|
|
64
|
+
.slice(0, maxLines);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (fd !== null) {
|
|
71
|
+
try {
|
|
72
|
+
fs.closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function extractSenderBlock(text) {
|
|
79
|
+
const match = text.match(/Sender \(untrusted metadata\):\s*```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
80
|
+
if (!match)
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(match[1]);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function extractConversationBlock(text) {
|
|
90
|
+
const match = text.match(/Conversation info \(untrusted metadata\):\s*```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
91
|
+
if (!match)
|
|
92
|
+
return null;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(match[1]);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function inferChannel(block) {
|
|
101
|
+
if (!block)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (block.channel)
|
|
104
|
+
return block.channel;
|
|
105
|
+
const label = block.conversation_label || '';
|
|
106
|
+
if (!label)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (/id:-\d+/.test(label))
|
|
109
|
+
return 'telegram';
|
|
110
|
+
if (/discord/i.test(label))
|
|
111
|
+
return 'discord';
|
|
112
|
+
if (/whatsapp/i.test(label))
|
|
113
|
+
return 'whatsapp';
|
|
114
|
+
if (/slack/i.test(label))
|
|
115
|
+
return 'slack';
|
|
116
|
+
if (/signal/i.test(label))
|
|
117
|
+
return 'signal';
|
|
118
|
+
if (/imessage/i.test(label))
|
|
119
|
+
return 'imessage';
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const HEARTBEAT_PATTERN = /\bHEARTBEAT_OK\b|\bRead HEARTBEAT\.md\b|\bheartbeat\b/i;
|
|
123
|
+
const CRON_PATTERN = /\bcron\b|\bscheduled job\b|\bscheduled task\b/i;
|
|
124
|
+
const SCHEDULE_EXPR_RE = /(?:\*\/\d+|\d+|\*)\s+(?:\*\/\d+|\d+|\*)\s+(?:\*\/\d+|\d+|\*)\s+(?:\*\/\d+|\d+|\*)\s+(?:\*\/\d+|\d+|\*)/;
|
|
125
|
+
const SUBAGENT_PATTERN = /subagent|sub-agent/i;
|
|
126
|
+
function extractCronSchedule(text) {
|
|
127
|
+
const m = text.match(SCHEDULE_EXPR_RE);
|
|
128
|
+
return m ? m[0].trim() : undefined;
|
|
129
|
+
}
|
|
130
|
+
function resolveAttribution(sessionId, agentId, sessionDir) {
|
|
131
|
+
const cached = _attributionCache.get(sessionId);
|
|
132
|
+
if (cached)
|
|
133
|
+
return cached;
|
|
134
|
+
const filePath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
135
|
+
const lines = readFirstLines(filePath, MAX_SCAN_LINES);
|
|
136
|
+
if (lines.length === 0) {
|
|
137
|
+
return { trigger_type: 'unknown' };
|
|
138
|
+
}
|
|
139
|
+
let sessionLabel;
|
|
140
|
+
let parentAgentId;
|
|
141
|
+
try {
|
|
142
|
+
const first = JSON.parse(lines[0]);
|
|
143
|
+
if (first.type === 'session') {
|
|
144
|
+
if (first.label)
|
|
145
|
+
sessionLabel = String(first.label);
|
|
146
|
+
if (first.parentAgentId)
|
|
147
|
+
parentAgentId = String(first.parentAgentId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch { }
|
|
151
|
+
if ((sessionLabel && SUBAGENT_PATTERN.test(sessionLabel)) ||
|
|
152
|
+
SUBAGENT_PATTERN.test(sessionId) ||
|
|
153
|
+
(parentAgentId != null)) {
|
|
154
|
+
const ctx = {
|
|
155
|
+
trigger_type: 'subagent',
|
|
156
|
+
session_label: sessionLabel,
|
|
157
|
+
parent_agent_id: parentAgentId || agentId,
|
|
158
|
+
};
|
|
159
|
+
_attributionCache.set(sessionId, ctx);
|
|
160
|
+
return ctx;
|
|
161
|
+
}
|
|
162
|
+
let userMessageCount = 0;
|
|
163
|
+
let firstUserText;
|
|
164
|
+
let senderBlock = null;
|
|
165
|
+
let conversationBlock = null;
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
let entry;
|
|
168
|
+
try {
|
|
169
|
+
entry = JSON.parse(line);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (entry.type !== 'message')
|
|
175
|
+
continue;
|
|
176
|
+
const msg = entry.message;
|
|
177
|
+
if (!msg || msg.role !== 'user')
|
|
178
|
+
continue;
|
|
179
|
+
userMessageCount++;
|
|
180
|
+
const textContent = Array.isArray(msg.content)
|
|
181
|
+
? msg.content
|
|
182
|
+
.filter((c) => c.type === 'text')
|
|
183
|
+
.map((c) => String(c.text || ''))
|
|
184
|
+
.join('\n')
|
|
185
|
+
: typeof msg.content === 'string'
|
|
186
|
+
? msg.content
|
|
187
|
+
: '';
|
|
188
|
+
if (!firstUserText)
|
|
189
|
+
firstUserText = textContent;
|
|
190
|
+
if (!senderBlock)
|
|
191
|
+
senderBlock = extractSenderBlock(textContent);
|
|
192
|
+
if (!conversationBlock)
|
|
193
|
+
conversationBlock = extractConversationBlock(textContent);
|
|
194
|
+
if (senderBlock && conversationBlock)
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (userMessageCount === 0) {
|
|
198
|
+
const ctx = {
|
|
199
|
+
trigger_type: 'autonomous',
|
|
200
|
+
session_label: sessionLabel,
|
|
201
|
+
};
|
|
202
|
+
_attributionCache.set(sessionId, ctx);
|
|
203
|
+
return ctx;
|
|
204
|
+
}
|
|
205
|
+
if (firstUserText && HEARTBEAT_PATTERN.test(firstUserText)) {
|
|
206
|
+
const ctx = {
|
|
207
|
+
trigger_type: 'heartbeat',
|
|
208
|
+
session_label: sessionLabel,
|
|
209
|
+
conversation_depth: userMessageCount,
|
|
210
|
+
};
|
|
211
|
+
_attributionCache.set(sessionId, ctx);
|
|
212
|
+
return ctx;
|
|
213
|
+
}
|
|
214
|
+
if (firstUserText && (CRON_PATTERN.test(firstUserText) || SCHEDULE_EXPR_RE.test(firstUserText))) {
|
|
215
|
+
const cronSchedule = extractCronSchedule(firstUserText);
|
|
216
|
+
const ctx = {
|
|
217
|
+
trigger_type: 'cron',
|
|
218
|
+
session_label: sessionLabel,
|
|
219
|
+
cron_schedule: cronSchedule,
|
|
220
|
+
conversation_depth: userMessageCount,
|
|
221
|
+
};
|
|
222
|
+
_attributionCache.set(sessionId, ctx);
|
|
223
|
+
return ctx;
|
|
224
|
+
}
|
|
225
|
+
if (senderBlock) {
|
|
226
|
+
const authorName = senderBlock.label || senderBlock.name || senderBlock.username;
|
|
227
|
+
const authorChannel = inferChannel(conversationBlock) ?? (senderBlock.channel);
|
|
228
|
+
const rawId = senderBlock.id != null
|
|
229
|
+
? String(senderBlock.id)
|
|
230
|
+
: senderBlock.username || authorName;
|
|
231
|
+
const authorIdHash = rawId ? (0, vault_1.hmacHash)('trigger.author_id', rawId) : undefined;
|
|
232
|
+
const promptHash = firstUserText ? (0, vault_1.hmacHash)('trigger.prompt', firstUserText) : undefined;
|
|
233
|
+
const ctx = {
|
|
234
|
+
trigger_type: 'user_message',
|
|
235
|
+
author_name: authorName,
|
|
236
|
+
author_channel: authorChannel,
|
|
237
|
+
author_hash: authorIdHash,
|
|
238
|
+
prompt_hash: promptHash,
|
|
239
|
+
session_label: sessionLabel,
|
|
240
|
+
conversation_depth: userMessageCount,
|
|
241
|
+
};
|
|
242
|
+
_attributionCache.set(sessionId, ctx);
|
|
243
|
+
return ctx;
|
|
244
|
+
}
|
|
245
|
+
if (userMessageCount > 0 && firstUserText) {
|
|
246
|
+
const promptHash = (0, vault_1.hmacHash)('trigger.prompt', firstUserText);
|
|
247
|
+
const ctx = {
|
|
248
|
+
trigger_type: 'user_message',
|
|
249
|
+
prompt_hash: promptHash,
|
|
250
|
+
session_label: sessionLabel,
|
|
251
|
+
conversation_depth: userMessageCount,
|
|
252
|
+
};
|
|
253
|
+
_attributionCache.set(sessionId, ctx);
|
|
254
|
+
return ctx;
|
|
255
|
+
}
|
|
256
|
+
return { trigger_type: 'unknown' };
|
|
257
|
+
}
|
package/dist/src/config.js
CHANGED
|
@@ -141,7 +141,7 @@ function loadConfig(overrides) {
|
|
|
141
141
|
collectHostMetrics: overrides?.collectHostMetrics ?? (process.env.COLLECT_HOST_METRICS === 'true'),
|
|
142
142
|
redactionEnabled: overrides?.redactionEnabled ?? (process.env.REDACTION_ENABLED !== 'false'),
|
|
143
143
|
localEventBuffer: process.env.SHIELD_LOCAL_EVENT_BUFFER !== 'false',
|
|
144
|
-
localEventLimit: safeParseInt(process.env.SHIELD_LOCAL_EVENT_LIMIT,
|
|
144
|
+
localEventLimit: safeParseInt(process.env.SHIELD_LOCAL_EVENT_LIMIT, 250),
|
|
145
145
|
credentials,
|
|
146
146
|
};
|
|
147
147
|
}
|
package/dist/src/event-store.js
CHANGED
|
@@ -42,7 +42,7 @@ exports._getConfigForTesting = _getConfigForTesting;
|
|
|
42
42
|
const fs_1 = require("fs");
|
|
43
43
|
const path_1 = require("path");
|
|
44
44
|
const log = __importStar(require("./log"));
|
|
45
|
-
const DEFAULT_MAX_EVENTS =
|
|
45
|
+
const DEFAULT_MAX_EVENTS = 250;
|
|
46
46
|
const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
47
47
|
let _config = null;
|
|
48
48
|
function initEventStore(dataDir, opts) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TriggerType as _TriggerType } from '../attributor';
|
|
2
|
+
export type TriggerType = _TriggerType;
|
|
1
3
|
export interface BaseEvent {
|
|
2
4
|
timestamp: string;
|
|
3
5
|
event_type: 'TOOL_CALL' | 'TOOL_RESULT';
|
|
@@ -14,6 +16,17 @@ export interface BaseEvent {
|
|
|
14
16
|
user: string;
|
|
15
17
|
};
|
|
16
18
|
tool_metadata?: Record<string, string | null>;
|
|
19
|
+
trigger?: {
|
|
20
|
+
type: TriggerType;
|
|
21
|
+
author_name?: string;
|
|
22
|
+
author_channel?: string;
|
|
23
|
+
author_hash?: string;
|
|
24
|
+
prompt_hash?: string;
|
|
25
|
+
session_label?: string;
|
|
26
|
+
parent_agent_id?: string;
|
|
27
|
+
cron_schedule?: string;
|
|
28
|
+
conversation_depth?: number;
|
|
29
|
+
};
|
|
17
30
|
}
|
|
18
31
|
export interface NetworkBlock {
|
|
19
32
|
network: {
|
|
@@ -34,6 +34,7 @@ function enrich(tool, ctx) {
|
|
|
34
34
|
const content = args.message || args.caption || '';
|
|
35
35
|
if (content) {
|
|
36
36
|
meta['openclaw.message_content'] = content.length > 500 ? content.slice(0, 500) : content;
|
|
37
|
+
meta['openclaw.message_length_bytes'] = String(Buffer.byteLength(String(content), 'utf8'));
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
if (args.buffer) {
|
package/dist/src/index.js
CHANGED
|
@@ -172,7 +172,7 @@ async function poll() {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
if (entries.length > 0) {
|
|
175
|
-
let envelopes = (0, transformer_1.transformEntries)(entries);
|
|
175
|
+
let envelopes = (0, transformer_1.transformEntries)(entries, config.sessionDirs);
|
|
176
176
|
const { valid: validEnvelopes, quarantined } = (0, validator_1.validate)(envelopes.map(e => e.event));
|
|
177
177
|
if (quarantined > 0) {
|
|
178
178
|
log.warn('bridge', `${quarantined} events quarantined (see ~/.openclaw/shield/data/quarantine.jsonl)`);
|
|
@@ -15,5 +15,5 @@ export declare function resolveAgentLabel(agentId: string): string;
|
|
|
15
15
|
export declare function getCachedPublicIp(): string | null;
|
|
16
16
|
export declare function isPrivateIp(ip: string): boolean;
|
|
17
17
|
export declare function resolveOutboundIp(): Promise<string | null>;
|
|
18
|
-
export declare function transformEntries(entries: RawEntry[]): EnvelopeEvent[];
|
|
18
|
+
export declare function transformEntries(entries: RawEntry[], sessionDirs?: string[]): EnvelopeEvent[];
|
|
19
19
|
export declare function generateHostTelemetry(): EnvelopeEvent | null;
|
package/dist/src/transformer.js
CHANGED
|
@@ -52,6 +52,7 @@ const log = __importStar(require("./log"));
|
|
|
52
52
|
const version_1 = require("./version");
|
|
53
53
|
const counters_1 = require("./counters");
|
|
54
54
|
const inventory_1 = require("./inventory");
|
|
55
|
+
const attributor_1 = require("./attributor");
|
|
55
56
|
let _cachedOpenClawVersion = "";
|
|
56
57
|
function normalizeSoftwareVersion(v) {
|
|
57
58
|
return v
|
|
@@ -273,9 +274,72 @@ function isAdministrativeEvent(toolName, args, sessionId) {
|
|
|
273
274
|
return true;
|
|
274
275
|
return false;
|
|
275
276
|
}
|
|
276
|
-
function
|
|
277
|
+
function findSessionDir(agentId, sessionDirs) {
|
|
278
|
+
const normalized = agentId.toLowerCase();
|
|
279
|
+
return sessionDirs.find(dir => {
|
|
280
|
+
const parts = dir.replace(/\\/g, '/').split('/');
|
|
281
|
+
return parts.length >= 2 &&
|
|
282
|
+
parts[parts.length - 1] === 'sessions' &&
|
|
283
|
+
parts[parts.length - 2].toLowerCase() === normalized;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function flattenTriggerToMetadata(event, ctx) {
|
|
287
|
+
if (ctx.trigger_type === 'unknown')
|
|
288
|
+
return;
|
|
289
|
+
if (!event.tool_metadata)
|
|
290
|
+
event.tool_metadata = {};
|
|
291
|
+
const m = event.tool_metadata;
|
|
292
|
+
m['trigger.type'] = ctx.trigger_type;
|
|
293
|
+
if (ctx.author_hash)
|
|
294
|
+
m['trigger.author_hash'] = ctx.author_hash;
|
|
295
|
+
if (ctx.prompt_hash)
|
|
296
|
+
m['trigger.prompt_hash'] = ctx.prompt_hash;
|
|
297
|
+
if (ctx.conversation_depth !== undefined)
|
|
298
|
+
m['trigger.conversation_depth'] = String(ctx.conversation_depth);
|
|
299
|
+
if (ctx.author_name)
|
|
300
|
+
m['trigger.author_name'] = ctx.author_name;
|
|
301
|
+
if (ctx.author_channel)
|
|
302
|
+
m['trigger.author_channel'] = ctx.author_channel;
|
|
303
|
+
if (ctx.session_label)
|
|
304
|
+
m['trigger.session_label'] = ctx.session_label;
|
|
305
|
+
if (ctx.parent_agent_id)
|
|
306
|
+
m['trigger.parent_agent_id'] = ctx.parent_agent_id;
|
|
307
|
+
if (ctx.cron_schedule)
|
|
308
|
+
m['trigger.cron_schedule'] = ctx.cron_schedule;
|
|
309
|
+
}
|
|
310
|
+
function buildTriggerField(ctx) {
|
|
311
|
+
if (ctx.trigger_type === 'unknown')
|
|
312
|
+
return undefined;
|
|
313
|
+
return {
|
|
314
|
+
type: ctx.trigger_type,
|
|
315
|
+
...(ctx.author_name !== undefined && { author_name: ctx.author_name }),
|
|
316
|
+
...(ctx.author_channel !== undefined && { author_channel: ctx.author_channel }),
|
|
317
|
+
...(ctx.author_hash !== undefined && { author_hash: ctx.author_hash }),
|
|
318
|
+
...(ctx.prompt_hash !== undefined && { prompt_hash: ctx.prompt_hash }),
|
|
319
|
+
...(ctx.session_label !== undefined && { session_label: ctx.session_label }),
|
|
320
|
+
...(ctx.parent_agent_id !== undefined && { parent_agent_id: ctx.parent_agent_id }),
|
|
321
|
+
...(ctx.cron_schedule !== undefined && { cron_schedule: ctx.cron_schedule }),
|
|
322
|
+
...(ctx.conversation_depth !== undefined && { conversation_depth: ctx.conversation_depth }),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function transformEntries(entries, sessionDirs) {
|
|
277
326
|
const baseSource = getSourceInfo();
|
|
278
327
|
const envelopes = [];
|
|
328
|
+
const attributionMap = new Map();
|
|
329
|
+
if (sessionDirs && sessionDirs.length > 0) {
|
|
330
|
+
const seen = new Set();
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
const key = `${entry._agentId}::${entry._sessionId}`;
|
|
333
|
+
if (seen.has(key))
|
|
334
|
+
continue;
|
|
335
|
+
seen.add(key);
|
|
336
|
+
const dir = findSessionDir(entry._agentId, sessionDirs);
|
|
337
|
+
if (dir) {
|
|
338
|
+
const ctx = (0, attributor_1.resolveAttribution)(entry._sessionId, entry._agentId, dir);
|
|
339
|
+
attributionMap.set(key, ctx);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
279
343
|
for (const entry of entries) {
|
|
280
344
|
const msg = entry.message;
|
|
281
345
|
const agentId = entry._agentId || baseSource.openclaw.agent_id;
|
|
@@ -311,6 +375,15 @@ function transformEntries(entries) {
|
|
|
311
375
|
event.tool_metadata['openclaw.cross_workspace_access'] = 'true';
|
|
312
376
|
event.tool_metadata['openclaw.target_workspace'] = targetWorkspace;
|
|
313
377
|
}
|
|
378
|
+
const attrKey = `${agentId}::${entry._sessionId}`;
|
|
379
|
+
const attrCtx = attributionMap.get(attrKey);
|
|
380
|
+
if (attrCtx) {
|
|
381
|
+
const triggerField = buildTriggerField(attrCtx);
|
|
382
|
+
if (triggerField) {
|
|
383
|
+
event.trigger = triggerField;
|
|
384
|
+
flattenTriggerToMetadata(event, attrCtx);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
314
387
|
log.debug('transformer', `TOOL_CALL tool=${toolName} session=${entry._sessionId} agent=${agentId} schema=${schema.constructor?.name || 'unknown'} admin=${event.tool_metadata?.['openclaw.is_administrative'] === 'true'}`, log.isDebug ? event : undefined);
|
|
315
388
|
(0, counters_1.recordEventType)(event.event_type);
|
|
316
389
|
envelopes.push({ source, event });
|
|
@@ -323,6 +396,15 @@ function transformEntries(entries) {
|
|
|
323
396
|
event.tool_metadata = {};
|
|
324
397
|
event.tool_metadata['openclaw.is_administrative'] = 'true';
|
|
325
398
|
}
|
|
399
|
+
const attrKeyR = `${agentId}::${entry._sessionId}`;
|
|
400
|
+
const attrCtxR = attributionMap.get(attrKeyR);
|
|
401
|
+
if (attrCtxR) {
|
|
402
|
+
const triggerFieldR = buildTriggerField(attrCtxR);
|
|
403
|
+
if (triggerFieldR) {
|
|
404
|
+
event.trigger = triggerFieldR;
|
|
405
|
+
flattenTriggerToMetadata(event, attrCtxR);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
326
408
|
log.debug('transformer', `TOOL_RESULT tool=${event.tool_name} session=${entry._sessionId} agent=${agentId}`, log.isDebug ? event : undefined);
|
|
327
409
|
(0, counters_1.recordEventType)(event.event_type);
|
|
328
410
|
envelopes.push({ source, event });
|
package/dist/src/validator.js
CHANGED
|
@@ -40,9 +40,31 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const os = __importStar(require("os"));
|
|
41
41
|
const events_1 = require("./events");
|
|
42
42
|
const base_1 = require("./events/base");
|
|
43
|
+
const attributor_1 = require("./attributor");
|
|
43
44
|
const log = __importStar(require("./log"));
|
|
44
45
|
const SHIELD_DATA_DIR = path.join(os.homedir(), '.openclaw', 'shield', 'data');
|
|
45
46
|
exports.QUARANTINE_FILE = path.join(SHIELD_DATA_DIR, 'quarantine.jsonl');
|
|
47
|
+
function validateTriggerField(event) {
|
|
48
|
+
const trigger = event.trigger;
|
|
49
|
+
if (!trigger)
|
|
50
|
+
return null;
|
|
51
|
+
if (!attributor_1.TRIGGER_TYPES.includes(trigger.type)) {
|
|
52
|
+
return `trigger.type '${trigger.type}' is not a valid TriggerType`;
|
|
53
|
+
}
|
|
54
|
+
if (trigger.author_hash !== undefined) {
|
|
55
|
+
if (typeof trigger.author_hash !== 'string' ||
|
|
56
|
+
!/^trigger\.author_id:[a-f0-9]{12}$/.test(trigger.author_hash)) {
|
|
57
|
+
return `trigger.author_hash has invalid format (expected trigger.author_id:[a-f0-9]{12})`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (trigger.prompt_hash !== undefined) {
|
|
61
|
+
if (typeof trigger.prompt_hash !== 'string' ||
|
|
62
|
+
!/^trigger\.prompt:[a-f0-9]{12}$/.test(trigger.prompt_hash)) {
|
|
63
|
+
return `trigger.prompt_hash has invalid format (expected trigger.prompt:[a-f0-9]{12})`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
46
68
|
function ensureDataDir() {
|
|
47
69
|
fs.mkdirSync(SHIELD_DATA_DIR, { recursive: true });
|
|
48
70
|
}
|
|
@@ -66,6 +88,18 @@ function validate(events) {
|
|
|
66
88
|
if (!schema) {
|
|
67
89
|
const baseResult = base_1.baseValidations.validate(event);
|
|
68
90
|
if (baseResult.valid) {
|
|
91
|
+
const triggerError = validateTriggerField(event);
|
|
92
|
+
if (triggerError) {
|
|
93
|
+
quarantined++;
|
|
94
|
+
quarantineBuffer.push(JSON.stringify({
|
|
95
|
+
quarantined_at: new Date().toISOString(),
|
|
96
|
+
validation_error: triggerError,
|
|
97
|
+
validation_field: 'trigger',
|
|
98
|
+
event,
|
|
99
|
+
}) + '\n');
|
|
100
|
+
log.warn('validator', `QUARANTINE tool=${event.tool_name} field=trigger error=${triggerError}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
69
103
|
valid.push(event);
|
|
70
104
|
}
|
|
71
105
|
else {
|
|
@@ -80,6 +114,20 @@ function validate(events) {
|
|
|
80
114
|
continue;
|
|
81
115
|
}
|
|
82
116
|
const result = (0, base_1.validateEvent)(event, schema);
|
|
117
|
+
if (result.valid) {
|
|
118
|
+
const triggerError = validateTriggerField(event);
|
|
119
|
+
if (triggerError) {
|
|
120
|
+
quarantined++;
|
|
121
|
+
quarantineBuffer.push(JSON.stringify({
|
|
122
|
+
quarantined_at: new Date().toISOString(),
|
|
123
|
+
validation_error: triggerError,
|
|
124
|
+
validation_field: 'trigger',
|
|
125
|
+
event,
|
|
126
|
+
}) + '\n');
|
|
127
|
+
log.warn('validator', `QUARANTINE tool=${event.tool_name} field=trigger error=${triggerError}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
83
131
|
if (result.valid) {
|
|
84
132
|
log.debug('validator', `PASS tool=${event.tool_name} category=${event.tool_category}`);
|
|
85
133
|
valid.push(event);
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shield",
|
|
3
3
|
"name": "OpenClaw Shield",
|
|
4
|
-
"description": "Real-time security monitoring
|
|
5
|
-
"version": "0.
|
|
4
|
+
"description": "Real-time security monitoring \u2014 streams enriched, redacted security events to the Shield detection platform.",
|
|
5
|
+
"version": "0.7.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"uiHints": {
|
|
55
55
|
"installationKey": {
|
|
56
56
|
"label": "Installation Key",
|
|
57
|
-
"description": "One-time key from your trial signup at https://www.upx.com/en/lp/openclaw-shield-upx
|
|
57
|
+
"description": "One-time key from your trial signup at https://www.upx.com/en/lp/openclaw-shield-upx \u2014 Required for first-time activation only. After activation, log in at https://uss.upx.com"
|
|
58
58
|
},
|
|
59
59
|
"enabled": {
|
|
60
60
|
"label": "Enable security monitoring"
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
},
|
|
79
79
|
"clawhub": {
|
|
80
80
|
"slug": "openclaw-shield-upx",
|
|
81
|
-
"skillVersion": "0.6.
|
|
81
|
+
"skillVersion": "0.6.6",
|
|
82
82
|
"note": "ClawHub auto-increments on publish. Update this after each clawhub submission."
|
|
83
83
|
}
|
|
84
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upx-us/shield",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Security monitoring plugin for OpenClaw agents — streams enriched security events to the Shield detection platform",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -58,7 +58,10 @@
|
|
|
58
58
|
"threat-detection",
|
|
59
59
|
"agent-protection",
|
|
60
60
|
"audit",
|
|
61
|
+
"protect",
|
|
61
62
|
"realtime-monitoring",
|
|
63
|
+
"event-monitoring",
|
|
64
|
+
"security-monitoring",
|
|
62
65
|
"dlp",
|
|
63
66
|
"anomaly-detection"
|
|
64
67
|
],
|
package/skills/shield/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: openclaw-shield-upx
|
|
3
|
-
description: "
|
|
3
|
+
description: "Security monitoring and threat detection for OpenClaw agents — protect your agent with real-time SIEM, detect threats, monitor agent activity, and audit events. Use when: user asks about security status, Shield health, event logs, redaction vault, setting up agent protection, enabling SIEM, detecting threats, monitoring agent activity, or auditing agent actions. NOT for: general OS hardening, firewall config, or network security unrelated to OpenClaw agents."
|
|
4
4
|
homepage: https://www.upx.com/en/lp/openclaw-shield-upx
|
|
5
5
|
source: https://www.npmjs.com/package/@upx-us/shield
|
|
6
6
|
license: "Proprietary — UPX Technologies, Inc. All rights reserved."
|
|
@@ -37,12 +37,40 @@ Shield requires the `@upx-us/shield` plugin and an active subscription.
|
|
|
37
37
|
|
|
38
38
|
## Plugin State Check
|
|
39
39
|
|
|
40
|
-
Before responding to
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
Before responding to ANY Shield-related request, run `openclaw shield status` and interpret the result:
|
|
41
|
+
|
|
42
|
+
### State A — Plugin not installed
|
|
43
|
+
(command not found, or exit code non-zero with plugin_not_found)
|
|
44
|
+
Respond with:
|
|
45
|
+
> Shield is not installed yet. Start your free 30-day trial at:
|
|
46
|
+
> https://www.upx.com/en/lp/openclaw-shield-upx
|
|
47
|
+
>
|
|
48
|
+
> Then install the plugin:
|
|
49
|
+
> ```
|
|
50
|
+
> openclaw plugins install @upx-us/shield
|
|
51
|
+
> openclaw plugins run shield setup
|
|
52
|
+
> ```
|
|
53
|
+
|
|
54
|
+
### State B — Plugin installed but no installationKey
|
|
55
|
+
(status contains installationKey: null or state: "unconfigured")
|
|
56
|
+
Respond with:
|
|
57
|
+
> Shield is installed but not activated. Sign up for a trial at:
|
|
58
|
+
> https://www.upx.com/en/lp/openclaw-shield-upx
|
|
59
|
+
>
|
|
60
|
+
> Once you have your installation key, run:
|
|
61
|
+
> ```
|
|
62
|
+
> openclaw plugins run shield setup
|
|
63
|
+
> ```
|
|
64
|
+
|
|
65
|
+
### State C — Key set but not yet activated
|
|
66
|
+
(status contains state: "pending" or state: "unregistered")
|
|
67
|
+
Respond with:
|
|
68
|
+
> Shield has an installation key but hasn't activated yet. This usually takes under a minute.
|
|
69
|
+
> If it has been more than 5 minutes, check your key at https://uss.upx.com or contact support.
|
|
70
|
+
|
|
71
|
+
### State D — Fully active
|
|
72
|
+
(status contains state: "connected" or connected: true)
|
|
73
|
+
Proceed normally. No onboarding message needed.
|
|
46
74
|
|
|
47
75
|
**Constraints**: Only use `openclaw shield` commands for detection. Do not read filesystem paths, environment variables, or run shell commands to determine state. Do not install or uninstall packages on behalf of the user.
|
|
48
76
|
|
|
@@ -50,6 +78,20 @@ Before responding to any Shield-related request, run `openclaw shield status` an
|
|
|
50
78
|
|
|
51
79
|
When a Shield case fires or the user asks about an alert: use `openclaw shield cases` to list open cases and `openclaw shield cases --id <id>` for full detail (timeline, matched events, playbook). Severity guidance: **CRITICAL/HIGH** → surface immediately and ask if they want to investigate; **MEDIUM** → present and offer a playbook walkthrough; **LOW/INFO** → mention without interrupting the current task. Always include: rule name, what it detects, when it fired, and the first recommended remediation step. Confirm with the user before resolving — never resolve autonomously.
|
|
52
80
|
|
|
81
|
+
## Case Investigation Workflow
|
|
82
|
+
|
|
83
|
+
When a Shield case fires, correlate three data sources to determine true positive vs. false positive:
|
|
84
|
+
|
|
85
|
+
**Step 1 — Case detail** (`openclaw shield cases show <CASE_ID>`): What triggered the rule. Note the case timestamp — it anchors the correlation window.
|
|
86
|
+
|
|
87
|
+
**Step 2 — Surrounding logs** (`openclaw shield logs --since 30m --type TOOL_CALL`): Look for events 5–15 minutes before and after the case timestamp. Reveals if the alert was isolated or part of a sequence.
|
|
88
|
+
|
|
89
|
+
**Step 3 — Vault context** (`openclaw shield vault show`): If the case involves redacted credentials, hostnames, or commands, the vault reveals hashed representations and redaction categories.
|
|
90
|
+
|
|
91
|
+
**Step 4 — Correlate and assess**: Case detail = *what* fired the rule; Logs = *context*; Vault = *what was actually accessed*. Present findings and ask whether to resolve, investigate further, or add to the allowlist.
|
|
92
|
+
|
|
93
|
+
Note: a future `openclaw shield investigate <CASE_ID>` helper command will automate this workflow.
|
|
94
|
+
|
|
53
95
|
## Threat & Protection Questions
|
|
54
96
|
|
|
55
97
|
When asked "is my agent secure?", "am I protected?", or "what's being detected?": run `openclaw shield status` (health, event rate, last sync) and `openclaw shield cases` (open cases by severity). Summarise: rules active, last event ingested, any open cases. No cases → "Shield is monitoring X rules across Y event categories." Open cases → list by severity. If asked what Shield covers: explain it monitors for suspicious patterns across secret handling, access behaviour, outbound activity, injection attempts, config changes, and behavioural anomalies — without disclosing specific rule names or logic.
|
|
@@ -104,6 +146,25 @@ RPC responses include a `display` field with pre-formatted text. When present, u
|
|
|
104
146
|
|
|
105
147
|
When discussing a case, offer action buttons (resolve, false positive, investigate) via the message tool so users can act with one tap.
|
|
106
148
|
|
|
149
|
+
## Uninstalling
|
|
150
|
+
|
|
151
|
+
To fully remove Shield:
|
|
152
|
+
|
|
153
|
+
1. Uninstall the plugin:
|
|
154
|
+
```
|
|
155
|
+
openclaw plugins uninstall shield
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
2. Optionally remove local Shield data:
|
|
159
|
+
```
|
|
160
|
+
rm -rf ~/.openclaw/shield/
|
|
161
|
+
```
|
|
162
|
+
Files removed include: `config.json`, `data/event-buffer.jsonl`, `data/redaction-vault.json`, `data/cursor.json`, `data/instance.json`, `logs/shield.log`, `logs/bridge.log`, `state/monitor.json`.
|
|
163
|
+
|
|
164
|
+
⚠️ Deleting `data/redaction-vault.json` removes the ability to reverse-lookup past redacted values. Check your data retention needs before deleting.
|
|
165
|
+
|
|
166
|
+
3. Deactivate your instance at [uss.upx.com](https://uss.upx.com) — local uninstall does not deactivate your platform subscription or instance.
|
|
167
|
+
|
|
107
168
|
## Notes
|
|
108
169
|
|
|
109
170
|
- Shield does not interfere with agent behavior or performance
|