openclaw-plugin-vt-sentinel 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +27 -0
- package/dist/index.js +159 -22
- package/dist/status-renderer.d.ts +3 -0
- package/dist/status-renderer.js +9 -0
- package/package.json +1 -1
- package/skills/vt-sentinel/SKILL.md +8 -0
package/dist/index.d.ts
CHANGED
|
@@ -39,11 +39,38 @@ interface PluginApi {
|
|
|
39
39
|
registerHook?: (events: string | string[], handler: (event: any) => Promise<any>, opts?: object) => void;
|
|
40
40
|
onToolResult?: (handler: (event: any) => Promise<any>) => void;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Read current plugin version from package.json.
|
|
44
|
+
*/
|
|
45
|
+
declare function getCurrentVersion(): string;
|
|
42
46
|
/**
|
|
43
47
|
* Simple semver comparison: returns true if `latest` is newer than `current`.
|
|
44
48
|
* Only handles x.y.z format (no pre-release tags).
|
|
45
49
|
*/
|
|
46
50
|
export declare function isNewerVersion(latest: string, current: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Fetch latest version string from npm registry. Returns null on error.
|
|
53
|
+
* Single source of truth — used by checkForUpdates() and vt_sentinel_update.
|
|
54
|
+
*/
|
|
55
|
+
declare function fetchLatestVersion(): Promise<string | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Get the OpenClaw state directory (respects OPENCLAW_STATE_DIR env var).
|
|
58
|
+
*/
|
|
59
|
+
declare function getStateDir(): string;
|
|
60
|
+
/**
|
|
61
|
+
* Generate update instructions or preview. Pure function — all inputs are arguments.
|
|
62
|
+
* Returns text for the agent/user.
|
|
63
|
+
*/
|
|
64
|
+
declare function generateUpdateCommands(opts: {
|
|
65
|
+
currentVersion: string;
|
|
66
|
+
latestVersion: string;
|
|
67
|
+
confirm: boolean;
|
|
68
|
+
stateDir: string;
|
|
69
|
+
}): string;
|
|
47
70
|
export declare function isSelfPath(filePath: string): boolean;
|
|
48
71
|
export default function vtSentinelPlugin(api: PluginApi): void;
|
|
72
|
+
export declare const _generateUpdateCommands: typeof generateUpdateCommands;
|
|
73
|
+
export declare const _fetchLatestVersion: typeof fetchLatestVersion;
|
|
74
|
+
export declare const _getCurrentVersion: typeof getCurrentVersion;
|
|
75
|
+
export declare const _getStateDir: typeof getStateDir;
|
|
49
76
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports._getStateDir = exports._getCurrentVersion = exports._fetchLatestVersion = exports._generateUpdateCommands = void 0;
|
|
39
40
|
exports.isNewerVersion = isNewerVersion;
|
|
40
41
|
exports.isSelfPath = isSelfPath;
|
|
41
42
|
exports.default = vtSentinelPlugin;
|
|
@@ -78,6 +79,17 @@ function textResponse(text) {
|
|
|
78
79
|
// --- Update Check ---
|
|
79
80
|
const PACKAGE_NAME = 'openclaw-plugin-vt-sentinel';
|
|
80
81
|
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
82
|
+
/**
|
|
83
|
+
* Read current plugin version from package.json.
|
|
84
|
+
*/
|
|
85
|
+
function getCurrentVersion() {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8')).version || '0.0.0';
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return '0.0.0';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
81
93
|
/**
|
|
82
94
|
* Simple semver comparison: returns true if `latest` is newer than `current`.
|
|
83
95
|
* Only handles x.y.z format (no pre-release tags).
|
|
@@ -94,21 +106,98 @@ function isNewerVersion(latest, current) {
|
|
|
94
106
|
return false;
|
|
95
107
|
}
|
|
96
108
|
/**
|
|
97
|
-
*
|
|
109
|
+
* Fetch latest version string from npm registry. Returns null on error.
|
|
110
|
+
* Single source of truth — used by checkForUpdates() and vt_sentinel_update.
|
|
98
111
|
*/
|
|
99
|
-
async function
|
|
112
|
+
async function fetchLatestVersion() {
|
|
100
113
|
try {
|
|
101
|
-
const pkgPath = path.resolve(__dirname, '..', 'package.json');
|
|
102
|
-
const currentVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
103
114
|
const resp = await axios_1.default.get(NPM_REGISTRY_URL, { timeout: 5000 });
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
return resp.data?.version || null;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the OpenClaw state directory (respects OPENCLAW_STATE_DIR env var).
|
|
123
|
+
*/
|
|
124
|
+
function getStateDir() {
|
|
125
|
+
return process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Generate update instructions or preview. Pure function — all inputs are arguments.
|
|
129
|
+
* Returns text for the agent/user.
|
|
130
|
+
*/
|
|
131
|
+
function generateUpdateCommands(opts) {
|
|
132
|
+
if (!isNewerVersion(opts.latestVersion, opts.currentVersion)) {
|
|
133
|
+
return `VT Sentinel v${opts.currentVersion} is already the latest version.`;
|
|
134
|
+
}
|
|
135
|
+
if (!opts.confirm) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
lines.push(`Update available: v${opts.currentVersion} → v${opts.latestVersion}`);
|
|
138
|
+
lines.push('');
|
|
139
|
+
lines.push('What will happen:');
|
|
140
|
+
lines.push(' - The plugin will be updated to the latest version');
|
|
141
|
+
lines.push(' - Your configuration, audit logs, and VTAI credentials are preserved');
|
|
142
|
+
lines.push(' - The gateway will need to be restarted');
|
|
143
|
+
lines.push('');
|
|
144
|
+
lines.push('Call vt_sentinel_update with confirm: true to get the upgrade commands.');
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
const stateDir = opts.stateDir;
|
|
148
|
+
const quotedExtDir = `"${path.join(stateDir, 'extensions', PACKAGE_NAME)}"`;
|
|
149
|
+
const configPath = path.join(stateDir, 'openclaw.json');
|
|
150
|
+
const lines = [];
|
|
151
|
+
lines.push(`Upgrade: v${opts.currentVersion} → v${opts.latestVersion}`);
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push('Run these commands in a separate terminal (stopping the gateway will end this chat session):');
|
|
154
|
+
lines.push('');
|
|
155
|
+
lines.push(' 1. openclaw gateway stop');
|
|
156
|
+
lines.push(` 2. openclaw plugins update ${PACKAGE_NAME}`);
|
|
157
|
+
lines.push(' 3. openclaw gateway start');
|
|
158
|
+
lines.push('');
|
|
159
|
+
lines.push('Your configuration, audit logs, and credentials are preserved.');
|
|
160
|
+
lines.push('After restart, use vt_sentinel_status to verify the new version.');
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push('---');
|
|
163
|
+
lines.push('If step 2 reports "already at X.Y.Z", the install spec may be version-pinned.');
|
|
164
|
+
lines.push('In that case, replace step 2 with:');
|
|
165
|
+
lines.push('');
|
|
166
|
+
lines.push(` 2a. Remove the extension directory:`);
|
|
167
|
+
lines.push(` rm -rf ${quotedExtDir} (Linux/macOS)`);
|
|
168
|
+
lines.push(` rmdir /s /q ${quotedExtDir.replace(/\//g, '\\\\')} (Windows)`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
lines.push(` 2b. Back up and clean the config entry:`);
|
|
171
|
+
// Generate a safe node -e script for config cleanup
|
|
172
|
+
const cleanupScript = `node -e "const fs=require('fs'),p='${configPath.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}';try{const b=fs.readFileSync(p,'utf8');fs.writeFileSync(p+'.bak',b);const c=JSON.parse(b);if(c.plugins){delete(c.plugins.entries||{})['${PACKAGE_NAME}'];delete(c.plugins.installs||{})['${PACKAGE_NAME}'];}fs.writeFileSync(p,JSON.stringify(c,null,2));console.log('Config cleaned (backup: '+p+'.bak)')}catch(e){console.error('Failed: '+e.message);process.exit(1)}"`;
|
|
173
|
+
lines.push(` ${cleanupScript}`);
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push(` 2c. Reinstall:`);
|
|
176
|
+
lines.push(` openclaw plugins install ${PACKAGE_NAME}`);
|
|
177
|
+
return lines.join('\n');
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Check npm registry for a newer version. Fire-and-forget, never throws.
|
|
181
|
+
*/
|
|
182
|
+
async function checkForUpdates(logger, callbacks) {
|
|
183
|
+
try {
|
|
184
|
+
const currentVersion = getCurrentVersion();
|
|
185
|
+
const latestVersion = await fetchLatestVersion();
|
|
186
|
+
if (!latestVersion) {
|
|
187
|
+
callbacks?.onError();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
106
191
|
logger.info(`[VT-Sentinel] Update available: ${currentVersion} → ${latestVersion}. ` +
|
|
107
|
-
`
|
|
192
|
+
`Use vt_sentinel_update to check upgrade instructions.`);
|
|
193
|
+
callbacks?.onNewer(latestVersion);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
callbacks?.onUpToDate();
|
|
108
197
|
}
|
|
109
198
|
}
|
|
110
199
|
catch {
|
|
111
|
-
|
|
200
|
+
callbacks?.onError();
|
|
112
201
|
}
|
|
113
202
|
}
|
|
114
203
|
// --- Self-exclusion: never scan/quarantine our own plugin files ---
|
|
@@ -138,6 +227,9 @@ function vtSentinelPlugin(api) {
|
|
|
138
227
|
let scanner = null;
|
|
139
228
|
/** Tracks root directories passed to chokidar. Never use getWatched() for diffs. */
|
|
140
229
|
const watchRoots = new Set();
|
|
230
|
+
// Update check state (closure-scoped, not module-level)
|
|
231
|
+
let latestKnownVersion = null;
|
|
232
|
+
let updateCheckFailed = false;
|
|
141
233
|
const getConfig = () => {
|
|
142
234
|
const entry = api.config?.plugins?.entries?.['openclaw-plugin-vt-sentinel'];
|
|
143
235
|
return entry?.config ?? null;
|
|
@@ -701,15 +793,6 @@ function vtSentinelPlugin(api) {
|
|
|
701
793
|
}
|
|
702
794
|
},
|
|
703
795
|
});
|
|
704
|
-
// --- Helper: get current plugin version ---
|
|
705
|
-
const getCurrentVersion = () => {
|
|
706
|
-
try {
|
|
707
|
-
return JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8')).version || '0.0.0';
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
710
|
-
return '0.0.0';
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
796
|
// --- Helper: apply config changes to scanner and watcher ---
|
|
714
797
|
const applyConfigChange = (diff, newConfig) => {
|
|
715
798
|
if (diff.scannerNeedsRebuild && scanner) {
|
|
@@ -782,6 +865,9 @@ function vtSentinelPlugin(api) {
|
|
|
782
865
|
blockedFileCount: blocklist.size,
|
|
783
866
|
runtimeOverrideCount: Object.keys(configManager.getRuntimeOverrides()).length,
|
|
784
867
|
presetName: eff.configPreset,
|
|
868
|
+
updateAvailable: latestKnownVersion != null && !updateCheckFailed,
|
|
869
|
+
latestVersion: latestKnownVersion || undefined,
|
|
870
|
+
updateCheckFailed,
|
|
785
871
|
}));
|
|
786
872
|
},
|
|
787
873
|
});
|
|
@@ -930,6 +1016,48 @@ function vtSentinelPlugin(api) {
|
|
|
930
1016
|
return textResponse((0, status_renderer_1.renderHelp)());
|
|
931
1017
|
},
|
|
932
1018
|
});
|
|
1019
|
+
// --- Tool: vt_sentinel_update ---
|
|
1020
|
+
api.registerTool({
|
|
1021
|
+
name: 'vt_sentinel_update',
|
|
1022
|
+
description: 'Check for VT Sentinel updates and get upgrade instructions. Call with confirm: true to generate the exact commands to run in a separate terminal.',
|
|
1023
|
+
parameters: {
|
|
1024
|
+
type: 'object',
|
|
1025
|
+
properties: {
|
|
1026
|
+
confirm: {
|
|
1027
|
+
type: 'boolean',
|
|
1028
|
+
description: 'Set true to generate upgrade commands (default: just checks for updates)',
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
required: [],
|
|
1032
|
+
},
|
|
1033
|
+
execute: async (_ctx, rawParams) => {
|
|
1034
|
+
const params = rawParams || {};
|
|
1035
|
+
// Strict validation: reject non-boolean confirm
|
|
1036
|
+
if ('confirm' in params && typeof params.confirm !== 'boolean') {
|
|
1037
|
+
return textResponse('Error: confirm must be true or false');
|
|
1038
|
+
}
|
|
1039
|
+
const latestVersion = await fetchLatestVersion();
|
|
1040
|
+
if (!latestVersion) {
|
|
1041
|
+
updateCheckFailed = true;
|
|
1042
|
+
return textResponse('Error: Could not reach npm registry. Check internet connectivity and try again.');
|
|
1043
|
+
}
|
|
1044
|
+
const currentVersion = getCurrentVersion();
|
|
1045
|
+
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
1046
|
+
latestKnownVersion = latestVersion;
|
|
1047
|
+
updateCheckFailed = false;
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
latestKnownVersion = null;
|
|
1051
|
+
updateCheckFailed = false;
|
|
1052
|
+
}
|
|
1053
|
+
return textResponse(generateUpdateCommands({
|
|
1054
|
+
currentVersion,
|
|
1055
|
+
latestVersion,
|
|
1056
|
+
confirm: params.confirm === true,
|
|
1057
|
+
stateDir: getStateDir(),
|
|
1058
|
+
}));
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
933
1061
|
// --- Hook: auto-scan tool results ---
|
|
934
1062
|
const handleToolResult = async (event) => {
|
|
935
1063
|
const s = await ensureScanner();
|
|
@@ -946,10 +1074,8 @@ function vtSentinelPlugin(api) {
|
|
|
946
1074
|
};
|
|
947
1075
|
if (!stateStore.isFirstRunShown(scope)) {
|
|
948
1076
|
try {
|
|
949
|
-
const pkgPath = path.resolve(__dirname, '..', 'package.json');
|
|
950
|
-
const version = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version || '0.0.0';
|
|
951
1077
|
const onboardingText = (0, status_renderer_1.renderOnboarding)({
|
|
952
|
-
version,
|
|
1078
|
+
version: getCurrentVersion(),
|
|
953
1079
|
apiMode: process.env.VIRUSTOTAL_API_KEY === 'vtai-active' ? 'vtai' : 'user_key',
|
|
954
1080
|
watchDirs: [...watchRoots],
|
|
955
1081
|
effectiveConfig: configManager.getEffective(),
|
|
@@ -957,6 +1083,7 @@ function vtSentinelPlugin(api) {
|
|
|
957
1083
|
'vt_scan_file', 'vt_check_hash', 'vt_upload_consent',
|
|
958
1084
|
'vt_sentinel_status', 'vt_sentinel_configure',
|
|
959
1085
|
'vt_sentinel_reset_policy', 'vt_sentinel_help',
|
|
1086
|
+
'vt_sentinel_update',
|
|
960
1087
|
],
|
|
961
1088
|
});
|
|
962
1089
|
const injected = injectOnboarding(event, onboardingText);
|
|
@@ -1171,9 +1298,13 @@ function vtSentinelPlugin(api) {
|
|
|
1171
1298
|
vtSentinelPlugin._computeAutoWatchDirs = computeAutoWatchDirs;
|
|
1172
1299
|
vtSentinelPlugin._handleWatcherFile = handleWatcherFile;
|
|
1173
1300
|
vtSentinelPlugin._enrichFromContext = enrichFromContext;
|
|
1174
|
-
api.logger.info('[VT-Sentinel] Plugin loaded —
|
|
1301
|
+
api.logger.info('[VT-Sentinel] Plugin loaded — 8 tools + active protection hooks registered (VTAI auto-registration enabled)');
|
|
1175
1302
|
// Non-blocking update check (fire-and-forget)
|
|
1176
|
-
checkForUpdates(api.logger
|
|
1303
|
+
checkForUpdates(api.logger, {
|
|
1304
|
+
onNewer: (v) => { latestKnownVersion = v; updateCheckFailed = false; },
|
|
1305
|
+
onUpToDate: () => { latestKnownVersion = null; updateCheckFailed = false; },
|
|
1306
|
+
onError: () => { updateCheckFailed = true; },
|
|
1307
|
+
});
|
|
1177
1308
|
}
|
|
1178
1309
|
// --- Hook helpers ---
|
|
1179
1310
|
function extractResultText(event) {
|
|
@@ -1228,3 +1359,9 @@ function injectWarning(event, result) {
|
|
|
1228
1359
|
event.toolResult._vtSentinelWarning = warning;
|
|
1229
1360
|
}
|
|
1230
1361
|
}
|
|
1362
|
+
// --- Test exports ---
|
|
1363
|
+
// Exported for unit testing only. Not part of the public API.
|
|
1364
|
+
exports._generateUpdateCommands = generateUpdateCommands;
|
|
1365
|
+
exports._fetchLatestVersion = fetchLatestVersion;
|
|
1366
|
+
exports._getCurrentVersion = getCurrentVersion;
|
|
1367
|
+
exports._getStateDir = getStateDir;
|
|
@@ -14,6 +14,9 @@ export declare function renderStatus(opts: {
|
|
|
14
14
|
blockedFileCount: number;
|
|
15
15
|
runtimeOverrideCount: number;
|
|
16
16
|
presetName: string;
|
|
17
|
+
updateAvailable?: boolean;
|
|
18
|
+
latestVersion?: string;
|
|
19
|
+
updateCheckFailed?: boolean;
|
|
17
20
|
}): string;
|
|
18
21
|
export declare function renderPolicyMatrix(config: FullConfig): string;
|
|
19
22
|
export declare function renderHelp(): string;
|
package/dist/status-renderer.js
CHANGED
|
@@ -38,6 +38,12 @@ function renderStatus(opts) {
|
|
|
38
38
|
const lines = [];
|
|
39
39
|
const cfg = opts.effectiveConfig;
|
|
40
40
|
lines.push(`VT Sentinel v${opts.version} — Status`);
|
|
41
|
+
if (opts.updateAvailable && opts.latestVersion) {
|
|
42
|
+
lines.push(` Update available: v${opts.version} → v${opts.latestVersion} — use vt_sentinel_update for upgrade instructions`);
|
|
43
|
+
}
|
|
44
|
+
else if (opts.updateCheckFailed) {
|
|
45
|
+
lines.push(' Update check: last check failed (network error). Use vt_sentinel_update to retry.');
|
|
46
|
+
}
|
|
41
47
|
lines.push('');
|
|
42
48
|
// Config
|
|
43
49
|
lines.push('Effective Configuration:');
|
|
@@ -138,6 +144,9 @@ function renderHelp() {
|
|
|
138
144
|
lines.push(' vt_sentinel_help {}');
|
|
139
145
|
lines.push(' Show this guide');
|
|
140
146
|
lines.push('');
|
|
147
|
+
lines.push(' vt_sentinel_update { confirm: true }');
|
|
148
|
+
lines.push(' Check for updates and get upgrade instructions');
|
|
149
|
+
lines.push('');
|
|
141
150
|
lines.push('PRESETS:');
|
|
142
151
|
lines.push(' balanced (default)');
|
|
143
152
|
lines.push(' Ask before uploading sensitive files. Quarantine malicious. Log all scans.');
|
package/package.json
CHANGED
|
@@ -140,6 +140,14 @@ Shows usage examples, privacy explanation, and available presets.
|
|
|
140
140
|
vt_sentinel_help {}
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
### `vt_sentinel_update` — Check for Updates
|
|
144
|
+
Checks npm for a newer version and generates upgrade instructions.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
vt_sentinel_update {}
|
|
148
|
+
vt_sentinel_update { "confirm": true }
|
|
149
|
+
```
|
|
150
|
+
|
|
143
151
|
## Active Protection
|
|
144
152
|
|
|
145
153
|
VT Sentinel automatically protects the system in real-time:
|