Pilot on PVE VM 300 established strong counterfactuals: the IDENTICAL script +
the user's EXACT journey both succeed on a healthy Win10 -
CF1: clean (Remove-All + reboot) -> VL install -> office/ok
CF2: retail O365HomePremRetail -> script targeted-remove -> reboot -> VL install
-> office/ok
So a persistent [Failing PreReq=SXSMSI]/1603 with all script-checkable causes
clean (msiserver healthy, EventLog up, no DisableMSI, no stale MSI, disk OK, no
pending reboot) is machine-specific Windows servicing/Installer corruption below
DISM/SFC - not the script, ODT, retail->VL transition, or KMS.
Cover the case without looping:
- Repair-OfficePrereq now persists a marker (HKLM\SOFTWARE\kms-bootstrap
DeepRepairDone).
- On a 1603 install failure: first time -> offer the deep repair; if the deep
repair already ran and it STILL fails -> Show-InPlaceRepairHint (the only
reliable fix: in-place Windows repair-install, keeps files+apps) + emit
'sxsmsi-unrecoverable' telemetry.
When the Office VL install fails with setup.exe 1603 (C2R 'SXSMSI' prereq) AND no
reboot is pending AND the common causes are clean (verified via telemetry:
msiserver healthy, EventLog running, no DisableMSI policy, no stale InProgress MSI,
disk OK) AND a manual DISM/SFC + reboot did not help, the install subsystem itself
is wedged. New Repair-OfficePrereq (consent-gated; $env:KMS_DEEP_REPAIR=1 to
auto-consent) goes one level past DISM without uninstalling anything: re-registers
the Windows Installer engine (msiexec /unregister + /regserver) and resets the
servicing/update caches (SoftwareDistribution + catroot2), then prompts a restart
and re-run. Offered automatically from Reinstall-OfficeVL on a 1603 with no pending
reboot. ODT exit code now exposed via $script:OdtExitCode.
Telemetry ruled out the obvious SXSMSI causes (msiserver Manual/normal, no
pending reboot, 37 GB free, no stale InProgress MSI), yet 1603 persists on a
clean machine. Add the last two web-documented script-detectable causes to the
state snapshot: Windows Event Log service status, TrustedInstaller start-type,
and the DisableMSI group policy. Also auto-start EventLog if it's not running
(Office C2R install depends on it). If all clean, the remaining cause is
servicing-stack corruption -> DISM /RestoreHealth + sfc.
Post-restart telemetry: officePFRO cleared (real restart worked) but install still
fails [Failing PreReq=SXSMSI]/1603 with NO pending reboot - so 1603 is not a reboot
issue here.
- Stop claiming "reboot needed" on every 1603; only when a reboot is actually
pending (Test-OfficeRebootPending = CBS/WU or an Office file-rename).
- Fast-Startup-aware restart hint (use Restart, NOT Shut down) when reboot IS
pending; gate before the ~3 GB download (this is the user-visible restart notice).
- Capture Windows Installer state in diagnostics (msiserver status/starttype,
Installer\InProgress, free disk GB) to pinpoint the SXSMSI prereq failure.
- Defensively re-enable msiserver if Disabled (a common SXSMSI cause).
- Get-OdtLogTail also matches prereq/sxsmsi lines.
Users can't always paste logs, so capture install-relevant system state in
telemetry instead. New Get-OfficeState (ProductReleaseIds, Office root-folder
count, reboot-signal breakdown cbs/wu/officePFRO, ClickToRun service, ospp
presence) is sent (1) BEFORE the install (ships before the ~3 GB download, so a
failure is debuggable even if aborted) and (2) on ODT failure alongside a
tightened ODT error tail. Stop dumping the verbose C2R log to the screen.
Scrub COMPUTERNAME/USERNAME from all telemetry (a C2R log filename leaked the
machine name) and raise the detail cap 600->1800.
setup.exe exit 1603 right after removing the bundled consumer Office almost
always means the old install is pending a reboot. The script handed users a bare
1603; now it explains it. Changes:
- Test-RebootRequiredHard (CBS/WU only) GATES Reinstall-OfficeVL before the ~3 GB
download, so a pending reboot stops early with reboot+re-run guidance instead
of failing with 1603.
- Invoke-Odt failure path detects pending-reboot / 1603 and tells the user to
reboot + re-run; ships reboot status in telemetry.
- Get-OdtLogTail also searches %TEMP% (where the C2R client logs the real error)
and prefers error-bearing lines, so a failure no longer reports an empty log.
Invoke-Odt returned $true unconditionally after setup.exe, so a failed or
not-yet-finished Click-to-Run install surfaced only as a bare "ospp.vbs not
found after install". Root-cause fixes:
- <Logging> in the config XML -> a capture dir, read back on failure so the
real setup.exe exit code / error is reported (and sent as telemetry).
- setup.exe run with -PassThru; non-zero (not 0/3010) exit -> fail + log tail.
- Wait-OfficeInstalled polls on-disk state (ospp.vbs + ProductReleaseIds)
instead of trusting setup.exe's early return under Display Level=None.
- After removing incompatible consumer Office (e.g. O365HomePremRetail), a
pending reboot now stops the run with reboot+re-run guidance rather than
half-completing the VL install in the same session (idempotent on re-run).
When the Office install path runs and a non-VL Click-to-Run Office is present
(ProductReleaseIds not ending in 'Volume' = retail/M365), it can't coexist with
a VL install of the same suite. Now: detect it, show it in the consent prompt,
ODT /configure <Remove> only those products (VL products of other families
preserved), then proceed with the VL install. Refactored the ODT run into a
shared Invoke-Odt used by both install (<Add>) and uninstall (<Remove>).
Telemetry on the uninstall step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fire-and-forget telemetry so script failures are captured server-side (Loki via
the kms-diag collector). kms-bootstrap.ps1 + setup-kms.ps1 POST a small anonymous
JSON event at each outcome (action, ok/fail, error text + exit codes, EditionID/
build/locale, detected Office products; no hostname/user/keys). 3s timeout,
errors swallowed -- never affects activation. $env:KMS_NO_TELEMETRY=1 opts out;
$env:KMS_DIAG_URL overrides. Version baked at build via Dockerfile sed
(__KMS_VERSION__ -> SCRIPT_VERSION build-arg). FAQ updated to disclose it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Microsoft's download.microsoft.com ODT URL is build-numbered and rotates every
release; the hardcoded officedeploymenttool_19127-20198.exe now 404s, so the
"install latest VL Office" path failed right after the consent prompt. Serve a
known-good ODT bootstrapper from our own /scripts/odt-setup.exe (carved out of
Anubis already) and point Reinstall-OfficeVL at it. $env:KMS_ODT_URL overrides.
The bootstrapper self-updates the Office payload, so it rarely needs refreshing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The activate-windows/office wrappers set $env:KMS_AUTO to pre-select the
product, but Approve() treated any KMS_AUTO as "non-interactive" and skipped the
consequences prompt -- so on a machine needing the ODT/edition install it printed
the consequences then exited without asking. Gate the prompt on a real console
([Environment]::UserInteractive + not IsInputRedirected, guarded) instead of
KMS_AUTO. KMS_AUTO now only selects WHICH products to activate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two most common operations now have clean, key-free commands (no
$env:KMS_AUTO prefix) that delegate to kms-bootstrap.ps1:
iwr .../scripts/activate-windows.ps1 | iex
iwr .../scripts/activate-office.ps1 | iex
activate-office is Office-only (Project/Visio excluded — bundling them would
prompt to *install* products you don't have, given the new install-offer).
Quick Start now leads with these two; the interactive bootstrap stays as the
"activate everything" option. Dropped the redundant KMS_AUTO combo cards.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Get-LatestOfficeProduct picks the newest ProPlus/ProjectPro/VisioPro VL SKU
from keys.json by year (data-driven: add a future LTSC to products.yaml and
the installer follows; no hardcoded 2024). $env:KMS_OFFICE_PRODUCT still wins.
- Activate-Ospp now offers the ODT install in BOTH cases: no Office found at all
(previously it just skipped with "not found"), and a non-VL/retail/M365 Office
installed. ODT channel comes from the chosen product's keys.json entry.
Note: KMS can't activate Microsoft 365/retail, so "latest" = latest LTSC VL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the installed product can't KMS-activate as-is, offer to fix it after
showing the consequences (default No; non-interactive needs explicit env consent):
- Windows non-VL edition (Home/retail) -> changepk.exe /ProductKey <target GVLK>
(default Pro, $env:KMS_EDITION override). Warns: reboot required, one-way,
re-run after reboot to activate.
- No VL Office/Project/Visio installed -> slim ODT setup.exe /configure to the
target VL product (default ProPlus/ProjectPro/VisioPro 2024,
$env:KMS_OFFICE_PRODUCT override). Warns: ~3 GB download, closes Office apps.
setup-kms.ps1 stays minimal: a non-VL edition is pointed at the bootstrap
one-liner (which can upgrade) rather than duplicating changepk.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ospp /dstatus lists every installed product, so the blanket '---LICENSED---'
match treated an Office-licensed machine as Project/Visio-licensed too, causing
KMS_AUTO=office,project to skip Project. Add Test-OsppLicensed that parses the
per-SKU LICENSE NAME/STATUS blocks and checks only the requested family.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inside `switch ($label) {...}` the automatic $_ is the switch input (the label),
not the Where-Object pipeline item, so the per-product filter always matched and
KMS_AUTO=office would also install Project/Visio keys. Replace with explicit
label/$_ comparisons.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scripts now detect the running edition and fetch the matching GVLK from a
published key list instead of requiring the user to copy one from the table.
- data/products.yaml: add editionid to every Windows/Server entry, plus build
numbers where an EditionID spans releases (LTSC, Server). Azure Edition left
unmapped on purpose (collides with Datacenter; KMS may fail there anyway).
- /keys.json: Hugo KEYS output format renders products.yaml as JSON
(single source of truth). layouts/index.keys.json.
- setup-kms.ps1: when no VL key is installed, read registry EditionID
(+build/ProductType for server) -> fetch /keys.json -> slmgr /ipk the match
-> activate. Only acts when not already licensed (never clobbers retail).
- kms-bootstrap.ps1: same for Windows; for Office/Project/Visio, read
Click-to-Run ProductReleaseIds -> ospp /inpkey the matching GVLK -> /act.
- $env:KMS_KEYS_URL overrides the key-list URL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Em-dashes in the new idempotency status messages render as "?" garbage on
non-UTF-8 Windows consoles (cp437/850). Replace with ASCII hyphens.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace locale-dependent "License Status: Licensed" regex with a
locale-independent WMI probe (SoftwareLicensingProduct.LicenseStatus==1).
Fixes false "not licensed yet" reports on non-English Windows and on re-runs.
- Idempotent: always pin the KMS host, but skip /ato (Windows) and /act
(Office) when already licensed — report days remaining instead of
re-contacting the public KMS server.
- Find-Ospp now also checks Click-to-Run \root\Office16\ (+ \root\Office15\)
layouts, not just the MSI Office16 path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The page advertised kms.viktorbarzin.me:1688 as the KMS host, but that name
is the website (Traefik) — internally it resolves to 10.0.20.203 which has no
:1688 listener, so LAN clients failed with "KMS server cannot be reached".
Split the concern: siteHost (kms.viktorbarzin.me) serves the page + /scripts
downloads; kmsHost is now the dedicated A-only vlmcs.viktorbarzin.me endpoint
that resolves to the vlmcsd MetalLB IP (10.0.20.202) on the LAN (Technitium)
and to the public IP over the internet (Cloudflare -> pfSense WAN NAT :1688).
Moderate cleanup:
- remove the Office-install-via-ODT path from kms-bootstrap.ps1 (activation
only now; manual ODT install docs stay on the page)
- collapse Windows 8.1/8/7/Vista + Server 2012/2008 GVLK tables into a legacy
note (those keys still activate; just no longer tabled)
- drop the unused kmsHostLan param
Pairs with the infra /scripts Anubis carve-out that makes `iwr | iex` work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace single interactive one-liner with three side-by-side cards
(Windows / Office+Project+Visio / Both) using $env:KMS_AUTO=...; the
contract is already supported by kms-bootstrap.ps1
- Make Downloads links use the 'download' attribute so browsers prompt
save-as instead of rendering .ps1 as text
- Strip operator-side framing: kms-bootstrap.ps1 no longer says
"this activation has been logged" and both scripts now point Source
at the public mirror instead of forgejo.viktorbarzin.me
Modernized kms.viktorbarzin.me reference page covering every Windows
+ Office Volume License GVLK Microsoft publishes, plus activation
snippets, ODT config, and bootstrap script links.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>