From 947a6c1db5a0bf0e45d849593b2684158dac3462 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 16 Jun 2026 22:23:11 -0700 Subject: [PATCH] [eric] perf: draft opt-in defender exclusion helper for windows cold-start (#9 item 5) - scripts/add-defender-exclusion.ps1: dry-run default; -Apply/-Remove (admin); -Status - excludes %LOCALAPPDATA%/%APPDATA%/~ openswarm dirs; kills defender cold-scan entirely - security-sensitive: opt-in only, never silent; settings-toggle design documented, not auto-applied --- docs/perf/winv2/README.md | 2 +- scripts/add-defender-exclusion.ps1 | 73 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 scripts/add-defender-exclusion.ps1 diff --git a/docs/perf/winv2/README.md b/docs/perf/winv2/README.md index 3de7d94c..38b39663 100644 --- a/docs/perf/winv2/README.md +++ b/docs/perf/winv2/README.md @@ -163,7 +163,7 @@ validated on a real packaged EXE (Task #10). 2. [DONE] Ship webapp_template node_modules PRE-EXTRACTED in resources + junction to it (kills the 14.2 s extract -> ~0 s). build-app-win.ps1 step 4b robocopies the tree into resources; runtime _bundled_extracted_modules()/_ensure_warm_cache() prefer it; tests in test_bundled_extracted_modules.py. Mac still ships the .tar.gz (unchanged). 3. [DRAFTED, build-gated] Ship site-packages as sourceless .pyc only (drop .py). Draft: scripts/strip-py-to-pyc.ps1 (dry-run default; NOT wired into the build). Measured: 3,352 .py (26.9 MB) + 362 __pycache__ dirs strippable from site-packages (keep-list excludes pip/setuptools). compileall -b writes legacy module.pyc next to source; we delete the .py whose .pyc exists and drop __pycache__. Sourceless import proven with the bundled 3.13 interpreter. Scope: site-packages ONLY (NOT backend app code -- the swarm-debug debugger reads our own source for frame annotation). .pyc magic must match the shipped interpreter, so compile with the bundled python. Validate on a packaged EXE (Task #10); some packages use inspect.getsource and may need the keep-list. Combined with #1 + #2 this takes python-env from ~13,554 files toward ~9,300 (~31% fewer for Defender). 4. [APPLIED, build-gated] Trim app.asar. Inventory (docs/perf/winv2/inspect_asar.js) found the 607 MB asar is almost entirely DUPLICATION: python-env (408 MB, incl. a 242 MB bundled claude.exe) and build-staging (197 MB: node.exe 67 MB, uv.exe 65 MB, mcp-bundles, frontend) are packed into the asar AND already shipped UNPACKED in resources/ via extraResources. The runtime reads from resources/ (confirmed: "Starting backend: ...resources\python-env\python.exe"), never from inside the asar. Source maps were a red herring (0.4 MB). Fix: added a build.files exclusion in electron/package.json ("!python-env/**", "!build-staging/**") so those trees no longer pack into the asar -> ~607 MB -> ~2 MB (just main.js/preload/node_modules). Removes the entire 639 MB cold-read on first launch. Validate on a packaged EXE (Task #10): app still boots (python/node/router resolved from resources), asar size shrunk. -5. Opt-in Defender exclusion for install/data dirs, documented, never silent (needs admin/UAC; security-sensitive). Settings toggle only; do not auto-apply. +5. [DRAFTED, opt-in] Defender exclusion for OpenSwarm's dirs -- the nuclear cold-start fix (stops real-time scanning entirely, so it kills BOTH the 54-138s post-update launch and the ~14s extract). Draft: scripts/add-defender-exclusion.ps1 (dry-run by default; -Apply/-Remove need admin; -Status lists). Excludes %LOCALAPPDATA%\openswarm, %APPDATA%\openswarm, ~/.openswarm (verified the paths resolve). SECURITY: reduces AV coverage of those folders, so it must ALWAYS be an explicit user choice -- never auto-run, never a startup prompt. Proposed surface: an OFF-by-default Settings > Advanced toggle ("Faster Windows startup -- adds a Defender exclusion for OpenSwarm; one-time admin approval; reversible"), which on enable spawns an elevated `powershell Start-Process -Verb RunAs` to run the script -Apply (UAC), and -Remove on disable. This is a passive opt-in toggle, NOT a banner/tip/prompt, so it respects the no-user-action-UI rule. Not wired into the frontend yet (design only). Recommended order: #2 (biggest UX win, lowest risk), then #1 (largest cold win, careful import testing), then #3/#4. Validation: re-run profile_startup.sh + a fresh-extract timing on the packaged EXE after each change, diff vs baseline_startup.csv. diff --git a/scripts/add-defender-exclusion.ps1 b/scripts/add-defender-exclusion.ps1 new file mode 100644 index 00000000..4537c739 --- /dev/null +++ b/scripts/add-defender-exclusion.ps1 @@ -0,0 +1,73 @@ +<# +.SYNOPSIS + #9 item 5 (DRAFT, opt-in, NEVER silent): add a Windows Defender exclusion for + OpenSwarm's install + data dirs. This is the nuclear cold-start fix -- it stops + Defender real-time-scanning those folders entirely, which is the root of the + 54-138s post-update cold launch AND the ~14s first-app extract. + +.SECURITY + Excluding a folder from Defender reduces AV coverage of it. This must ALWAYS be + an explicit, informed user choice -- never auto-run, never a startup prompt. The + install is Azure code-signed, so the risk is bounded, but the user owns the + call. Fully reversible with -Remove. Requires admin (Add/Remove-MpPreference do). + +.USAGE + pwsh scripts\add-defender-exclusion.ps1 # show plan + paths, change nothing + pwsh scripts\add-defender-exclusion.ps1 -Status # list current openswarm exclusions + pwsh scripts\add-defender-exclusion.ps1 -Apply # add (run elevated) + pwsh scripts\add-defender-exclusion.ps1 -Remove # undo (run elevated) +#> +param( + [switch]$Apply, + [switch]$Remove, + [switch]$Status +) + +$ErrorActionPreference = 'Stop' + +# The three trees Defender rescans on launch / first-app: the Squirrel install +# (executables + python-env + node_modules), the Electron user data, and the +# warm caches. +$paths = @( + (Join-Path $env:LOCALAPPDATA 'openswarm'), + (Join-Path $env:APPDATA 'openswarm'), + (Join-Path $env:USERPROFILE '.openswarm') +) | Where-Object { $_ } + +function Test-Admin { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + (New-Object Security.Principal.WindowsPrincipal $id).IsInRole( + [Security.Principal.WindowsBuiltinRole]::Administrator) +} + +if ($Status) { + try { + $ex = (Get-MpPreference).ExclusionPath | Where-Object { $_ -match 'openswarm' } + if ($ex) { $ex | ForEach-Object { Write-Host " excluded: $_" } } else { Write-Host " (no openswarm Defender exclusions set)" } + } catch { + Write-Warning "Defender not queryable here (non-Defender AV, or needs elevation): $_" + } + return +} + +Write-Host "OpenSwarm Defender exclusion (OPT-IN). Would apply to:" +$paths | ForEach-Object { Write-Host " $_" } +Write-Host "" +Write-Host "SECURITY: this stops Windows Defender from real-time-scanning those folders." +Write-Host "Only do this if you trust this install (it is code-signed). Reversible with -Remove." + +if (-not ($Apply -or $Remove)) { + Write-Host "" + Write-Host "DRY RUN -- nothing changed. Re-run ELEVATED with -Apply (add), -Remove (undo), or -Status (list)." + return +} + +if (-not (Test-Admin)) { + throw "Needs admin. Re-run from an elevated PowerShell (Add/Remove-MpPreference require elevation)." +} + +foreach ($p in $paths) { + if ($Apply) { Add-MpPreference -ExclusionPath $p; Write-Host "added exclusion: $p" } + else { Remove-MpPreference -ExclusionPath $p; Write-Host "removed exclusion: $p" } +} +Write-Host "Done. Verify with -Status."