diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000..90ce7cf
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,15 @@
+name: Checks
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ base64-drift:
+ runs-on: ubuntu-latest
+ timeout-minutes: 2
+ steps:
+ - uses: actions/checkout@v4
+ - name: Verify autounattend base64 ↔ standalone .ps1
+ run: python3 scripts/check-base64-drift.py
diff --git a/README.md b/README.md
index 1e3be03..a4ca106 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ Contents:
- `scripts/build-qemu.sh` — reproducible local QEMU build, with one rolling screenshot file and a bounded first-boot settle loop
- `scripts/verify.ps1` + `scripts/remediate.ps1` — in-guest verification / remediation loop
- `scripts/firstboot-state.ps1` — lightweight first-boot probe used to wait for concrete SAC runtime components before verification
+- `scripts/cocoon-nic-autoheal.ps1` — body of the `CocoonNicAutoHeal` scheduled task; cycles every Net PnP device once a minute to recover chained-clone guests where vm.restore leaves the NIC bound but unable to transmit
+- `scripts/install-cocoon-agent-bootstrap.ps1` — downloads the pinned `cocoon-agent` Windows release from GitHub, verifies SHA256, runs the bundled installer; lands at `C:\Scripts\` so `remediate.ps1` can re-invoke it
- `scripts/verify-ch.sh` + `scripts/sac_probe.py` — Cloud Hypervisor runtime validation for DHCP, RDP, real SAC, and clean shutdown
- `.github/workflows/build.yml` — headless QEMU/KVM build on `ubuntu-latest`, publishes to GHCR via ORAS
@@ -21,6 +23,9 @@ An image produced from this repo is only considered valid for Cocoon if all of t
- It acquires a DHCP lease from a plain `dnsmasq` bridge and reports hostname `COCOON-VM`.
- `3389/tcp` accepts a real RDP authentication attempt.
- `COM1` exposes a real SAC console after Cloud Hypervisor boot. A live `SAC>` prompt is the hard requirement; missing in-guest `ACPI\\PNP0501` enumeration is only a warning. `bcdedit /ems on` by itself is **not** sufficient.
+- The `Virtio Vsock STREAM` Winsock provider is registered (Address Family 40 in `netsh winsock show catalog`) and the `viosock` device is bound (`Get-PnpDevice` reports `Status=OK`). Required for `cocoon-agent`'s `AF_VSOCK` listener; `virtio-win-guest-tools.exe /S` does not register the WSP, so a separate `pnputil /add-driver viosock.inf /install` runs at firstboot.
+- The `cocoon-agent` Windows service is installed (`Get-Service cocoon-agent`) and running, listening on vsock port 1024. Pinned to a specific `cocoon-agent` release (currently v0.1.1); bumping requires a coordinated update of `scripts/install-cocoon-agent-bootstrap.ps1` + the matching base64 in `autounattend.xml` Order 53.
+- The `CocoonNicAutoHeal` scheduled task is registered (1-minute, SYSTEM, HIGHEST) and `C:\CocoonNicAutoHeal.ps1` exists. Recovers chained-clone NDIS state where vm.restore leaves the NIC bound but unable to transmit.
- A remote `shutdown /s /t 10` cleanly terminates the Cloud Hypervisor process.
## Pulling a pre-built image
@@ -399,7 +404,7 @@ The included [`autounattend.xml`](autounattend.xml) drives the install across th
- **International-Core**: `InputLocale=0409:00000409` only. The component must be present here for Windows 11 25H2 OOBE to skip the country / keyboard selection screens.
- **OOBE**: hides EULA, online account, wireless setup.
- **User account**: local admin `cocoon` with auto-logon (password base64-encoded in XML).
-- **FirstLogonCommands**: 53 commands.
+- **FirstLogonCommands**: 56 commands.
| Order | Action | Notes |
|--------|------------------------------|-------|
@@ -429,8 +434,11 @@ The included [`autounattend.xml`](autounattend.xml) drives the install across th
| 47 | **Zero startup delay** | `Explorer\Serialize\StartupDelayInMSec=0` |
| 48-50 | **DWM tuning** | No minimize animation, no drag full windows, ClearType font smoothing |
| 51 | **Disable scheduled tasks** | Compatibility Appraiser, ScheduledDefrag, DiskDiagnostic |
-| 52 | **QuickEdit restore** | Restore QuickEdit after install |
-| 53 | **Install marker** | `cmd /c "echo %date% %time% > C:\install.success"` |
+| 52 | **viosock driver** | `pnputil /add-driver D:\viosock\w11\amd64\viosock.inf /install` (D: + E: + standard + attestation paths). Required for cocoon-agent's `AF_VSOCK` listener — registers the `Virtio Vsock STREAM` Winsock provider (Address Family 40, `viosocklib.dll`). `virtio-win-guest-tools.exe /S` (Order 21) does not install this driver. |
+| 53 | **cocoon-agent install** | Download pinned `cocoon-agent_v0.1.1_Windows_x86_64.zip` from GitHub Releases, verify SHA256, run bundled `install-cocoon-agent.ps1` (registers `cocoon-agent` Windows service: LocalSystem, auto-start, restart-on-crash, vsock port 1024). Bootstrap dropped at `C:\Scripts\install-cocoon-agent-bootstrap.ps1` for `remediate.ps1` re-invoke. |
+| 54 | **NIC auto-heal task** | Write `C:\CocoonNicAutoHeal.ps1` and register `CocoonNicAutoHeal` schtasks (every minute, SYSTEM, HIGHEST). Cycles all Net PnP devices to recover chained-clone NDIS state. |
+| 55 | **QuickEdit restore** | Restore QuickEdit after install |
+| 56 | **Install marker** | `cmd /c "echo %date% %time% > C:\install.success"` |
> **Note on WinRM persistence**: `Enable-PSRemoting` + the `AllowUnencrypted`/`Basic` WSMan settings set by orders 14-16 do not always survive the very first post-install reboot on Win11 25H2. `remediate.ps1` re-applies them from the same deterministic settings, and the CI loop reboots → verifies → remediates → re-verifies to make the final image idempotent.
diff --git a/autounattend.xml b/autounattend.xml
index 9a90dd0..5025acb 100644
--- a/autounattend.xml
+++ b/autounattend.xml
@@ -156,9 +156,35 @@
51powershell -Command "Disable-ScheduledTask -TaskName '\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser' -EA SilentlyContinue; Disable-ScheduledTask -TaskName '\Microsoft\Windows\Defrag\ScheduledDefrag' -EA SilentlyContinue; Disable-ScheduledTask -TaskName '\Microsoft\Windows\DiskDiagnostic\Microsoft-Windows-DiskDiagnosticDataCollector' -EA SilentlyContinue"
+
+ 52cmd /c "if exist D:\viosock\w11\amd64\viosock.inf (pnputil /add-driver D:\viosock\w11\amd64\viosock.inf /install) else if exist E:\viosock\w11\amd64\viosock.inf (pnputil /add-driver E:\viosock\w11\amd64\viosock.inf /install) else if exist D:\Win11\amd64\viosock\viosock.inf (pnputil /add-driver D:\Win11\amd64\viosock\viosock.inf /install) else if exist E:\Win11\amd64\viosock\viosock.inf (pnputil /add-driver E:\Win11\amd64\viosock\viosock.inf /install)"
+
+
+
+ 53powershell.exe -NoProfile -EncodedCommand JABjAG8AbgB0AGUAbgB0ACAAPQAgAEAAJwAKACMAIABCAG8AbwB0AHMAdAByAGEAcAAgAGkAbgBzAHQAYQBsAGwAZQByACAAZgBvAHIAIABjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAA6ACAAZABvAHcAbgBsAG8AYQBkACAAdABoAGUAIABwAGkAbgBuAGUAZAAgAFcAaQBuAGQAbwB3AHMAIAByAGUAbABlAGEAcwBlAAoAIwAgAGYAcgBvAG0AIABHAGkAdABIAHUAYgAsACAAdgBlAHIAaQBmAHkAIABTAEgAQQAyADUANgAsACAAZQB4AHQAcgBhAGMAdAAsACAAcgB1AG4AIAB0AGgAZQAgAGIAdQBuAGQAbABlAGQAIABpAG4AcwB0AGEAbABsAGUAcgAuAAoAIwAKACMAIABUAG8AIABiAHUAbQBwACAAdABoAGUAIABhAGcAZQBuAHQAOgAgAHUAcABkAGEAdABlACAAJAB2AGUAcgBzAGkAbwBuACAAKwAgACQAZQB4AHAAZQBjAHQAZQBkACAAdABvAGcAZQB0AGgAZQByACAAKAByAGUAbABlAGEAcwBlACAAcABhAGcAZQAnAHMACgAjACAAYwBoAGUAYwBrAHMAdQBtAHMALgB0AHgAdAAgAGgAYQBzACAAdABoAGUAIABjAGEAbgBvAG4AaQBjAGEAbAAgAFMASABBADIANQA2ACkALgAgAFQAaABlACAAbQBhAHQAYwBoAGkAbgBnACAAYgBhAHMAZQA2ADQAIABiAGwAbwBjAGsAIABpAG4ACgAjACAAYQB1AHQAbwB1AG4AYQB0AHQAZQBuAGQALgB4AG0AbAAgAE8AcgBkAGUAcgAgADUAMwAgAG0AdQBzAHQAIABiAGUAIAByAGUAZwBlAG4AZQByAGEAdABlAGQAIABmAHIAbwBtACAAdABoAGkAcwAgAGYAaQBsAGUAIABpAG4AIABsAG8AYwBrAHMAdABlAHAACgAjACAAbwByACAAaQBtAGEAZwBlAHMAIAB3AGkAbABsACAAcwBpAGwAZQBuAHQAbAB5ACAAcwBoAGkAcAAgAHQAaABlACAAbwBsAGQAIAB2AGUAcgBzAGkAbwBuAC4ACgAKACQARQByAHIAbwByAEEAYwB0AGkAbwBuAFAAcgBlAGYAZQByAGUAbgBjAGUAIAA9ACAAJwBTAHQAbwBwACcACgAkAFAAcgBvAGcAcgBlAHMAcwBQAHIAZQBmAGUAcgBlAG4AYwBlACAAIAAgACAAPQAgACcAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAnAAoACgAkAHYAZQByAHMAaQBvAG4AIAAgAD0AIAAnADAALgAxAC4AMQAnAAoAJAB1AHIAbAAgACAAIAAgACAAIAA9ACAAIgBoAHQAdABwAHMAOgAvAC8AZwBpAHQAaAB1AGIALgBjAG8AbQAvAGMAbwBjAG8AbwBuAHMAdABhAGMAawAvAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC8AcgBlAGwAZQBhAHMAZQBzAC8AZABvAHcAbgBsAG8AYQBkAC8AdgAkAHYAZQByAHMAaQBvAG4ALwBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdABfACQAewB2AGUAcgBzAGkAbwBuAH0AXwBXAGkAbgBkAG8AdwBzAF8AeAA4ADYAXwA2ADQALgB6AGkAcAAiAAoAJABlAHgAcABlAGMAdABlAGQAIAA9ACAAJwBhAGMAZAAyAGUAOAA3AGMAMwA1ADkANAA3AGQAYgA0ADAANwA2AGYAOQBmAGEAMQBiADkAMQBhADMAMQA2ADIAZgA5ADEAMAA2ADEAZQA1AGEAOAA2ADMAZQA3ADUAZQA1AGIAMAAzADIANwAxADQAZQBmAGQAYwA0ADAAYQBiACcACgAkAHoAaQBwACAAIAAgACAAIAAgAD0AIABKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAIgBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdABfACQAewB2AGUAcgBzAGkAbwBuAH0AXwBXAGkAbgBkAG8AdwBzAF8AeAA4ADYAXwA2ADQALgB6AGkAcAAiAAoAJABlAHgAdAByAGEAYwB0ACAAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAGUAbgB2ADoAVABFAE0AUAAgACIAYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALQAkAHsAdgBlAHIAcwBpAG8AbgB9ACIACgAKACMAIABEAEgAQwBQAC8ARABOAFMAIABjAGEAbgAgAHQAYQBrAGUAIABhACAAZgBlAHcAIABzAGUAYwBvAG4AZABzACAAdABvACAAcwBlAHQAdABsAGUAIABhAHQAIABmAGkAcgBzAHQAYgBvAG8AdAAuAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgADUAOwAgACQAaQArACsAKQAgAHsACgAgACAAIAAgAHQAcgB5ACAAewAgAEkAbgB2AG8AawBlAC0AVwBlAGIAUgBlAHEAdQBlAHMAdAAgAC0AVQByAGkAIAAkAHUAcgBsACAALQBPAHUAdABGAGkAbABlACAAJAB6AGkAcAAgAC0AVQBzAGUAQgBhAHMAaQBjAFAAYQByAHMAaQBuAGcAOwAgAGIAcgBlAGEAawAgAH0ACgAgACAAIAAgAGMAYQB0AGMAaAAgAHsAIABpAGYAIAAoACQAaQAgAC0AZQBxACAANQApACAAewAgAHQAaAByAG8AdwAgAH0AOwAgAFMAdABhAHIAdAAtAFMAbABlAGUAcAAgAC0AUwBlAGMAbwBuAGQAcwAgADUAIAB9AAoAfQAKAAoAJABhAGMAdAB1AGEAbAAgAD0AIAAoAEcAZQB0AC0ARgBpAGwAZQBIAGEAcwBoACAALQBQAGEAdABoACAAJAB6AGkAcAAgAC0AQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgApAC4ASABhAHMAaAAuAFQAbwBMAG8AdwBlAHIAKAApAAoAaQBmACAAKAAkAGEAYwB0AHUAYQBsACAALQBuAGUAIAAkAGUAeABwAGUAYwB0AGUAZAApACAAewAKACAAIAAgACAAdABoAHIAbwB3ACAAIgBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAAgAHoAaQBwACAAYwBoAGUAYwBrAHMAdQBtACAAbQBpAHMAbQBhAHQAYwBoADoAIABlAHgAcABlAGMAdABlAGQAIAAkAGUAeABwAGUAYwB0AGUAZAAsACAAZwBvAHQAIAAkAGEAYwB0AHUAYQBsACIACgB9AAoACgBpAGYAIAAoAFQAZQBzAHQALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACkAIAB7ACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBSAGUAYwB1AHIAcwBlACAALQBGAG8AcgBjAGUAIAAkAGUAeAB0AHIAYQBjAHQAIAB9AAoARQB4AHAAYQBuAGQALQBBAHIAYwBoAGkAdgBlACAALQBQAGEAdABoACAAJAB6AGkAcAAgAC0ARABlAHMAdABpAG4AYQB0AGkAbwBuAFAAYQB0AGgAIAAkAGUAeAB0AHIAYQBjAHQAIAAtAEYAbwByAGMAZQAKAAoAIwAgAEwAbwBjAGEAdABlACAAYgB5ACAAZwBsAG8AYgAgABQgIAB6AGkAcAAgAHAAdQB0AHMAIABlAHgAZQAgAGEAdAAgAHIAbwBvAHQAIABhAG4AZAAgAHAAcwAxACAAdQBuAGQAZQByACAAcABhAGMAawBhAGcAaQBuAGcALwAuAAoAJABwAHMAMQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACAALQBSAGUAYwB1AHIAcwBlACAALQBGAGkAbAB0AGUAcgAgACcAaQBuAHMAdABhAGwAbAAtAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AcABzADEAJwAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAALQBGAGkAcgBzAHQAIAAxAAoAJABlAHgAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACAALQBSAGUAYwB1AHIAcwBlACAALQBGAGkAbAB0AGUAcgAgACcAYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALgBlAHgAZQAnACAAIAAgACAAIAAgACAAIAAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAALQBGAGkAcgBzAHQAIAAxAAoAaQBmACAAKAAtAG4AbwB0ACAAJABwAHMAMQApACAAewAgAHQAaAByAG8AdwAgACIAaQBuAHMAdABhAGwAbAAtAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AcABzADEAIABuAG8AdAAgAGYAbwB1AG4AZAAgAGkAbgAgACQAZQB4AHQAcgBhAGMAdAAiACAAfQAKAGkAZgAgACgALQBuAG8AdAAgACQAZQB4AGUAKQAgAHsAIAB0AGgAcgBvAHcAIAAiAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AZQB4AGUAIABuAG8AdAAgAGYAbwB1AG4AZAAgAGkAbgAgACQAZQB4AHQAcgBhAGMAdAAiACAAfQAKAAoAJgAgACQAcABzADEALgBGAHUAbABsAE4AYQBtAGUAIAAtAEIAaQBuAGEAcgB5AFMAbwB1AHIAYwBlACAAJABlAHgAZQAuAEYAdQBsAGwATgBhAG0AZQAKACcAQAAKAE4AZQB3AC0ASQB0AGUAbQAgAC0ASQB0AGUAbQBUAHkAcABlACAARABpAHIAZQBjAHQAbwByAHkAIAAtAEYAbwByAGMAZQAgAC0AUABhAHQAaAAgACcAQwA6AFwAUwBjAHIAaQBwAHQAcwAnACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKAFMAZQB0AC0AQwBvAG4AdABlAG4AdAAgAC0AUABhAHQAaAAgACcAQwA6AFwAUwBjAHIAaQBwAHQAcwBcAGkAbgBzAHQAYQBsAGwALQBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAAtAGIAbwBvAHQAcwB0AHIAYQBwAC4AcABzADEAJwAgAC0AVgBhAGwAdQBlACAAJABjAG8AbgB0AGUAbgB0ACAALQBFAG4AYwBvAGQAaQBuAGcAIABBAFMAQwBJAEkAIAAtAEYAbwByAGMAZQAKACYAIAAnAEMAOgBcAFMAYwByAGkAcAB0AHMAXABpAG4AcwB0AGEAbABsAC0AYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALQBiAG8AbwB0AHMAdAByAGEAcAAuAHAAcwAxACcACgA=
+
+
+ 54powershell.exe -NoProfile -EncodedCommand JABjAG8AbgB0AGUAbgB0ACAAPQAgAEAAJwAKACMAIABjAG8AYwBvAG8AbgAtAG4AaQBjAC0AYQB1AHQAbwBoAGUAYQBsAC4AcABzADEAIAAUICAAYwB5AGMAbABlACAAZQB2AGUAcgB5ACAATgBlAHQALQBjAGwAYQBzAHMAIABQAG4AUAAgAGQAZQB2AGkAYwBlACAAbwBuAGMAZQAuAAoAIwAKACMAIABUAHIAaQBnAGcAZQByAGUAZAAgAGIAeQAgAHQAaABlACAAQwBvAGMAbwBvAG4ATgBpAGMAQQB1AHQAbwBIAGUAYQBsACAAcwBjAGgAZQBkAHUAbABlAGQAIAB0AGEAcwBrACAAKAByAGUAZwBpAHMAdABlAHIAZQBkACAAYQB0ACAAZgBpAHIAcwB0AGIAbwBvAHQAKQAKACMAIABvAG4AIABhACAAMQAtAG0AaQBuAHUAdABlACAAcgBlAHAAZQBhAHQALgAgAFIAZQBjAG8AdgBlAHIAcwAgAGMAaABhAGkAbgBlAGQALQBjAGwAbwBuAGUAIABXAGkAbgAxADEAIABnAHUAZQBzAHQAcwAgAHcAaABlAHIAZQAgAHYAbQAuAHIAZQBzAHQAbwByAGUACgAjACAAbABlAGEAdgBlAHMAIAB0AGgAZQAgAE4ASQBDACAAYgBvAHUAbgBkACAAYQB0ACAAdABoAGUAIABPAFMAIABsAGEAeQBlAHIAIABiAHUAdAAgAHUAbgBhAGIAbABlACAAdABvACAAdAByAGEAbgBzAG0AaQB0ACAAFCAgAFMAdABhAHQAdQBzACAAcgBlAHAAbwByAHQAcwAKACMAIAAnAE8ASwAnACAAcwBvACAAYQAgACIAUwB0AGEAdAB1AHMAIAAtAEUAUQAgAEUAcgByAG8AcgAiACAAZgBpAGwAdABlAHIAIAB3AG8AdQBsAGQAIABtAGkAcwBzACAAaQB0AC4AIABDAHkAYwBsAGUAIAB1AG4AYwBvAG4AZABpAHQAaQBvAG4AYQBsAGwAeQAuAAoAJABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAiAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAIgAKAGYAbwByAGUAYQBjAGgAIAAoACQAZAAgAGkAbgAgACgARwBlAHQALQBQAG4AcABEAGUAdgBpAGMAZQAgAC0AQwBsAGEAcwBzACAATgBlAHQAKQApACAAewAKACAAIAAgACAARABpAHMAYQBiAGwAZQAtAFAAbgBwAEQAZQB2AGkAYwBlACAALQBJAG4AcwB0AGEAbgBjAGUASQBkACAAJABkAC4ASQBuAHMAdABhAG4AYwBlAEkAZAAgAC0AQwBvAG4AZgBpAHIAbQA6ACQAZgBhAGwAcwBlAAoAIAAgACAAIABTAHQAYQByAHQALQBTAGwAZQBlAHAAIAAtAFMAZQBjAG8AbgBkAHMAIAAyAAoAIAAgACAAIABFAG4AYQBiAGwAZQAtAFAAbgBwAEQAZQB2AGkAYwBlACAALQBJAG4AcwB0AGEAbgBjAGUASQBkACAAJABkAC4ASQBuAHMAdABhAG4AYwBlAEkAZAAgAC0AQwBvAG4AZgBpAHIAbQA6ACQAZgBhAGwAcwBlAAoAfQAKACcAQAAKAFMAZQB0AC0AQwBvAG4AdABlAG4AdAAgAC0AUABhAHQAaAAgAEMAOgBcAEMAbwBjAG8AbwBuAE4AaQBjAEEAdQB0AG8ASABlAGEAbAAuAHAAcwAxACAALQBWAGEAbAB1AGUAIAAkAGMAbwBuAHQAZQBuAHQAIAAtAEUAbgBjAG8AZABpAG4AZwAgAEEAUwBDAEkASQAgAC0ARgBvAHIAYwBlAAoAcwBjAGgAdABhAHMAawBzACAALwBjAHIAZQBhAHQAZQAgAC8AdABuACAAQwBvAGMAbwBvAG4ATgBpAGMAQQB1AHQAbwBIAGUAYQBsACAALwB0AHIAIAAiAHAAbwB3AGUAcgBzAGgAZQBsAGwALgBlAHgAZQAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEUAeABlAGMAdQB0AGkAbwBuAFAAbwBsAGkAYwB5ACAAQgB5AHAAYQBzAHMAIAAtAEYAaQBsAGUAIABDADoAXABDAG8AYwBvAG8AbgBOAGkAYwBBAHUAdABvAEgAZQBhAGwALgBwAHMAMQAiACAALwBzAGMAIABtAGkAbgB1AHQAZQAgAC8AbQBvACAAMQAgAC8AcgB1ACAAUwBZAFMAVABFAE0AIAAvAHIAbAAgAEgASQBHAEgARQBTAFQAIAAvAGYACgA=
+
- 52reg add "HKCU\Console" /v QuickEdit /t REG_DWORD /d 1 /f
- 53cmd /c "echo %date% %time% > C:\install.success"
+ 55reg add "HKCU\Console" /v QuickEdit /t REG_DWORD /d 1 /f
+ 56cmd /c "echo %date% %time% > C:\install.success"
diff --git a/scripts/check-base64-drift.py b/scripts/check-base64-drift.py
new file mode 100755
index 0000000..27d33e1
--- /dev/null
+++ b/scripts/check-base64-drift.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+"""Verify the base64-encoded PowerShell wrappers in autounattend.xml decode
+to a here-string body byte-for-byte matching the corresponding standalone
+script in scripts/. The base64 in autounattend Orders 53 / 54 is a self-
+extracting wrapper that drops the body to disk at firstboot; if the two
+sources drift, images silently install the old version.
+
+To regenerate a base64 block after editing the standalone .ps1, run this
+script in --regen mode (TODO: not yet implemented; for now, edit by hand
+using the recipe below).
+
+Encoding recipe (matches autounattend.xml Order 53/54 format):
+
+ wrapper = f'''$content = @'
+{ps1_body}
+'@
+... wrapper body that writes $content to disk and invokes it ...
+'''
+ base64.b64encode(wrapper.encode('utf-16-le')).decode('ascii')
+
+Run from repo root: python3 scripts/check-base64-drift.py
+"""
+
+import base64
+import re
+import sys
+from pathlib import Path
+
+PAIRS = [
+ (53, 'scripts/install-cocoon-agent-bootstrap.ps1'),
+ (54, 'scripts/cocoon-nic-autoheal.ps1'),
+]
+
+
+def check_pair(autounattend: str, order: int, ps1_path: str) -> tuple[bool, str]:
+ pattern = (
+ rf'{order}powershell\.exe -NoProfile '
+ rf'-EncodedCommand ([^<]+)'
+ )
+ m = re.search(pattern, autounattend)
+ if not m:
+ return False, f'Order {order}: ... not found in autounattend.xml'
+ decoded = base64.b64decode(m.group(1)).decode('utf-16-le')
+ inner = re.search(r"@'\n(.*?)\n'@", decoded, re.DOTALL)
+ if not inner:
+ return False, f'Order {order}: single-quoted here-string body not found in decoded wrapper'
+ embedded = inner.group(1).rstrip()
+ standalone = Path(ps1_path).read_text().rstrip()
+ if embedded == standalone:
+ return True, f'OK Order {order} matches {ps1_path}'
+ return False, (
+ f'Order {order} drift: decoded base64 body does not match {ps1_path}\n'
+ f' re-encode the wrapper from the standalone .ps1 (see this script header)'
+ )
+
+
+def main() -> int:
+ autounattend = Path('autounattend.xml').read_text()
+ failures = 0
+ for order, ps1_path in PAIRS:
+ ok, msg = check_pair(autounattend, order, ps1_path)
+ print(msg if ok else f'FAIL: {msg}', file=sys.stderr if not ok else sys.stdout)
+ if not ok:
+ failures += 1
+ return 1 if failures else 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/scripts/cocoon-nic-autoheal.ps1 b/scripts/cocoon-nic-autoheal.ps1
new file mode 100644
index 0000000..feaaf87
--- /dev/null
+++ b/scripts/cocoon-nic-autoheal.ps1
@@ -0,0 +1,12 @@
+# cocoon-nic-autoheal.ps1 — cycle every Net-class PnP device once.
+#
+# Triggered by the CocoonNicAutoHeal scheduled task (registered at firstboot)
+# on a 1-minute repeat. Recovers chained-clone Win11 guests where vm.restore
+# leaves the NIC bound at the OS layer but unable to transmit — Status reports
+# 'OK' so a "Status -EQ Error" filter would miss it. Cycle unconditionally.
+$ErrorActionPreference = "SilentlyContinue"
+foreach ($d in (Get-PnpDevice -Class Net)) {
+ Disable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false
+ Start-Sleep -Seconds 2
+ Enable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false
+}
diff --git a/scripts/install-cocoon-agent-bootstrap.ps1 b/scripts/install-cocoon-agent-bootstrap.ps1
new file mode 100644
index 0000000..b1983f0
--- /dev/null
+++ b/scripts/install-cocoon-agent-bootstrap.ps1
@@ -0,0 +1,38 @@
+# Bootstrap installer for cocoon-agent: download the pinned Windows release
+# from GitHub, verify SHA256, extract, run the bundled installer.
+#
+# To bump the agent: update $version + $expected together (release page's
+# checksums.txt has the canonical SHA256). The matching base64 block in
+# autounattend.xml Order 53 must be regenerated from this file in lockstep
+# or images will silently ship the old version.
+
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue'
+
+$version = '0.1.1'
+$url = "https://github.com/cocoonstack/cocoon-agent/releases/download/v$version/cocoon-agent_${version}_Windows_x86_64.zip"
+$expected = 'acd2e87c35947db4076f9fa1b91a3162f91061e5a863e75e5b032714efdc40ab'
+$zip = Join-Path $env:TEMP "cocoon-agent_${version}_Windows_x86_64.zip"
+$extract = Join-Path $env:TEMP "cocoon-agent-${version}"
+
+# DHCP/DNS can take a few seconds to settle at firstboot.
+for ($i = 1; $i -le 5; $i++) {
+ try { Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing; break }
+ catch { if ($i -eq 5) { throw }; Start-Sleep -Seconds 5 }
+}
+
+$actual = (Get-FileHash -Path $zip -Algorithm SHA256).Hash.ToLower()
+if ($actual -ne $expected) {
+ throw "cocoon-agent zip checksum mismatch: expected $expected, got $actual"
+}
+
+if (Test-Path $extract) { Remove-Item -Recurse -Force $extract }
+Expand-Archive -Path $zip -DestinationPath $extract -Force
+
+# Locate by glob — zip puts exe at root and ps1 under packaging/.
+$ps1 = Get-ChildItem -Path $extract -Recurse -Filter 'install-cocoon-agent.ps1' | Select-Object -First 1
+$exe = Get-ChildItem -Path $extract -Recurse -Filter 'cocoon-agent.exe' | Select-Object -First 1
+if (-not $ps1) { throw "install-cocoon-agent.ps1 not found in $extract" }
+if (-not $exe) { throw "cocoon-agent.exe not found in $extract" }
+
+& $ps1.FullName -BinarySource $exe.FullName
diff --git a/scripts/remediate.ps1 b/scripts/remediate.ps1
index 5004769..0af4f26 100644
--- a/scripts/remediate.ps1
+++ b/scripts/remediate.ps1
@@ -148,4 +148,45 @@ Disable-ScheduledTask -TaskName '\Microsoft\Windows\Application Experience\Micro
Disable-ScheduledTask -TaskName '\Microsoft\Windows\Defrag\ScheduledDefrag' -ErrorAction SilentlyContinue
Disable-ScheduledTask -TaskName '\Microsoft\Windows\DiskDiagnostic\Microsoft-Windows-DiskDiagnosticDataCollector' -ErrorAction SilentlyContinue
+# --- viosock driver ---
+$vsockCat = (netsh winsock show catalog 2>&1 | Out-String)
+if ($vsockCat -notmatch 'Virtio Vsock STREAM') {
+ Write-Output "Installing viosock driver..."
+ foreach ($p in 'D:\viosock\w11\amd64\viosock.inf',
+ 'E:\viosock\w11\amd64\viosock.inf',
+ 'D:\Win11\amd64\viosock\viosock.inf',
+ 'E:\Win11\amd64\viosock\viosock.inf') {
+ if (Test-Path $p) { pnputil /add-driver $p /install | Out-Null; break }
+ }
+}
+
+# --- NIC auto-heal task ---
+$autoheal = schtasks /query /tn CocoonNicAutoHeal 2>&1 | Out-String
+if ($LASTEXITCODE -ne 0) {
+ Write-Output "Re-creating CocoonNicAutoHeal task..."
+ @'
+$ErrorActionPreference = "SilentlyContinue"
+foreach ($d in (Get-PnpDevice -Class Net)) {
+ Disable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false
+ Start-Sleep -Seconds 2
+ Enable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false
+}
+'@ | Out-File -Encoding ASCII -FilePath 'C:\CocoonNicAutoHeal.ps1' -Force
+ schtasks /create /tn CocoonNicAutoHeal `
+ /tr 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\CocoonNicAutoHeal.ps1' `
+ /sc minute /mo 1 /ru SYSTEM /rl HIGHEST /f | Out-Null
+}
+
+# --- cocoon-agent service ---
+$cocoon = Get-Service -Name cocoon-agent -ErrorAction SilentlyContinue
+if ($null -eq $cocoon -or $cocoon.Status -ne 'Running') {
+ $bootstrap = 'C:\Scripts\install-cocoon-agent-bootstrap.ps1'
+ if (Test-Path $bootstrap) {
+ Write-Output "Re-running cocoon-agent bootstrap..."
+ & $bootstrap
+ } else {
+ Write-Output "WARN: cocoon-agent bootstrap missing at $bootstrap; service install requires re-running autounattend"
+ }
+}
+
Write-Output "=== Remediation complete ==="
diff --git a/scripts/verify.ps1 b/scripts/verify.ps1
index 6d41624..b1a546a 100644
--- a/scripts/verify.ps1
+++ b/scripts/verify.ps1
@@ -183,6 +183,32 @@ $vgt = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninsta
Where-Object { $_.DisplayName -match 'Virtio-win' }
Check "virtio-win guest tools installed" ($null -ne $vgt)
+# --- viosock (virtio-vsock) driver: required for cocoon-agent's AF_VSOCK ---
+# virtio-win-guest-tools.exe /S installs viostor / NetKvm / balloon but skips
+# viosock; autounattend Order 52 runs pnputil for it. Two checks: hardware bind
+# (Get-PnpDevice) plus Winsock provider registration (catalog must list AF=40).
+$vsockDev = Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue |
+ Where-Object { $_.InstanceId -like '*VEN_1AF4*DEV_1053*' }
+Check "viosock device bound (Status=OK)" ($null -ne $vsockDev -and $vsockDev.Status -eq 'OK')
+
+$vsockCat = (netsh winsock show catalog 2>&1 | Out-String)
+Check "Virtio Vsock STREAM provider registered" ($vsockCat -match 'Virtio Vsock STREAM')
+
+# --- NIC auto-heal scheduled task ---
+$autoheal = schtasks /query /tn CocoonNicAutoHeal /fo LIST 2>&1 | Out-String
+Check "CocoonNicAutoHeal task registered" ($LASTEXITCODE -eq 0 -and $autoheal -match 'CocoonNicAutoHeal')
+Check "C:\CocoonNicAutoHeal.ps1 present" (Test-Path 'C:\CocoonNicAutoHeal.ps1')
+
+# --- cocoon-agent service ---
+$cocoon = Get-Service -Name cocoon-agent -ErrorAction SilentlyContinue
+Check "cocoon-agent service running" ($null -ne $cocoon -and $cocoon.Status -eq 'Running')
+Check "cocoon-agent bootstrap present" (Test-Path 'C:\Scripts\install-cocoon-agent-bootstrap.ps1')
+$cocoonExe = "$env:ProgramFiles\Cocoon\cocoon-agent.exe"
+if (Test-Path $cocoonExe) {
+ $ver = (& $cocoonExe --version 2>&1 | Out-String).Trim()
+ Write-Host " cocoon-agent: $ver"
+}
+
# --- Install marker ---
Check "C:\install.success exists" (Test-Path C:\install.success)