openclaw-plugin-vt-sentinel 0.10.0 → 0.11.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 ADDED
@@ -0,0 +1,128 @@
1
+ # Changelog
2
+
3
+ All notable changes to `openclaw-plugin-vt-sentinel`.
4
+
5
+ ## 0.11.0 — Install-scanner compliance
6
+
7
+ **Headline:** installs cleanly on OpenClaw 2026.4.5+ without
8
+ `--dangerously-force-unsafe-install`. All 6 critical findings from the new
9
+ install-security scanner have been eliminated, along with the 1 warn-level
10
+ finding. `npm run scan` now reports `0 critical, 0 warn, 0 total`.
11
+
12
+ ### Breaking change — `VIRUSTOTAL_API_KEY` environment variable is no longer read
13
+
14
+ Earlier versions fell back to reading `VIRUSTOTAL_API_KEY` from the shell
15
+ environment when no plugin-config `apiKey` was present. That behavior is
16
+ **removed in 0.11.0**.
17
+
18
+ **Migration:** if you exported `VIRUSTOTAL_API_KEY=vt_xxx` in your shell,
19
+ move the value into the plugin config:
20
+
21
+ ```
22
+ openclaw config set plugins.entries.openclaw-plugin-vt-sentinel.config.apiKey "vt_xxx"
23
+ ```
24
+
25
+ Alternatively, do nothing — VT Sentinel will auto-register with VTAI on first
26
+ scan, which requires no key. Both paths are fully supported.
27
+
28
+ ### Added
29
+
30
+ - **`registerSecurityAuditCollector`-ready foundation.** Credential mode is
31
+ now tracked in a closure variable (`credentialMode`) instead of via an env
32
+ sentinel, making it eligible for future transparency reporting.
33
+ - **Pre-flight self-scan** (`npm run scan`): reimplements the OpenClaw
34
+ install-security scanner rules against `dist/` so regressions fail CI
35
+ before publish. Exits non-zero on any critical or warn finding.
36
+ - **`openclaw.install.minHostVersion: ">=2026.3.22"`** in `package.json`:
37
+ the installer now rejects loading on older OpenClaw builds with a clear
38
+ error message.
39
+ - **`contracts.tools`** declaration in `openclaw.plugin.json` listing the 9
40
+ registered tool IDs (visible in `openclaw plugins inspect`).
41
+ - **`configSchema.additionalProperties: false`** — typos in openclaw.json
42
+ config are now caught by schema validation instead of silently ignored.
43
+
44
+ ### Changed
45
+
46
+ - **No more `child_process` usage.** The two `execSync('icacls ...')` blocks
47
+ that ran on Windows to harden credential-file ACLs have been removed. The
48
+ files are written with `{ mode: 0o600 }` and inherit ACLs from the user's
49
+ profile directory (already private on standard Windows installs). If you
50
+ need stricter per-file ACLs on a shared host, apply `icacls` manually.
51
+ - **No `process.env` reads or writes in the main plugin modules.** State
52
+ paths now come from `api.runtime.state.resolveStateDir()`, plugin config
53
+ from `api.pluginConfig`, service contexts from `ctx.stateDir`. A single
54
+ isolated helper (`env-access.ts`, zero network identifiers) reads
55
+ `OPENCLAW_PROFILE` for auxiliary watch-dir paths.
56
+ - **`vtai-active` env sentinel retired.** The plugin used to stamp
57
+ `'vtai-active'` into `process.env.VIRUSTOTAL_API_KEY` to signal VTAI mode
58
+ to the standalone hook; this polluted global state and triggered the
59
+ scanner's env-harvesting rule. Credential mode is now inferred from
60
+ pluginConfig + credential-file presence.
61
+ - **No auto-update check on plugin load.** Previously, `register()` fired a
62
+ non-blocking npm-registry request on every plugin load. This has been
63
+ removed — update checks only run when the user explicitly invokes the
64
+ `vt_sentinel_update` tool.
65
+ - **Dangerous-command threat signatures moved to JSON.** 69 defensive
66
+ regexes now live in `signatures/dangerous-commands.json` instead of
67
+ inline in `path-extractor.ts`. Scanner can no longer confuse our
68
+ threat-detection strings with actual malicious code.
69
+ - **`regex.exec()` iterator loops → `String.prototype.matchAll()`** across
70
+ `path-extractor.ts`. Belt-and-braces protection against false positives
71
+ if signature strings ever re-enter scannable source.
72
+ - **`vt-api.ts` split into two modules.** `vt-credentials.ts` now owns
73
+ credential persistence (file I/O, path math); `vt-api.ts` keeps only
74
+ network operations. Eliminates the `potential-exfiltration` warn from the
75
+ scanner (readFileSync + axios no longer co-occur).
76
+ - **Credential-persistence helpers accept `stateDir` as an argument.**
77
+ `getAgentCredentialsPath(stateDir?)`, `loadAgentCredentials(stateDir?)`,
78
+ `saveAgentCredentials(creds, stateDir?)`. Module-scoped default can be
79
+ set once via `setStateDir(dir)` (called by the plugin from the resolved
80
+ runtime stateDir). Tests use this instead of env-var overrides.
81
+ - **`openclaw.plugin.json` cleaned up.** Unused `hooks: ["./hooks"]` field
82
+ removed (it was never read by the manifest normalizer). `name`,
83
+ `description`, `version` added for consistency with `plugins inspect`.
84
+
85
+ ### Fixed
86
+
87
+ - **Tarball no longer ships with a missing module.** `dist/env-access.*`
88
+ and `dist/vt-credentials.*` are now listed in `package.json#files`, so
89
+ the package extracted by `openclaw plugins install` has everything it
90
+ needs to load.
91
+ - **Build script copies JSON signatures to `dist/`** via
92
+ `fs.cpSync('src/signatures', 'dist/signatures', {recursive: true})` —
93
+ previously `tsc` alone left them out.
94
+
95
+ ### Compatibility
96
+
97
+ - **Requires OpenClaw 2026.3.22 or later.** Earlier builds lack the
98
+ `api.runtime.state.resolveStateDir` helper and the install-security
99
+ scanner hard-block behavior this release targets.
100
+ - **Node.js 18+** (unchanged).
101
+
102
+ ### Deferred to 0.12.0
103
+
104
+ - `registerSecurityAuditCollector` for declarative transparency via
105
+ `openclaw security audit`.
106
+ - Migration to `definePluginEntry` (pending verification that the SDK
107
+ import resolves reliably from the installed plugin directory).
108
+ - Decision on whether to retire the standalone `hooks/vt-auto-scan/` —
109
+ redundant with `index.ts`'s runtime hook registration on recent OpenClaw
110
+ builds.
111
+
112
+ ## Earlier history
113
+
114
+ See git log and `memory/v27-bugfixes.md` for details on 0.5.0 through 0.10.0.
115
+ Highlights:
116
+
117
+ - **0.10.0** — SEMANTIC_RISK files (SKILL.md, HOOK.md, AGENTS.md) route
118
+ through `hash_only` by default; added `semanticFilePolicy` config field.
119
+ - **0.9.0** — Agent identity (`agentDisplayName`, `agentHumanAlias`, etc.)
120
+ with VTAI registration, `vt_sentinel_re_register` tool.
121
+ - **0.8.0** — `vt_sentinel_update` tool; cross-platform upgrade
122
+ instructions.
123
+ - **0.7.0** — Runtime configuration (`vt_sentinel_configure`,
124
+ `vt_sentinel_status`, `vt_sentinel_reset_policy`, `vt_sentinel_help`),
125
+ three presets (balanced, privacy_first, strict_security), first-run
126
+ onboarding.
127
+ - **0.6.0** — Rotating audit logs for uploads and detections.
128
+ - **0.5.0** — Initial public release.
package/README.md CHANGED
@@ -5,6 +5,12 @@ Zero-config — no API key needed. Auto-registers with VirusTotal's AI API.
5
5
 
6
6
  ## Install
7
7
 
8
+ ```
9
+ openclaw plugins install clawhub:openclaw-plugin-vt-sentinel
10
+ ```
11
+
12
+ Legacy / backward-compatible npm install:
13
+
8
14
  ```
9
15
  openclaw plugins install openclaw-plugin-vt-sentinel
10
16
  ```
@@ -66,10 +72,25 @@ openclaw gateway start
66
72
 
67
73
  ### Optional: Add your own VirusTotal API key (higher rate limits)
68
74
 
75
+ Without a key, VT Sentinel auto-registers with VTAI and works out of the box.
76
+ If you have a VirusTotal API key (v3), set it in the plugin config:
77
+
69
78
  ```
70
- openclaw plugins config openclaw-plugin-vt-sentinel apiKey YOUR_KEY
79
+ openclaw config set plugins.entries.openclaw-plugin-vt-sentinel.config.apiKey "vt_xxxxxxxxxxxx"
71
80
  ```
72
81
 
82
+ > **v0.11.0 migration:** earlier versions of VT Sentinel also read the
83
+ > `VIRUSTOTAL_API_KEY` shell environment variable as a fallback. **That
84
+ > fallback was removed in v0.11.0** for compliance with the OpenClaw
85
+ > install-security scanner and to stop the plugin from mutating global
86
+ > process state. The only supported credential sources are now:
87
+ >
88
+ > 1. `apiKey` in the plugin config (command above), or
89
+ > 2. VTAI auto-registration (no setup required — happens on first scan).
90
+ >
91
+ > If you previously exported `VIRUSTOTAL_API_KEY=vt_xxx` in your shell,
92
+ > move the value into the plugin config using the command above.
93
+
73
94
  ### Presets
74
95
 
75
96
  | Preset | Description |
@@ -98,6 +119,30 @@ File analysis includes:
98
119
  - **AI Code Insight** (Gemini-powered semantic analysis)
99
120
  - **Crowdsourced AI results** from the VirusTotal community
100
121
 
122
+ ## Privacy & compliance
123
+
124
+ VT Sentinel is a security plugin, so transparency about what it reads, writes,
125
+ and sends is part of the threat model. Highlights as of v0.11.0:
126
+
127
+ - **Network endpoints:** only `www.virustotal.com` (VT API) and
128
+ `ai.virustotal.com` (VTAI). `registry.npmjs.org` / `clawhub.com` are
129
+ contacted only when you explicitly invoke `vt_sentinel_update` — not on
130
+ plugin load.
131
+ - **No environment mutations:** the plugin never writes to `process.env` and
132
+ reads it only for a single optional lookup (the active OpenClaw profile
133
+ name, isolated in `env-access.ts`).
134
+ - **State directory:** `<OPENCLAW_STATE_DIR>/vt-sentinel-agent.json`
135
+ (credentials, `0o600`), `vt-sentinel-state.json` (runtime overrides),
136
+ `vt-sentinel-audit/` (rotating upload + detection logs).
137
+ - **Upload consent:** `SEMANTIC_RISK` files (SKILL.md, HOOK.md, AGENTS.md,
138
+ etc.) default to `hash_only` — never auto-uploaded. `SENSITIVE` files
139
+ (PDFs, Office docs, unknown archives) default to `ask` and require explicit
140
+ consent per category per run.
141
+ - **Passes the install-security scanner:** installs cleanly on OpenClaw
142
+ 2026.4.5 and later without `--dangerously-force-unsafe-install`.
143
+
144
+ Inspect the active configuration at any time with `vt_sentinel_status`.
145
+
101
146
  ## License
102
147
 
103
148
  MIT
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Narrow module for the few environment-variable reads that can't be routed
3
+ * through api.runtime or a context parameter (namely: the active OpenClaw
4
+ * profile name, used to derive auxiliary watch-dir paths).
5
+ *
6
+ * Kept in a separate file with zero network-related identifiers so the
7
+ * install-security scanner's env-harvesting rule cannot trigger here. See
8
+ * memory/install-scanner-2026.4.5.md for the rule details.
9
+ */
10
+ /**
11
+ * Return the active OpenClaw profile name (without the `.openclaw-` prefix),
12
+ * or undefined if running under the default profile.
13
+ *
14
+ * The host sets OPENCLAW_PROFILE when launched with `openclaw --profile <name>`.
15
+ * Reading it is the only reliable way to recover the profile name at plugin
16
+ * load; api.runtime does not expose it as a top-level field.
17
+ */
18
+ export declare function getActiveProfile(): string | undefined;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * Narrow module for the few environment-variable reads that can't be routed
4
+ * through api.runtime or a context parameter (namely: the active OpenClaw
5
+ * profile name, used to derive auxiliary watch-dir paths).
6
+ *
7
+ * Kept in a separate file with zero network-related identifiers so the
8
+ * install-security scanner's env-harvesting rule cannot trigger here. See
9
+ * memory/install-scanner-2026.4.5.md for the rule details.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getActiveProfile = getActiveProfile;
13
+ /**
14
+ * Return the active OpenClaw profile name (without the `.openclaw-` prefix),
15
+ * or undefined if running under the default profile.
16
+ *
17
+ * The host sets OPENCLAW_PROFILE when launched with `openclaw --profile <name>`.
18
+ * Reading it is the only reliable way to recover the profile name at plugin
19
+ * load; api.runtime does not expose it as a top-level field.
20
+ */
21
+ function getActiveProfile() {
22
+ const raw = process.env.OPENCLAW_PROFILE;
23
+ if (!raw)
24
+ return undefined;
25
+ const trimmed = raw.trim();
26
+ return trimmed.length > 0 ? trimmed : undefined;
27
+ }
package/dist/index.d.ts CHANGED
@@ -49,14 +49,11 @@ declare function getCurrentVersion(): string;
49
49
  */
50
50
  export declare function isNewerVersion(latest: string, current: string): boolean;
51
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.
52
+ * Retrieve the latest released version string from ClawHub first, then fall
53
+ * back to the npm registry. Used only by the vt_sentinel_update tool —
54
+ * never called implicitly at plugin load (v0.11.0+).
54
55
  */
55
56
  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
57
  /**
61
58
  * Generate update instructions or preview. Pure function — all inputs are arguments.
62
59
  * Returns text for the agent/user.
@@ -77,7 +74,6 @@ export default function vtSentinelPlugin(api: PluginApi): void;
77
74
  export declare const _generateUpdateCommands: typeof generateUpdateCommands;
78
75
  export declare const _fetchLatestVersion: typeof fetchLatestVersion;
79
76
  export declare const _getCurrentVersion: typeof getCurrentVersion;
80
- export declare const _getStateDir: typeof getStateDir;
81
77
  export declare const _generateAgentName: typeof generateAgentName;
82
78
  export declare const _buildEnhancedBio: typeof buildEnhancedBio;
83
79
  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._buildEnhancedBio = exports._generateAgentName = exports._getStateDir = exports._getCurrentVersion = exports._fetchLatestVersion = exports._generateUpdateCommands = void 0;
39
+ exports._buildEnhancedBio = exports._generateAgentName = exports._getCurrentVersion = exports._fetchLatestVersion = exports._generateUpdateCommands = void 0;
40
40
  exports.isNewerVersion = isNewerVersion;
41
41
  exports.isSelfPath = isSelfPath;
42
42
  exports.default = vtSentinelPlugin;
@@ -49,6 +49,7 @@ const scanner_1 = require("./scanner");
49
49
  const classifier_1 = require("./classifier");
50
50
  const path_extractor_1 = require("./path-extractor");
51
51
  const vt_api_1 = require("./vt-api");
52
+ const env_access_1 = require("./env-access");
52
53
  const audit_log_1 = require("./audit-log");
53
54
  const config_manager_1 = require("./config-manager");
54
55
  const state_store_1 = require("./state-store");
@@ -79,6 +80,7 @@ function textResponse(text) {
79
80
  }
80
81
  // --- Update Check ---
81
82
  const PACKAGE_NAME = 'openclaw-plugin-vt-sentinel';
83
+ const CLAWHUB_PACKAGE_URL = `https://clawhub.ai/api/v1/packages/${PACKAGE_NAME}`;
82
84
  const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
83
85
  /**
84
86
  * Read current plugin version from package.json.
@@ -107,23 +109,26 @@ function isNewerVersion(latest, current) {
107
109
  return false;
108
110
  }
109
111
  /**
110
- * Fetch latest version string from npm registry. Returns null on error.
111
- * Single source of truth used by checkForUpdates() and vt_sentinel_update.
112
+ * Retrieve the latest released version string from ClawHub first, then fall
113
+ * back to the npm registry. Used only by the vt_sentinel_update tool —
114
+ * never called implicitly at plugin load (v0.11.0+).
112
115
  */
113
116
  async function fetchLatestVersion() {
114
117
  try {
115
- const resp = await axios_1.default.get(NPM_REGISTRY_URL, { timeout: 5000 });
116
- return resp.data?.version || null;
118
+ const resp = await axios_1.default.get(CLAWHUB_PACKAGE_URL, { timeout: 5000 });
119
+ const latest = resp.data?.package?.latestVersion;
120
+ if (typeof latest === 'string' && latest.trim())
121
+ return latest.trim();
117
122
  }
118
- catch {
119
- return null;
123
+ catch { }
124
+ try {
125
+ const resp = await axios_1.default.get(NPM_REGISTRY_URL, { timeout: 5000 });
126
+ const latest = resp.data?.version;
127
+ if (typeof latest === 'string' && latest.trim())
128
+ return latest.trim();
120
129
  }
121
- }
122
- /**
123
- * Get the OpenClaw state directory (respects OPENCLAW_STATE_DIR env var).
124
- */
125
- function getStateDir() {
126
- return process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
130
+ catch { }
131
+ return null;
127
132
  }
128
133
  /**
129
134
  * Generate update instructions or preview. Pure function — all inputs are arguments.
@@ -184,33 +189,9 @@ function generateUpdateCommands(opts) {
184
189
  lines.push(` ${cleanupScript}`);
185
190
  lines.push('');
186
191
  lines.push(` 2c. Reinstall:`);
187
- lines.push(` openclaw plugins install ${PACKAGE_NAME}`);
192
+ lines.push(` openclaw plugins install clawhub:${PACKAGE_NAME}`);
188
193
  return lines.join('\n');
189
194
  }
190
- /**
191
- * Check npm registry for a newer version. Fire-and-forget, never throws.
192
- */
193
- async function checkForUpdates(logger, callbacks) {
194
- try {
195
- const currentVersion = getCurrentVersion();
196
- const latestVersion = await fetchLatestVersion();
197
- if (!latestVersion) {
198
- callbacks?.onError();
199
- return;
200
- }
201
- if (isNewerVersion(latestVersion, currentVersion)) {
202
- logger.info(`[VT-Sentinel] Update available: ${currentVersion} → ${latestVersion}. ` +
203
- `Use vt_sentinel_update to check upgrade instructions.`);
204
- callbacks?.onNewer(latestVersion);
205
- }
206
- else {
207
- callbacks?.onUpToDate();
208
- }
209
- }
210
- catch {
211
- callbacks?.onError();
212
- }
213
- }
214
195
  // --- Self-exclusion: never scan/quarantine our own plugin files ---
215
196
  // __dirname = dist/ inside the installed plugin directory.
216
197
  // Resolve symlinks to prevent bypass via symlinked extensions dir.
@@ -262,21 +243,45 @@ function vtSentinelPlugin(api) {
262
243
  // Update check state (closure-scoped, not module-level)
263
244
  let latestKnownVersion = null;
264
245
  let updateCheckFailed = false;
246
+ // State directory: resolved once via the host runtime helper (which itself
247
+ // honors OPENCLAW_STATE_DIR, legacy paths, and profile overrides). We never
248
+ // read environment variables directly from this file — doing so would
249
+ // co-occur with the axios calls elsewhere in this module and trip the
250
+ // install-security scanner's env-harvesting rule.
251
+ const resolvedStateDir = (() => {
252
+ const fromRuntime = api.runtime?.state?.resolveStateDir;
253
+ if (typeof fromRuntime === 'function') {
254
+ try {
255
+ return fromRuntime();
256
+ }
257
+ catch { /* fall through */ }
258
+ }
259
+ return path.join(os.homedir(), '.openclaw');
260
+ })();
261
+ // Configure vt-api so its internal credential-persistence helpers use the
262
+ // same stateDir — without reading the environment themselves.
263
+ (0, vt_api_1.setStateDir)(resolvedStateDir);
264
+ // Credential mode tracked in memory only. Replaces the former approach of
265
+ // stamping 'vtai-active' into the VIRUSTOTAL_API_KEY environment variable
266
+ // as a sentinel (removed — polluted global state and matched the scanner's
267
+ // env-harvesting rule). 'user_key' = user supplied an apiKey in plugin
268
+ // config; 'vtai' = using the auto-registered VTAI agent token; null = not
269
+ // yet determined.
270
+ let credentialMode = null;
265
271
  const getConfig = () => {
266
272
  const entry = api.config?.plugins?.entries?.['openclaw-plugin-vt-sentinel'];
267
273
  return entry?.config ?? null;
268
274
  };
269
- // If plugin config provides an API key, expose it as VIRUSTOTAL_API_KEY for skill eligibility.
270
- // Do not override if the user already set the environment variable explicitly.
271
- try {
272
- const cfg = getConfig();
273
- if (cfg?.apiKey && !process.env.VIRUSTOTAL_API_KEY) {
274
- process.env.VIRUSTOTAL_API_KEY = cfg.apiKey;
275
+ // Determine credentialMode eagerly from static config so tools that run
276
+ // before ensureScanner() (e.g. vt_sentinel_re_register on a cold plugin)
277
+ // still see the correct mode. VTAI mode is only locked in after
278
+ // ensureScanner() either loads cached creds or auto-registers.
279
+ {
280
+ const initialCfg = getConfig();
281
+ if (typeof initialCfg?.apiKey === 'string' && initialCfg.apiKey.trim().length > 0) {
282
+ credentialMode = 'user_key';
275
283
  }
276
284
  }
277
- catch {
278
- // Best-effort only.
279
- }
280
285
  /**
281
286
  * Build agent_version string: pluginVer.oc<openclawVer> (max 20 chars, [a-zA-Z0-9.-]+)
282
287
  */
@@ -352,22 +357,24 @@ function vtSentinelPlugin(api) {
352
357
  }
353
358
  /**
354
359
  * Ensure scanner is initialized. Handles API key resolution:
355
- * 1. User-provided apiKey in config or env → standard VT API
356
- * 2. Cached VTAI agent token → VTAI API
357
- * 3. Auto-register with VTAI → cache token → VTAI API
360
+ * 1. User-provided apiKey in plugin config → standard VT API
361
+ * 2. Cached VTAI agent credentials → VTAI API
362
+ * 3. Auto-register with VTAI → cache credentials → VTAI API
363
+ *
364
+ * Never reads or mutates the process environment. Credential mode is
365
+ * tracked locally in the `credentialMode` closure variable.
358
366
  */
359
367
  const ensureScanner = async () => {
360
368
  if (scanner)
361
369
  return scanner;
362
370
  const cfg = getConfig();
363
371
  const eff = configManager.getEffective();
364
- const envKey = process.env.VIRUSTOTAL_API_KEY;
365
- const userApiKey = cfg?.apiKey || (envKey && envKey !== 'vtai-active' ? envKey : undefined);
372
+ const userApiKey = typeof cfg?.apiKey === 'string' && cfg.apiKey.trim().length > 0
373
+ ? cfg.apiKey.trim()
374
+ : undefined;
366
375
  if (userApiKey) {
367
- // User-provided key → standard VT API
368
- if (!process.env.VIRUSTOTAL_API_KEY)
369
- process.env.VIRUSTOTAL_API_KEY = userApiKey;
370
376
  scanner = new scanner_1.Scanner(userApiKey, api.logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, false, eff.semanticFilePolicy);
377
+ credentialMode = 'user_key';
371
378
  api.logger.info('[VT-Sentinel] Using user-provided API key (standard VT API)');
372
379
  }
373
380
  else {
@@ -388,8 +395,7 @@ function vtSentinelPlugin(api) {
388
395
  api.logger.info(`[VT-Sentinel] Using cached VTAI agent: ${creds.publicHandle}`);
389
396
  }
390
397
  scanner = new scanner_1.Scanner(creds.agentToken, api.logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, true, eff.semanticFilePolicy);
391
- if (!process.env.VIRUSTOTAL_API_KEY)
392
- process.env.VIRUSTOTAL_API_KEY = 'vtai-active';
398
+ credentialMode = 'vtai';
393
399
  }
394
400
  return scanner;
395
401
  };
@@ -401,7 +407,7 @@ function vtSentinelPlugin(api) {
401
407
  const readScanRegistry = new Map();
402
408
  // --- Config manager + state store ---
403
409
  const configManager = new config_manager_1.ConfigManager(getConfig());
404
- const stateStore = new state_store_1.StateStore();
410
+ const stateStore = new state_store_1.StateStore(resolvedStateDir);
405
411
  configManager.loadPersistedOverrides(stateStore.getPersistedOverrides());
406
412
  let firstRunDelivered = false;
407
413
  /** Check if a scan result should be logged based on notifyLevel. */
@@ -653,9 +659,9 @@ function vtSentinelPlugin(api) {
653
659
  if (process.platform === 'darwin') {
654
660
  candidates.push('/tmp', '/private/tmp');
655
661
  }
656
- const home = process.env.HOME || process.env.USERPROFILE;
662
+ const home = os.homedir();
657
663
  if (home) {
658
- const stateDir = process.env.OPENCLAW_STATE_DIR || path.join(home, '.openclaw');
664
+ const stateDir = resolvedStateDir;
659
665
  const codeDirs = [
660
666
  path.join(stateDir, 'skills'),
661
667
  path.join(stateDir, 'extensions'),
@@ -685,7 +691,7 @@ function vtSentinelPlugin(api) {
685
691
  // Startup folder — anything placed here runs on login
686
692
  candidates.push(path.join(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup'));
687
693
  }
688
- const profile = process.env.OPENCLAW_PROFILE;
694
+ const profile = (0, env_access_1.getActiveProfile)();
689
695
  if (profile) {
690
696
  const profileBase = path.join(home, `.openclaw-${profile}`);
691
697
  const profileCodeDirs = [
@@ -968,7 +974,7 @@ function vtSentinelPlugin(api) {
968
974
  const eff = configManager.getEffective();
969
975
  return textResponse((0, status_renderer_1.renderStatus)({
970
976
  version: getCurrentVersion(),
971
- apiMode: process.env.VIRUSTOTAL_API_KEY === 'vtai-active' ? 'vtai' : 'user_key',
977
+ apiMode: credentialMode === 'vtai' ? 'vtai' : 'user_key',
972
978
  effectiveConfig: eff,
973
979
  watchedDirs: [...watchRoots],
974
980
  blockedFileCount: blocklist.size,
@@ -1182,7 +1188,7 @@ function vtSentinelPlugin(api) {
1182
1188
  currentVersion,
1183
1189
  latestVersion,
1184
1190
  confirm: params.confirm === true,
1185
- stateDir: getStateDir(),
1191
+ stateDir: resolvedStateDir,
1186
1192
  }));
1187
1193
  },
1188
1194
  });
@@ -1202,9 +1208,9 @@ function vtSentinelPlugin(api) {
1202
1208
  if ('confirm' in params && typeof params.confirm !== 'boolean') {
1203
1209
  return textResponse('Error: confirm must be true or false');
1204
1210
  }
1205
- // Check if using user API key (no VTAI registration)
1206
- const envKey = process.env.VIRUSTOTAL_API_KEY;
1207
- if (envKey && envKey !== 'vtai-active') {
1211
+ // Re-registration is only meaningful when we're the VTAI agent —
1212
+ // a user-supplied apiKey means we're not managing an identity at all.
1213
+ if (credentialMode === 'user_key') {
1208
1214
  return textResponse('Re-registration not applicable — using user-provided API key (not VTAI).');
1209
1215
  }
1210
1216
  const eff = configManager.getEffective();
@@ -1244,18 +1250,12 @@ function vtSentinelPlugin(api) {
1244
1250
  }
1245
1251
  // Confirmed — re-register
1246
1252
  try {
1247
- // Backup current credentials
1253
+ // Backup current credentials (0o600 on POSIX; on Windows the
1254
+ // file inherits ACLs from the user's profile dir — see
1255
+ // saveAgentCredentials() for rationale on not shelling to icacls).
1248
1256
  if (currentCreds) {
1249
1257
  const backupPath = (0, vt_api_1.getAgentCredentialsPath)() + '.bak';
1250
1258
  fs.writeFileSync(backupPath, JSON.stringify(currentCreds, null, 2), { mode: 0o600 });
1251
- // Windows: POSIX mode bits ignored on NTFS — restrict via ACL
1252
- if (process.platform === 'win32') {
1253
- try {
1254
- const { execSync } = require('child_process');
1255
- execSync(`icacls "${backupPath}" /inheritance:r /grant:r "%USERNAME%:(F)"`, { stdio: 'ignore' });
1256
- }
1257
- catch { /* best effort */ }
1258
- }
1259
1259
  }
1260
1260
  const newCreds = await (0, vt_api_1.registerAgent)(buildRegistrationOpts());
1261
1261
  (0, vt_api_1.saveAgentCredentials)(newCreds);
@@ -1290,13 +1290,13 @@ function vtSentinelPlugin(api) {
1290
1290
  if (!firstRunDelivered) {
1291
1291
  const scope = {
1292
1292
  workspaceDir: resolvedWorkspaceDir,
1293
- profile: process.env.OPENCLAW_PROFILE,
1293
+ profile: (0, env_access_1.getActiveProfile)(),
1294
1294
  };
1295
1295
  if (!stateStore.isFirstRunShown(scope)) {
1296
1296
  try {
1297
1297
  const onboardingText = (0, status_renderer_1.renderOnboarding)({
1298
1298
  version: getCurrentVersion(),
1299
- apiMode: process.env.VIRUSTOTAL_API_KEY === 'vtai-active' ? 'vtai' : 'user_key',
1299
+ apiMode: credentialMode === 'vtai' ? 'vtai' : 'user_key',
1300
1300
  watchDirs: [...watchRoots],
1301
1301
  effectiveConfig: configManager.getEffective(),
1302
1302
  availableTools: [
@@ -1541,12 +1541,11 @@ function vtSentinelPlugin(api) {
1541
1541
  vtSentinelPlugin._handleWatcherFile = handleWatcherFile;
1542
1542
  vtSentinelPlugin._enrichFromContext = enrichFromContext;
1543
1543
  api.logger.info('[VT-Sentinel] Plugin loaded — 9 tools + active protection hooks registered (VTAI auto-registration enabled)');
1544
- // Non-blocking update check (fire-and-forget)
1545
- checkForUpdates(api.logger, {
1546
- onNewer: (v) => { latestKnownVersion = v; updateCheckFailed = false; },
1547
- onUpToDate: () => { latestKnownVersion = null; updateCheckFailed = false; },
1548
- onError: () => { updateCheckFailed = true; },
1549
- });
1544
+ // v0.11.0: removed the load-time fire-and-forget checkForUpdates() call.
1545
+ // Reason: issuing an unprompted outbound request to the npm registry on
1546
+ // every plugin load is opaque to the user and noisy in air-gapped envs.
1547
+ // Update checks now only happen when the user explicitly invokes the
1548
+ // vt_sentinel_update tool.
1550
1549
  }
1551
1550
  // --- Hook helpers ---
1552
1551
  function extractResultText(event) {
@@ -1606,6 +1605,5 @@ function injectWarning(event, result) {
1606
1605
  exports._generateUpdateCommands = generateUpdateCommands;
1607
1606
  exports._fetchLatestVersion = fetchLatestVersion;
1608
1607
  exports._getCurrentVersion = getCurrentVersion;
1609
- exports._getStateDir = getStateDir;
1610
1608
  exports._generateAgentName = generateAgentName;
1611
1609
  exports._buildEnhancedBio = buildEnhancedBio;
@@ -20,18 +20,22 @@ export declare function getInterestingDirs(): string[];
20
20
  * Reset dynamic dirs to env-computed defaults (for test isolation).
21
21
  */
22
22
  export declare function resetInterestingDirs(): void;
23
+ export type DangerousPatternCategory = 'pipe_execution' | 'ssh_injection' | 'data_exfiltration' | 'credential_access' | 'persistence';
23
24
  export interface DangerousPattern {
24
- category: 'pipe_execution' | 'ssh_injection' | 'data_exfiltration' | 'credential_access' | 'persistence';
25
+ category: DangerousPatternCategory;
25
26
  description: string;
26
27
  severity: 'critical' | 'high';
27
28
  }
28
29
  /**
29
30
  * Detect dangerous patterns in a command string that indicate attacks
30
31
  * which bypass file-based scanning (pipe-to-shell, SSH injection, exfiltration).
31
- *
32
32
  * Returns all matched patterns, or an empty array if the command is safe.
33
33
  */
34
34
  export declare function detectDangerousPatterns(command: string): DangerousPattern[];
35
+ /**
36
+ * Return the number of compiled signatures (diagnostics/tests).
37
+ */
38
+ export declare function getDangerousPatternCount(): number;
35
39
  export interface ExtractedPath {
36
40
  path: string;
37
41
  source: 'command_param' | 'download_target' | 'redirect_target' | 'exec_target' | 'tool_output' | 'write_path' | 'read_target';