openclaw-plugin-vt-sentinel 0.8.6 → 0.9.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.
@@ -2,6 +2,7 @@ import type { SensitiveFilePolicy } from './scanner';
2
2
  export type NotifyLevel = 'all' | 'threats_only' | 'silent';
3
3
  export type BlockMode = 'quarantine' | 'block_only' | 'log_only';
4
4
  export type PresetName = 'balanced' | 'privacy_first' | 'strict_security';
5
+ export type AgentMetadataMode = 'minimal' | 'enhanced';
5
6
  export interface FullConfig {
6
7
  apiKey?: string;
7
8
  watchDirs: string[];
@@ -14,6 +15,11 @@ export interface FullConfig {
14
15
  blockMode: BlockMode;
15
16
  showCleanScanLogs: boolean;
16
17
  configPreset: PresetName;
18
+ agentDisplayName?: string;
19
+ agentHumanAlias?: string;
20
+ agentBio?: string;
21
+ agentContactEmail?: string;
22
+ agentMetadataMode?: AgentMetadataMode;
17
23
  }
18
24
  export type ConfigOverrides = Partial<Omit<FullConfig, 'apiKey'>>;
19
25
  export interface ConfigDiff {
@@ -33,6 +39,11 @@ export interface StaticConfig {
33
39
  blockMode?: BlockMode;
34
40
  showCleanScanLogs?: boolean;
35
41
  configPreset?: PresetName;
42
+ agentDisplayName?: string;
43
+ agentHumanAlias?: string;
44
+ agentBio?: string;
45
+ agentContactEmail?: string;
46
+ agentMetadataMode?: AgentMetadataMode;
36
47
  }
37
48
  /**
38
49
  * Check if a resolved path is a dangerous filesystem root.
@@ -91,6 +91,10 @@ const VALID_NOTIFY_LEVELS = new Set(['all', 'threats_only', 'silent']);
91
91
  const VALID_BLOCK_MODES = new Set(['quarantine', 'block_only', 'log_only']);
92
92
  const VALID_PRESETS = new Set(['balanced', 'privacy_first', 'strict_security']);
93
93
  const VALID_SENSITIVE_POLICIES = new Set(['ask', 'ask_once', 'always_upload', 'hash_only']);
94
+ const VALID_METADATA_MODES = new Set(['minimal', 'enhanced']);
95
+ const DISPLAY_NAME_RE = /^[a-zA-Z0-9 _-]+$/;
96
+ const HUMAN_ALIAS_RE = /^[a-zA-Z0-9_-]+$/;
97
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
94
98
  /**
95
99
  * Check if a resolved path is a dangerous filesystem root.
96
100
  * Handles Unix / and any Windows drive letter (C:\, d:/, E:\, etc.) case-insensitively.
@@ -190,6 +194,68 @@ function validateOverrides(input) {
190
194
  errors.push(`excludeGlobs must be an array of strings`);
191
195
  }
192
196
  }
197
+ // --- Agent identity fields ---
198
+ // Empty string or null clears the field (sets to undefined in overrides).
199
+ if ('agentDisplayName' in input) {
200
+ const v = input.agentDisplayName;
201
+ if (v === '' || v === null) {
202
+ valid.agentDisplayName = undefined;
203
+ }
204
+ else if (typeof v === 'string' && v.length >= 1 && v.length <= 50 && DISPLAY_NAME_RE.test(v)) {
205
+ valid.agentDisplayName = v;
206
+ }
207
+ else {
208
+ errors.push('agentDisplayName must be 1-50 chars matching [a-zA-Z0-9 _-] (or empty to clear)');
209
+ }
210
+ }
211
+ if ('agentHumanAlias' in input) {
212
+ const v = input.agentHumanAlias;
213
+ if (v === '' || v === null) {
214
+ valid.agentHumanAlias = undefined;
215
+ }
216
+ else if (typeof v === 'string' && v.length >= 1 && v.length <= 50 && HUMAN_ALIAS_RE.test(v)) {
217
+ valid.agentHumanAlias = v;
218
+ }
219
+ else {
220
+ errors.push('agentHumanAlias must be 1-50 chars matching [a-zA-Z0-9_-] (or empty to clear)');
221
+ }
222
+ }
223
+ if ('agentBio' in input) {
224
+ const v = input.agentBio;
225
+ if (v === '' || v === null) {
226
+ valid.agentBio = undefined;
227
+ }
228
+ else if (typeof v === 'string' && v.length >= 1 && v.length <= 200) {
229
+ valid.agentBio = v;
230
+ }
231
+ else {
232
+ errors.push('agentBio must be 1-200 chars (or empty to clear)');
233
+ }
234
+ }
235
+ if ('agentContactEmail' in input) {
236
+ const v = input.agentContactEmail;
237
+ if (v === '' || v === null) {
238
+ valid.agentContactEmail = undefined;
239
+ }
240
+ else if (typeof v === 'string' && v.length <= 100 && EMAIL_RE.test(v)) {
241
+ valid.agentContactEmail = v;
242
+ }
243
+ else {
244
+ errors.push('agentContactEmail must be a valid email (or empty to clear)');
245
+ }
246
+ }
247
+ if ('agentMetadataMode' in input) {
248
+ const v = input.agentMetadataMode;
249
+ if (v === '' || v === null) {
250
+ valid.agentMetadataMode = undefined;
251
+ }
252
+ else if (VALID_METADATA_MODES.has(v)) {
253
+ valid.agentMetadataMode = v;
254
+ }
255
+ else {
256
+ errors.push('agentMetadataMode must be: minimal, enhanced (or empty to clear)');
257
+ }
258
+ }
193
259
  return { valid, errors };
194
260
  }
195
261
  // --- Glob matcher ---
@@ -249,6 +315,17 @@ class ConfigManager {
249
315
  result.blockMode = s.blockMode;
250
316
  if (s.showCleanScanLogs !== undefined)
251
317
  result.showCleanScanLogs = s.showCleanScanLogs;
318
+ // Identity
319
+ if (s.agentDisplayName !== undefined)
320
+ result.agentDisplayName = s.agentDisplayName;
321
+ if (s.agentHumanAlias !== undefined)
322
+ result.agentHumanAlias = s.agentHumanAlias;
323
+ if (s.agentBio !== undefined)
324
+ result.agentBio = s.agentBio;
325
+ if (s.agentContactEmail !== undefined)
326
+ result.agentContactEmail = s.agentContactEmail;
327
+ if (s.agentMetadataMode !== undefined)
328
+ result.agentMetadataMode = s.agentMetadataMode;
252
329
  }
253
330
  // Layer 4: Overlay runtime overrides
254
331
  const o = this.runtimeOverrides;
@@ -272,6 +349,17 @@ class ConfigManager {
272
349
  result.showCleanScanLogs = o.showCleanScanLogs;
273
350
  if (o.configPreset !== undefined)
274
351
  result.configPreset = o.configPreset;
352
+ // Identity
353
+ if (o.agentDisplayName !== undefined)
354
+ result.agentDisplayName = o.agentDisplayName;
355
+ if (o.agentHumanAlias !== undefined)
356
+ result.agentHumanAlias = o.agentHumanAlias;
357
+ if (o.agentBio !== undefined)
358
+ result.agentBio = o.agentBio;
359
+ if (o.agentContactEmail !== undefined)
360
+ result.agentContactEmail = o.agentContactEmail;
361
+ if (o.agentMetadataMode !== undefined)
362
+ result.agentMetadataMode = o.agentMetadataMode;
275
363
  this.effectiveCache = result;
276
364
  return result;
277
365
  }
package/dist/index.d.ts CHANGED
@@ -68,9 +68,16 @@ declare function generateUpdateCommands(opts: {
68
68
  stateDir: string;
69
69
  }): string;
70
70
  export declare function isSelfPath(filePath: string): boolean;
71
+ declare function generateAgentName(): string;
72
+ declare function buildEnhancedBio(eff: {
73
+ configPreset?: string;
74
+ autoScan?: boolean;
75
+ }): string;
71
76
  export default function vtSentinelPlugin(api: PluginApi): void;
72
77
  export declare const _generateUpdateCommands: typeof generateUpdateCommands;
73
78
  export declare const _fetchLatestVersion: typeof fetchLatestVersion;
74
79
  export declare const _getCurrentVersion: typeof getCurrentVersion;
75
80
  export declare const _getStateDir: typeof getStateDir;
81
+ export declare const _generateAgentName: typeof generateAgentName;
82
+ export declare const _buildEnhancedBio: typeof buildEnhancedBio;
76
83
  export {};
package/dist/index.js CHANGED
@@ -36,7 +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
+ exports._buildEnhancedBio = exports._generateAgentName = exports._getStateDir = exports._getCurrentVersion = exports._fetchLatestVersion = exports._generateUpdateCommands = void 0;
40
40
  exports.isNewerVersion = isNewerVersion;
41
41
  exports.isSelfPath = isSelfPath;
42
42
  exports.default = vtSentinelPlugin;
@@ -231,6 +231,27 @@ function isSelfPath(filePath) {
231
231
  }
232
232
  return normalized.startsWith(SELF_DIR + path.sep) || normalized === SELF_DIR;
233
233
  }
234
+ // --- Agent Name Generator ---
235
+ const ADJECTIVES = ['Swift', 'Silent', 'Sharp', 'Bright', 'Steady', 'Bold', 'Keen', 'Quick', 'Iron', 'Steel',
236
+ 'Brave', 'Rapid', 'True', 'Deep', 'Clear', 'Calm', 'Noble', 'Dark', 'Wise', 'Frost'];
237
+ const ANIMALS = ['Falcon', 'Wolf', 'Hawk', 'Fox', 'Lynx', 'Bear', 'Eagle', 'Otter', 'Raven', 'Tiger',
238
+ 'Shark', 'Viper', 'Puma', 'Crane', 'Owl', 'Cobra', 'Stag', 'Mantis', 'Badger', 'Drake'];
239
+ function generateAgentName() {
240
+ const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
241
+ const animal = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
242
+ const hex = Math.floor(Math.random() * 0xFFFF).toString(16).padStart(4, '0');
243
+ return `Sentinel-${adj}${animal}-${hex}`;
244
+ }
245
+ function buildEnhancedBio(eff) {
246
+ const osFamily = process.platform === 'darwin' ? 'macos'
247
+ : process.platform === 'win32' ? 'windows' : 'linux';
248
+ const parts = [
249
+ `OpenClaw VT Sentinel on ${osFamily}`,
250
+ `preset ${eff.configPreset || 'balanced'}`,
251
+ eff.autoScan !== false ? 'auto-scan on' : 'auto-scan off',
252
+ ];
253
+ return parts.join(', ').slice(0, 200);
254
+ }
234
255
  // --- Plugin Entry Point ---
235
256
  function vtSentinelPlugin(api) {
236
257
  let watcher = null;
@@ -255,6 +276,79 @@ function vtSentinelPlugin(api) {
255
276
  catch {
256
277
  // Best-effort only.
257
278
  }
279
+ /**
280
+ * Build agent_version string: pluginVer.oc<openclawVer> (max 20 chars, [a-zA-Z0-9.-]+)
281
+ */
282
+ function buildAgentVersion() {
283
+ const pluginVer = getCurrentVersion();
284
+ try {
285
+ const meta = api.config?.meta;
286
+ const ocVer = meta?.version || meta?.lastTouchedVersion || '';
287
+ const sanitized = String(ocVer).replace(/[^a-zA-Z0-9.-]/g, '').slice(0, 10);
288
+ if (sanitized)
289
+ return `${pluginVer}.oc${sanitized}`.slice(0, 20);
290
+ }
291
+ catch { }
292
+ return pluginVer.slice(0, 20);
293
+ }
294
+ // VTAI API field constraints (used to sanitize static config values)
295
+ const VTAI_DISPLAY_NAME_RE = /^[a-zA-Z0-9 _-]+$/;
296
+ const VTAI_HUMAN_ALIAS_RE = /^[a-zA-Z0-9_-]+$/;
297
+ const VTAI_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
298
+ /**
299
+ * Build registration opts from effective config.
300
+ * Sanitizes identity fields against VTAI API constraints to prevent
301
+ * registration failures from invalid static config values.
302
+ */
303
+ function buildRegistrationOpts() {
304
+ const eff = configManager.getEffective();
305
+ // Resolve display name: config > persisted auto-name > generate new
306
+ let displayName = eff.agentDisplayName;
307
+ if (displayName) {
308
+ // Validate against VTAI constraints: 1-50 chars, [a-zA-Z0-9 _-]+
309
+ if (displayName.length > 50 || !VTAI_DISPLAY_NAME_RE.test(displayName)) {
310
+ api.logger.warn(`[VT-Sentinel] Invalid agentDisplayName in config: "${displayName.substring(0, 20)}..." — falling back to auto-generated name`);
311
+ displayName = undefined;
312
+ }
313
+ }
314
+ if (!displayName) {
315
+ let autoName = stateStore.getAutoAgentName();
316
+ if (!autoName) {
317
+ autoName = generateAgentName();
318
+ stateStore.setAutoAgentName(autoName);
319
+ }
320
+ displayName = autoName;
321
+ }
322
+ const regOpts = {
323
+ agentFamily: 'vt-sentinel',
324
+ agentVersion: buildAgentVersion(),
325
+ displayName,
326
+ };
327
+ // Validate humanAlias: 1-50 chars, [a-zA-Z0-9_-]+
328
+ if (eff.agentHumanAlias) {
329
+ if (eff.agentHumanAlias.length <= 50 && VTAI_HUMAN_ALIAS_RE.test(eff.agentHumanAlias)) {
330
+ regOpts.humanAlias = eff.agentHumanAlias;
331
+ }
332
+ else {
333
+ api.logger.warn(`[VT-Sentinel] Invalid agentHumanAlias in config — skipping`);
334
+ }
335
+ }
336
+ // Validate contactEmail: max 100 chars, email format
337
+ if (eff.agentContactEmail) {
338
+ if (eff.agentContactEmail.length <= 100 && VTAI_EMAIL_RE.test(eff.agentContactEmail)) {
339
+ regOpts.contactEmail = eff.agentContactEmail;
340
+ }
341
+ else {
342
+ api.logger.warn(`[VT-Sentinel] Invalid agentContactEmail in config — skipping`);
343
+ }
344
+ }
345
+ if (eff.agentMetadataMode === 'enhanced') {
346
+ const bio = eff.agentBio || buildEnhancedBio(eff);
347
+ // Validate: 1-200 chars
348
+ regOpts.defineYourSelf = bio.length <= 200 ? bio : bio.slice(0, 200);
349
+ }
350
+ return regOpts;
351
+ }
258
352
  /**
259
353
  * Ensure scanner is initialized. Handles API key resolution:
260
354
  * 1. User-provided apiKey in config or env → standard VT API
@@ -280,7 +374,7 @@ function vtSentinelPlugin(api) {
280
374
  let creds = (0, vt_api_1.loadAgentCredentials)();
281
375
  if (!creds) {
282
376
  try {
283
- creds = await (0, vt_api_1.registerAgent)();
377
+ creds = await (0, vt_api_1.registerAgent)(buildRegistrationOpts());
284
378
  (0, vt_api_1.saveAgentCredentials)(creds);
285
379
  api.logger.info(`[VT-Sentinel] Auto-registered agent: ${creds.publicHandle}`);
286
380
  }
@@ -878,6 +972,12 @@ function vtSentinelPlugin(api) {
878
972
  updateAvailable: latestKnownVersion != null && !updateCheckFailed,
879
973
  latestVersion: latestKnownVersion || undefined,
880
974
  updateCheckFailed,
975
+ agentIdentity: {
976
+ displayName: eff.agentDisplayName || stateStore.getAutoAgentName() || '(not set)',
977
+ publicHandle: (0, vt_api_1.loadAgentCredentials)()?.publicHandle,
978
+ metadataMode: eff.agentMetadataMode || 'minimal',
979
+ humanAlias: eff.agentHumanAlias,
980
+ },
881
981
  }));
882
982
  },
883
983
  });
@@ -900,6 +1000,11 @@ function vtSentinelPlugin(api) {
900
1000
  excludeGlobs: { type: 'array', items: { type: 'string' }, description: 'Glob patterns for files to skip (e.g., *.log)' },
901
1001
  blockMode: { type: 'string', enum: ['quarantine', 'block_only', 'log_only'], description: 'How to handle malicious files' },
902
1002
  showCleanScanLogs: { type: 'boolean', description: 'Log clean scan results' },
1003
+ agentDisplayName: { type: 'string', description: 'Display name for VTAI leaderboard (1-50 chars)' },
1004
+ agentHumanAlias: { type: 'string', description: 'Human alias (1-50 chars, no spaces)' },
1005
+ agentBio: { type: 'string', description: 'Agent description for VTAI (1-200 chars)' },
1006
+ agentContactEmail: { type: 'string', description: 'Contact email (optional, privacy-sensitive)' },
1007
+ agentMetadataMode: { type: 'string', enum: ['minimal', 'enhanced'], description: 'Metadata sent during registration: minimal=display_name only, enhanced=adds OS/preset info' },
903
1008
  persist: { type: 'string', enum: ['session', 'state'], description: 'session = until restart, state = saved to disk (default: state)' },
904
1009
  },
905
1010
  required: [],
@@ -967,7 +1072,13 @@ function vtSentinelPlugin(api) {
967
1072
  if (params.persist !== 'session') {
968
1073
  stateStore.persistOverrides(configManager.getRuntimeOverrides());
969
1074
  }
970
- return textResponse((0, status_renderer_1.renderConfigChangeResult)(diff, newConfig));
1075
+ let result = (0, status_renderer_1.renderConfigChangeResult)(diff, newConfig);
1076
+ // Hint about re-registration if identity fields changed
1077
+ const identityFields = ['agentDisplayName', 'agentHumanAlias', 'agentBio', 'agentContactEmail', 'agentMetadataMode'];
1078
+ if (diff.changedFields.some(f => identityFields.includes(f))) {
1079
+ result += '\n\nIdentity changes saved. To apply to VTAI leaderboard, use vt_sentinel_re_register { confirm: true }.';
1080
+ }
1081
+ return textResponse(result);
971
1082
  },
972
1083
  });
973
1084
  // --- Tool: vt_sentinel_reset_policy ---
@@ -1068,6 +1179,101 @@ function vtSentinelPlugin(api) {
1068
1179
  }));
1069
1180
  },
1070
1181
  });
1182
+ // --- Tool: vt_sentinel_re_register ---
1183
+ api.registerTool({
1184
+ name: 'vt_sentinel_re_register',
1185
+ description: 'Re-register agent with VTAI to apply identity changes. Creates a new agent identity (new public_handle). Use after changing agentDisplayName or other identity settings.',
1186
+ parameters: {
1187
+ type: 'object',
1188
+ properties: {
1189
+ confirm: { type: 'boolean', description: 'Must be true to proceed (generates new identity)' },
1190
+ },
1191
+ required: [],
1192
+ },
1193
+ execute: async (_ctx, rawParams) => {
1194
+ const params = (typeof rawParams === 'object' && rawParams !== null) ? rawParams : {};
1195
+ if ('confirm' in params && typeof params.confirm !== 'boolean') {
1196
+ return textResponse('Error: confirm must be true or false');
1197
+ }
1198
+ // Check if using user API key (no VTAI registration)
1199
+ const envKey = process.env.VIRUSTOTAL_API_KEY;
1200
+ if (envKey && envKey !== 'vtai-active') {
1201
+ return textResponse('Re-registration not applicable — using user-provided API key (not VTAI).');
1202
+ }
1203
+ const eff = configManager.getEffective();
1204
+ const currentCreds = (0, vt_api_1.loadAgentCredentials)();
1205
+ // Resolve display name for preview
1206
+ let displayName = eff.agentDisplayName;
1207
+ if (!displayName) {
1208
+ displayName = stateStore.getAutoAgentName() || '(will generate new name)';
1209
+ }
1210
+ if (!params.confirm) {
1211
+ // Preview mode
1212
+ const lines = ['Agent re-registration preview:'];
1213
+ lines.push('');
1214
+ if (currentCreds) {
1215
+ lines.push(` Current handle: ${currentCreds.publicHandle}`);
1216
+ lines.push(` Registered at: ${currentCreds.registeredAt}`);
1217
+ }
1218
+ else {
1219
+ lines.push(' No current registration found.');
1220
+ }
1221
+ lines.push('');
1222
+ lines.push(' New registration will use:');
1223
+ lines.push(` display_name: ${displayName}`);
1224
+ if (eff.agentHumanAlias)
1225
+ lines.push(` human_alias: ${eff.agentHumanAlias}`);
1226
+ if (eff.agentMetadataMode === 'enhanced') {
1227
+ lines.push(` define_your_self: ${eff.agentBio || buildEnhancedBio(eff)}`);
1228
+ }
1229
+ if (eff.agentContactEmail)
1230
+ lines.push(` contact_email: (set)`);
1231
+ lines.push('');
1232
+ lines.push('WARNING: This creates a NEW agent identity with a new public_handle.');
1233
+ lines.push('The old handle will remain on the leaderboard as a separate entry.');
1234
+ lines.push('');
1235
+ lines.push('Call with { confirm: true } to proceed.');
1236
+ return textResponse(lines.join('\n'));
1237
+ }
1238
+ // Confirmed — re-register
1239
+ try {
1240
+ // Backup current credentials
1241
+ if (currentCreds) {
1242
+ const backupPath = (0, vt_api_1.getAgentCredentialsPath)() + '.bak';
1243
+ fs.writeFileSync(backupPath, JSON.stringify(currentCreds, null, 2), { mode: 0o600 });
1244
+ // Windows: POSIX mode bits ignored on NTFS — restrict via ACL
1245
+ if (process.platform === 'win32') {
1246
+ try {
1247
+ const { execSync } = require('child_process');
1248
+ execSync(`icacls "${backupPath}" /inheritance:r /grant:r "%USERNAME%:(F)"`, { stdio: 'ignore' });
1249
+ }
1250
+ catch { /* best effort */ }
1251
+ }
1252
+ }
1253
+ const newCreds = await (0, vt_api_1.registerAgent)(buildRegistrationOpts());
1254
+ (0, vt_api_1.saveAgentCredentials)(newCreds);
1255
+ // Update scanner with new token
1256
+ if (scanner) {
1257
+ const scanEff = configManager.getEffective();
1258
+ scanner = new scanner_1.Scanner(newCreds.agentToken, api.logger, scanEff.maxFileSizeMb, scanEff.sensitiveFilePolicy, true);
1259
+ }
1260
+ const lines = ['Agent re-registered successfully:'];
1261
+ lines.push(` New handle: ${newCreds.publicHandle}`);
1262
+ lines.push(` Display name: ${buildRegistrationOpts().displayName || displayName}`);
1263
+ if (currentCreds) {
1264
+ lines.push(` Previous handle: ${currentCreds.publicHandle} (backup saved)`);
1265
+ }
1266
+ return textResponse(lines.join('\n'));
1267
+ }
1268
+ catch (err) {
1269
+ // Rollback on failure
1270
+ if (currentCreds) {
1271
+ (0, vt_api_1.saveAgentCredentials)(currentCreds);
1272
+ }
1273
+ return textResponse(`Re-registration failed: ${err.message}\nPrevious credentials restored.`);
1274
+ }
1275
+ },
1276
+ });
1071
1277
  // --- Hook: auto-scan tool results ---
1072
1278
  const handleToolResult = async (event) => {
1073
1279
  const s = await ensureScanner();
@@ -1093,7 +1299,7 @@ function vtSentinelPlugin(api) {
1093
1299
  'vt_scan_file', 'vt_check_hash', 'vt_upload_consent',
1094
1300
  'vt_sentinel_status', 'vt_sentinel_configure',
1095
1301
  'vt_sentinel_reset_policy', 'vt_sentinel_help',
1096
- 'vt_sentinel_update',
1302
+ 'vt_sentinel_update', 'vt_sentinel_re_register',
1097
1303
  ],
1098
1304
  });
1099
1305
  const injected = injectOnboarding(event, onboardingText);
@@ -1308,7 +1514,7 @@ function vtSentinelPlugin(api) {
1308
1514
  vtSentinelPlugin._computeAutoWatchDirs = computeAutoWatchDirs;
1309
1515
  vtSentinelPlugin._handleWatcherFile = handleWatcherFile;
1310
1516
  vtSentinelPlugin._enrichFromContext = enrichFromContext;
1311
- api.logger.info('[VT-Sentinel] Plugin loaded — 8 tools + active protection hooks registered (VTAI auto-registration enabled)');
1517
+ api.logger.info('[VT-Sentinel] Plugin loaded — 9 tools + active protection hooks registered (VTAI auto-registration enabled)');
1312
1518
  // Non-blocking update check (fire-and-forget)
1313
1519
  checkForUpdates(api.logger, {
1314
1520
  onNewer: (v) => { latestKnownVersion = v; updateCheckFailed = false; },
@@ -1375,3 +1581,5 @@ exports._generateUpdateCommands = generateUpdateCommands;
1375
1581
  exports._fetchLatestVersion = fetchLatestVersion;
1376
1582
  exports._getCurrentVersion = getCurrentVersion;
1377
1583
  exports._getStateDir = getStateDir;
1584
+ exports._generateAgentName = generateAgentName;
1585
+ exports._buildEnhancedBio = buildEnhancedBio;
@@ -1,8 +1,12 @@
1
1
  import type { ConfigOverrides } from './config-manager';
2
+ export interface AgentIdentityState {
3
+ autoAgentName?: string;
4
+ }
2
5
  export interface PersistedState {
3
- version: 1;
6
+ version: 2;
4
7
  firstRunShown: Record<string, boolean>;
5
8
  runtimeOverrides: ConfigOverrides;
9
+ agentIdentity?: AgentIdentityState;
6
10
  }
7
11
  export declare class StateStore {
8
12
  private state;
@@ -20,6 +24,9 @@ export declare class StateStore {
20
24
  getPersistedOverrides(): ConfigOverrides;
21
25
  persistOverrides(overrides: ConfigOverrides): void;
22
26
  clearPersistedOverrides(): void;
27
+ getAgentIdentity(): AgentIdentityState;
28
+ setAutoAgentName(name: string): void;
29
+ getAutoAgentName(): string | undefined;
23
30
  private scopeKey;
24
31
  private load;
25
32
  private save;
@@ -38,9 +38,10 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const os = __importStar(require("os"));
40
40
  const DEFAULT_STATE = {
41
- version: 1,
41
+ version: 2,
42
42
  firstRunShown: {},
43
43
  runtimeOverrides: {},
44
+ agentIdentity: {},
44
45
  };
45
46
  // --- StateStore ---
46
47
  class StateStore {
@@ -75,6 +76,19 @@ class StateStore {
75
76
  this.state.runtimeOverrides = {};
76
77
  this.save();
77
78
  }
79
+ // --- Agent Identity ---
80
+ getAgentIdentity() {
81
+ return { ...(this.state.agentIdentity || {}) };
82
+ }
83
+ setAutoAgentName(name) {
84
+ if (!this.state.agentIdentity)
85
+ this.state.agentIdentity = {};
86
+ this.state.agentIdentity.autoAgentName = name;
87
+ this.save();
88
+ }
89
+ getAutoAgentName() {
90
+ return this.state.agentIdentity?.autoAgentName;
91
+ }
78
92
  // --- Internals ---
79
93
  scopeKey(scope) {
80
94
  if (scope?.workspaceDir)
@@ -86,16 +100,17 @@ class StateStore {
86
100
  load() {
87
101
  try {
88
102
  const data = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
89
- if (data && data.version === 1) {
103
+ if (data && (data.version === 1 || data.version === 2)) {
90
104
  return {
91
- version: 1,
105
+ version: 2,
92
106
  firstRunShown: data.firstRunShown || {},
93
107
  runtimeOverrides: data.runtimeOverrides || {},
108
+ agentIdentity: data.agentIdentity || {},
94
109
  };
95
110
  }
96
111
  }
97
112
  catch { /* missing or corrupt — use defaults */ }
98
- return { ...DEFAULT_STATE, firstRunShown: {}, runtimeOverrides: {} };
113
+ return { ...DEFAULT_STATE, firstRunShown: {}, runtimeOverrides: {}, agentIdentity: {} };
99
114
  }
100
115
  save() {
101
116
  try {
@@ -17,6 +17,12 @@ export declare function renderStatus(opts: {
17
17
  updateAvailable?: boolean;
18
18
  latestVersion?: string;
19
19
  updateCheckFailed?: boolean;
20
+ agentIdentity?: {
21
+ displayName: string;
22
+ publicHandle?: string;
23
+ metadataMode: string;
24
+ humanAlias?: string;
25
+ };
20
26
  }): string;
21
27
  export declare function renderPolicyMatrix(config: FullConfig): string;
22
28
  export declare function renderHelp(): string;
@@ -45,6 +45,19 @@ function renderStatus(opts) {
45
45
  lines.push(' Update check: last check failed (network error). Use vt_sentinel_update to retry.');
46
46
  }
47
47
  lines.push('');
48
+ // Agent Identity
49
+ if (opts.agentIdentity) {
50
+ lines.push('Agent Identity:');
51
+ lines.push(` Display name: ${opts.agentIdentity.displayName}`);
52
+ if (opts.agentIdentity.publicHandle) {
53
+ lines.push(` Public handle: ${opts.agentIdentity.publicHandle}`);
54
+ }
55
+ lines.push(` Metadata mode: ${opts.agentIdentity.metadataMode}`);
56
+ if (opts.agentIdentity.humanAlias) {
57
+ lines.push(` Human alias: ${opts.agentIdentity.humanAlias}`);
58
+ }
59
+ lines.push('');
60
+ }
48
61
  // Config
49
62
  lines.push('Effective Configuration:');
50
63
  lines.push(` Preset: ${opts.presetName}`);
@@ -147,6 +160,12 @@ function renderHelp() {
147
160
  lines.push(' vt_sentinel_update { confirm: true }');
148
161
  lines.push(' Check for updates and get upgrade instructions');
149
162
  lines.push('');
163
+ lines.push(' vt_sentinel_configure { agentDisplayName: "MySecurityBot", agentMetadataMode: "enhanced" }');
164
+ lines.push(' Set agent display name and enable enhanced metadata');
165
+ lines.push('');
166
+ lines.push(' vt_sentinel_re_register { confirm: true }');
167
+ lines.push(' Re-register with VTAI to apply identity changes (creates new public_handle)');
168
+ lines.push('');
150
169
  lines.push('PRESETS:');
151
170
  lines.push(' balanced (default)');
152
171
  lines.push(' Ask before uploading sensitive files. Quarantine malicious. Log all scans.');
package/dist/vt-api.d.ts CHANGED
@@ -34,11 +34,23 @@ export interface AgentCredentials {
34
34
  export declare function getAgentCredentialsPath(): string;
35
35
  export declare function loadAgentCredentials(): AgentCredentials | null;
36
36
  export declare function saveAgentCredentials(creds: AgentCredentials): void;
37
+ /**
38
+ * Options for agent registration with VTAI.
39
+ * All fields optional — sensible defaults applied.
40
+ */
41
+ export interface RegisterAgentOpts {
42
+ agentFamily?: string;
43
+ agentVersion?: string;
44
+ displayName?: string;
45
+ humanAlias?: string;
46
+ defineYourSelf?: string;
47
+ contactEmail?: string;
48
+ }
37
49
  /**
38
50
  * Register a new agent with VirusTotal AI API.
39
51
  * No authentication required — zero-friction onboarding.
40
52
  */
41
- export declare function registerAgent(version?: string): Promise<AgentCredentials>;
53
+ export declare function registerAgent(opts?: RegisterAgentOpts): Promise<AgentCredentials>;
42
54
  export declare function calculateSHA256(filePath: string): Promise<string>;
43
55
  export declare class VTApiClient {
44
56
  private apiKey;
package/dist/vt-api.js CHANGED
@@ -86,12 +86,19 @@ function saveAgentCredentials(creds) {
86
86
  * Register a new agent with VirusTotal AI API.
87
87
  * No authentication required — zero-friction onboarding.
88
88
  */
89
- async function registerAgent(version = '0.2.0') {
90
- const resp = await axios_1.default.post(`${VTAI_API_URL}/agents/register`, {
91
- agent_family: 'vt-sentinel',
92
- agent_version: version,
93
- display_name: 'VT Sentinel',
94
- }, { timeout: HTTP_TIMEOUT_MS });
89
+ async function registerAgent(opts) {
90
+ const body = {
91
+ agent_family: opts?.agentFamily || 'vt-sentinel',
92
+ agent_version: opts?.agentVersion || '0.2.0',
93
+ display_name: opts?.displayName || 'VT Sentinel',
94
+ };
95
+ if (opts?.humanAlias)
96
+ body.human_alias = opts.humanAlias;
97
+ if (opts?.defineYourSelf)
98
+ body.define_your_self = opts.defineYourSelf;
99
+ if (opts?.contactEmail)
100
+ body.contact_email = opts.contactEmail;
101
+ const resp = await axios_1.default.post(`${VTAI_API_URL}/agents/register`, body, { timeout: HTTP_TIMEOUT_MS });
95
102
  return {
96
103
  agentId: resp.data.agent_id,
97
104
  agentToken: resp.data.agent_token,
@@ -62,6 +62,28 @@
62
62
  "enum": ["balanced", "privacy_first", "strict_security"],
63
63
  "default": "balanced",
64
64
  "description": "Configuration preset. Individual settings override the preset."
65
+ },
66
+ "agentDisplayName": {
67
+ "type": "string",
68
+ "description": "Display name for VTAI leaderboard (1-50 chars, [a-zA-Z0-9 _-]+)"
69
+ },
70
+ "agentHumanAlias": {
71
+ "type": "string",
72
+ "description": "Human alias for VTAI (1-50 chars, no spaces)"
73
+ },
74
+ "agentBio": {
75
+ "type": "string",
76
+ "description": "Agent description for VTAI define_your_self (1-200 chars)"
77
+ },
78
+ "agentContactEmail": {
79
+ "type": "string",
80
+ "description": "Contact email for VTAI (optional, privacy-sensitive)"
81
+ },
82
+ "agentMetadataMode": {
83
+ "type": "string",
84
+ "enum": ["minimal", "enhanced"],
85
+ "default": "minimal",
86
+ "description": "What metadata to send during registration. minimal = display_name only. enhanced = also sends OS family, preset, and auto-scan status (no personal data)."
65
87
  }
66
88
  },
67
89
  "required": []
@@ -77,6 +99,11 @@
77
99
  "excludeGlobs": { "label": "Exclude File Patterns" },
78
100
  "blockMode": { "label": "Block Mode" },
79
101
  "showCleanScanLogs": { "label": "Show Clean Scan Logs" },
80
- "configPreset": { "label": "Configuration Preset" }
102
+ "configPreset": { "label": "Configuration Preset" },
103
+ "agentDisplayName": { "label": "Agent Display Name" },
104
+ "agentHumanAlias": { "label": "Human Alias" },
105
+ "agentBio": { "label": "Agent Description" },
106
+ "agentContactEmail": { "label": "Contact Email", "sensitive": true },
107
+ "agentMetadataMode": { "label": "Metadata Mode" }
81
108
  }
82
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-plugin-vt-sentinel",
3
- "version": "0.8.6",
3
+ "version": "0.9.1",
4
4
  "description": "VirusTotal Sentinel for OpenClaw - Malware detection and AI-powered code analysis",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -148,6 +148,29 @@ vt_sentinel_update {}
148
148
  vt_sentinel_update { "confirm": true }
149
149
  ```
150
150
 
151
+ ### `vt_sentinel_re_register` — Re-register Agent Identity
152
+ Re-registers with VTAI using current identity settings. Creates a new `public_handle`.
153
+ Use after changing `agentDisplayName` or other identity settings via `vt_sentinel_configure`.
154
+
155
+ ```
156
+ vt_sentinel_re_register {}
157
+ vt_sentinel_re_register { "confirm": true }
158
+ ```
159
+
160
+ ## Agent Identity
161
+
162
+ Each VT Sentinel instance registers with VTAI and appears on the agent leaderboard.
163
+ By default, a unique name is auto-generated (e.g., `Sentinel-SwiftFalcon-a3f2`).
164
+
165
+ Configure identity via `vt_sentinel_configure`:
166
+ - `agentDisplayName`: Custom display name for the leaderboard
167
+ - `agentHumanAlias`: Human operator alias (no spaces)
168
+ - `agentBio`: Short description (maps to VTAI `define_your_self`)
169
+ - `agentContactEmail`: Optional contact email
170
+ - `agentMetadataMode`: `minimal` (default, display name only) or `enhanced` (adds OS, preset info — no personal data)
171
+
172
+ After changing identity settings, use `vt_sentinel_re_register { "confirm": true }` to apply.
173
+
151
174
  ## Active Protection
152
175
 
153
176
  VT Sentinel automatically protects the system in real-time: