alvin-bot 4.4.2 → 4.4.4

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 CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.4.4] — 2026-04-09
6
+
7
+ ### 🔐 Security / Data Layout
8
+
9
+ **`.env` now lives only in `DATA_DIR`** — The `ENV_FILE` path constant in `src/paths.ts` has been moved from `BOT_ROOT/.env` to `DATA_DIR/.env` (e.g. `~/.alvin-bot/.env`). This fixes a latent drift bug affecting 6 code sites in `web/server.ts`, `web/setup-api.ts`, `web/doctor-api.ts`, and `services/fallback-order.ts`: before this release, the Web UI's Settings tab, the setup wizard, the doctor repair flow, and the `/fallback` sync were all writing to `BOT_ROOT/.env`, while the bot's config loader in `src/config.ts` reads from `DATA_DIR/.env` first. Changes made through any of those tools were silently written to a file the bot never reads (for globally-installed users, `BOT_ROOT` is inside `node_modules/alvin-bot/` and gets wiped on `npm update -g`).
10
+
11
+ Why this also matters for security: keeping `.env` inside the code repo is defense-in-depth weak. `.gitignore` can be edited, editors create swap files (`.env.swp`, `.env~`), `git add -f` bypasses ignores, backup tools sync whole project folders, and screensharing shows project directories. Secrets belong physically outside the repo.
12
+
13
+ **Automatic migration for legacy installs** — `src/migrate.ts` now copies a legacy `BOT_ROOT/.env` to `DATA_DIR/.env` on first run (only if the destination doesn't exist) and enforces `0600` mode regardless of the source permissions. `hasLegacyData()` now recognizes a stray `BOT_ROOT/.env` as a migration trigger. No action is required from existing users — the bot migrates itself.
14
+
15
+ ### 📦 Compatibility
16
+
17
+ No breaking changes. Existing installs upgrade in place and are auto-migrated.
18
+
19
+ ```bash
20
+ npm update -g alvin-bot
21
+ ```
22
+
23
+ ## [4.4.3] — 2026-04-09
24
+
25
+ ### 🔐 Security
26
+ - **Sudo password storage** — Fixed a vulnerability where the sudo password was passed to `/usr/bin/security` as a command-line argument, making it briefly visible in `ps aux` output during keychain writes. Password is now piped via stdin using the documented `-w` prompt mode (must be the last option, and the password is supplied twice for the interactive prompt + confirmation). Byte-exact round-trip verified for arbitrary special characters.
27
+
28
+ ### 🛠 Providers
29
+ - **Gemini auto-registration narrowed** — The Google Gemini chat provider is no longer registered automatically just because `GOOGLE_API_KEY` is set. It is now registered only when `google` is referenced as the primary provider or in the fallback chain. The environment variable is still used for other Google-powered features (e.g. `/imagine` image generation) without forcing Gemini onto the chat provider list.
30
+
31
+ ### 🧰 Tooling
32
+ - `package-lock.json` now tracks `package.json` version correctly.
33
+
5
34
  ## [2.2.0] — 2026-02-24
6
35
 
7
36
  ### 🔐 Security
package/dist/migrate.js CHANGED
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import fs from "fs";
22
22
  import { resolve } from "path";
23
- import { BOT_ROOT, MEMORY_DIR, USERS_DIR, BACKUP_DIR, SOUL_FILE, TOOLS_MD, TOOLS_JSON, CRON_FILE, MCP_CONFIG, FALLBACK_FILE, CUSTOM_MODELS, WA_GROUPS, WHATSAPP_AUTH, WA_MEDIA_DIR, ACCESS_FILE, SUDO_ENC_FILE, SUDO_KEY_FILE, MEMORY_FILE, EMBEDDINGS_IDX } from "./paths.js";
23
+ import { BOT_ROOT, MEMORY_DIR, USERS_DIR, BACKUP_DIR, SOUL_FILE, TOOLS_MD, TOOLS_JSON, CRON_FILE, MCP_CONFIG, FALLBACK_FILE, CUSTOM_MODELS, WA_GROUPS, WHATSAPP_AUTH, WA_MEDIA_DIR, ACCESS_FILE, SUDO_ENC_FILE, SUDO_KEY_FILE, MEMORY_FILE, EMBEDDINGS_IDX, ENV_FILE } from "./paths.js";
24
24
  /**
25
25
  * Check if legacy data exists in the old locations.
26
26
  */
@@ -31,7 +31,13 @@ export function hasLegacyData() {
31
31
  resolve(BOT_ROOT, "docs", "users"),
32
32
  resolve(BOT_ROOT, "data", "access.json"),
33
33
  resolve(BOT_ROOT, "SOUL.md"),
34
- ];
34
+ // A BOT_ROOT/.env without a corresponding DATA_DIR/.env is a legacy layout
35
+ // — the loader prefers DATA_DIR, so keeping .env in BOT_ROOT silently
36
+ // breaks Settings/Setup/Doctor/fallback-order sync.
37
+ (fs.existsSync(resolve(BOT_ROOT, ".env")) && !fs.existsSync(ENV_FILE))
38
+ ? resolve(BOT_ROOT, ".env")
39
+ : "",
40
+ ].filter(Boolean);
35
41
  return legacyIndicators.some(p => fs.existsSync(p));
36
42
  }
37
43
  /**
@@ -47,6 +53,21 @@ function copyIfNew(src, dest) {
47
53
  }
48
54
  return false;
49
55
  }
56
+ /**
57
+ * Copy a file if source exists and destination doesn't, then enforce a specific file mode.
58
+ * Used for files containing secrets (e.g. .env) where 0600 must be guaranteed
59
+ * regardless of the source file's permissions or the process umask.
60
+ */
61
+ function copyIfNewWithMode(src, dest, mode) {
62
+ const copied = copyIfNew(src, dest);
63
+ if (copied) {
64
+ try {
65
+ fs.chmodSync(dest, mode);
66
+ }
67
+ catch { /* best effort */ }
68
+ }
69
+ return copied;
70
+ }
50
71
  /**
51
72
  * Recursively copy a directory if source exists and destination doesn't have the files.
52
73
  */
@@ -89,6 +110,8 @@ export function migrateFromLegacy() {
89
110
  skipped.push(label);
90
111
  }
91
112
  // ── Single files ─────────────────────────────────────────
113
+ // .env → .env (secrets — enforce 0600 mode regardless of source perms)
114
+ track(".env → .env", copyIfNewWithMode(resolve(BOT_ROOT, ".env"), ENV_FILE, 0o600));
92
115
  // SOUL.md → soul.md
93
116
  track("SOUL.md → soul.md", copyIfNew(resolve(BOT_ROOT, "SOUL.md"), SOUL_FILE));
94
117
  // TOOLS.md → tools.md
package/dist/paths.js CHANGED
@@ -23,13 +23,22 @@ export const PLUGINS_DIR = resolve(BOT_ROOT, "plugins");
23
23
  export const SKILLS_DIR = resolve(BOT_ROOT, "skills");
24
24
  /** User skills directory (custom, outside repo) */
25
25
  export const USER_SKILLS_DIR = resolve(DATA_DIR, "skills");
26
- /** .env — Environment config (stays in BOT_ROOT for dev, or DATA_DIR for packaged) */
27
- export const ENV_FILE = resolve(BOT_ROOT, ".env");
28
26
  /** Example/template files (always in repo) */
29
27
  export const SOUL_EXAMPLE = resolve(BOT_ROOT, "SOUL.example.md");
30
28
  export const TOOLS_EXAMPLE_MD = resolve(BOT_ROOT, "TOOLS.example.md");
31
29
  export const TOOLS_EXAMPLE_JSON = resolve(BOT_ROOT, "docs", "tools.example.json");
32
30
  // ── Data paths (DATA_DIR = ~/.alvin-bot) ───────────────────────────
31
+ /**
32
+ * .env — Environment config with secrets (BOT_TOKEN, API keys, etc.)
33
+ *
34
+ * Lives in DATA_DIR (outside the code repo) for three reasons:
35
+ * 1. Defense in depth against accidental commits — secrets never touch BOT_ROOT
36
+ * 2. Survives `npm update -g` (BOT_ROOT in global installs = node_modules, gets wiped)
37
+ * 3. Consistent with the loader priority in src/config.ts (DATA_DIR is Priority 1)
38
+ *
39
+ * Legacy installs with BOT_ROOT/.env are auto-migrated on first run (see src/migrate.ts).
40
+ */
41
+ export const ENV_FILE = resolve(DATA_DIR, ".env");
33
42
  /** memory/ — Daily logs and embeddings */
34
43
  export const MEMORY_DIR = resolve(DATA_DIR, "memory");
35
44
  /** memory/MEMORY.md — Long-term curated memory */
@@ -185,12 +185,13 @@ export function createRegistry(config) {
185
185
  model: "claude-opus-4-6",
186
186
  };
187
187
  }
188
- // Auto-register Google Gemini (single model) if key is available
189
- if (config.apiKeys?.google) {
188
+ // Register Google Gemini only if explicitly referenced as primary/fallback
189
+ // (GOOGLE_API_KEY is also used for image generation — doesn't mean Gemini should be a chat provider)
190
+ if (config.primary === "google" || config.fallbacks?.includes("google")) {
190
191
  providers["google"] = {
191
192
  ...PROVIDER_PRESETS["gemini-2.5-flash"],
192
193
  name: "Google Gemini",
193
- apiKey: config.apiKeys.google,
194
+ apiKey: config.apiKeys?.google,
194
195
  };
195
196
  }
196
197
  // Always try to detect local Ollama
@@ -11,7 +11,7 @@
11
11
  * - OS dialog handling (Accessibility, Full Disk Access, etc.)
12
12
  * - Status check (is sudo configured? does it work?)
13
13
  */
14
- import { execSync, spawn } from "child_process";
14
+ import { execSync, spawn, spawnSync } from "child_process";
15
15
  import os from "os";
16
16
  import fs from "fs";
17
17
  import crypto from "crypto";
@@ -34,7 +34,33 @@ export function storePassword(password) {
34
34
  execSync(`security delete-generic-password -a "${KEYCHAIN_ACCOUNT}" -s "${KEYCHAIN_SERVICE}" 2>/dev/null`, { stdio: "pipe" });
35
35
  }
36
36
  catch { /* didn't exist */ }
37
- execSync(`security add-generic-password -a "${KEYCHAIN_ACCOUNT}" -s "${KEYCHAIN_SERVICE}" -w "${password.replace(/"/g, '\\"')}" -U`, { stdio: "pipe", timeout: 5000 });
37
+ // Pipe password via stdin NEVER pass it as an argv element,
38
+ // or it leaks into `ps aux` during the lifetime of the child process.
39
+ //
40
+ // From `man security`:
41
+ // "Use of the -p or -w options is insecure. Specify -w as the last
42
+ // option to be prompted."
43
+ //
44
+ // Requirements for this to work:
45
+ // 1. `-w` must be the LAST argument (otherwise security consumes
46
+ // the next argv element as the password).
47
+ // 2. The password must be written to stdin TWICE: once for the
48
+ // prompt, once for the "retype password" confirmation.
49
+ const result = spawnSync("security", [
50
+ "add-generic-password",
51
+ "-a", KEYCHAIN_ACCOUNT,
52
+ "-s", KEYCHAIN_SERVICE,
53
+ "-U",
54
+ "-w", // MUST be last — triggers interactive prompt reading from stdin
55
+ ], {
56
+ input: password + "\n" + password + "\n", // prompt + confirm
57
+ stdio: ["pipe", "pipe", "pipe"],
58
+ timeout: 5000,
59
+ });
60
+ if (result.status !== 0) {
61
+ const stderr = result.stderr?.toString().trim() || `exit ${result.status}`;
62
+ throw new Error(stderr);
63
+ }
38
64
  return { ok: true, method: "macOS Keychain" };
39
65
  }
40
66
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "4.4.2",
3
+ "version": "4.4.4",
4
4
  "description": "Alvin Bot — Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",