From 6370ce3d7e71934e691695048ca625539b6bbfac Mon Sep 17 00:00:00 2001 From: "Benjamin D. Smith" Date: Tue, 21 Jul 2026 15:48:16 +1000 Subject: [PATCH] fix(infrastructure): chmod 644 autopilot supervisor files on install (#2963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Summary: • launchd rejects group/world-writable agent plists — when the installer runs under a umask-0 parent shell, `writeFileSync(plistPath(), plist)` produces a 0666 plist that makes `launchctl load`/`bootstrap` fail with the opaque `Bootstrap failed: 5: Input/output error` and the login-time LaunchAgents scan skip the file silently • on an affected machine the daemon never registers while everything looks installed — the plist exists, launchd's disabled-table says enabled, and no log file is ever created 🔧 Technical Improvements: • `installLaunchd`: write plist with `{ mode: 0o644 }` AND `chmodSync(0o644)` — writeFileSync mode applies only on create, so a reinstall over an existing 0666 plist must normalize explicitly • `installSystemd`: same hardening on the unit file (systemd warns on world-writable units); symmetric with the launchd path • Restart-policy rewrite path (`generateSystemdUnit` rewrite of an existing unit): chmod is load-bearing here — the file always exists, so the write mode never applies • `chmodSync` added to the fs import 📊 Code Changes: 18 insertions, 4 deletions (net +14) 📦 Files Modified: • src/commands/autopilot.ts (minor updates) — mode + chmod on the three supervisor-file writers; comments carry the launchd failure signature so the next EIO hunt greps straight to it --- src/commands/autopilot.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/commands/autopilot.ts b/src/commands/autopilot.ts index 8fd753123..82979fb12 100644 --- a/src/commands/autopilot.ts +++ b/src/commands/autopilot.ts @@ -17,7 +17,7 @@ * gbrain autopilot --status [--json] */ -import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, utimesSync, unlinkSync } from 'fs'; +import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, utimesSync, unlinkSync, chmodSync } from 'fs'; import { setCliExitVerdict } from '../core/cli-force-exit.ts'; import { join } from 'path'; import { execSync } from 'child_process'; @@ -1263,7 +1263,14 @@ function installLaunchd(wrapperPath: string, home: string, repoPath: string) { try { const agentsDir = join(home, 'Library', 'LaunchAgents'); mkdirSync(agentsDir, { recursive: true }); - writeFileSync(plistPath(), plist); + writeFileSync(plistPath(), plist, { mode: 0o644 }); + // launchd rejects group/world-writable agent plists: bootstrap/load fails + // with the opaque "Bootstrap failed: 5: Input/output error" and the login + // scan skips the file silently. writeFileSync's mode only applies on + // create — a reinstall over an existing plist keeps the old bits (a 0666 + // plist written under an umask-0 parent stays 0666 forever) — so + // normalize unconditionally. + chmodSync(plistPath(), 0o644); execSync(`launchctl load "${plistPath()}"`, { stdio: 'pipe' }); console.log('Installed launchd service: com.gbrain.autopilot'); console.log(` Repo: ${repoPath}`); @@ -1353,7 +1360,11 @@ export function migrateSystemdUnitToRestartAlways(): { rewritten: boolean; reaso return { rewritten: false, reason: 'hand-edited' }; } try { - writeFileSync(unitPath, generateSystemdUnit(execMatch![1])); + writeFileSync(unitPath, generateSystemdUnit(execMatch![1]), { mode: 0o644 }); + // This path always rewrites an EXISTING unit, so writeFileSync's mode + // never applies — chmod is the only thing that normalizes a unit born + // 0666 under a umask-0 parent (systemd warns on world-writable units). + chmodSync(unitPath, 0o644); try { execSync('systemctl --user daemon-reload', { stdio: 'pipe', timeout: 10_000 }); } catch { @@ -1370,7 +1381,10 @@ function installSystemd(wrapperPath: string, repoPath: string) { try { const unitPath = systemdUnitPath(); mkdirSync(join(process.env.HOME || '', '.config', 'systemd', 'user'), { recursive: true }); - writeFileSync(unitPath, unit); + writeFileSync(unitPath, unit, { mode: 0o644 }); + // Same umask-0 hardening as the launchd path (systemd warns on + // world-writable units); mode only applies on create, so normalize. + chmodSync(unitPath, 0o644); execSync('systemctl --user daemon-reload', { stdio: 'pipe', timeout: 10_000 }); execSync('systemctl --user enable --now gbrain-autopilot.service', { stdio: 'pipe', timeout: 15_000 }); console.log('Installed systemd user service: gbrain-autopilot.service');