Repository navigation
Add VM lifecycle v2, an image library and golden image builds to Hyper-V Control - #9
Merged
Merged
Conversation
Add HyperVScript, a builder that every host operation now uses: one hardened
module bootstrap shared with the typed HyperVService, in-script resolution of
a VM by exact name or GUID (one PowerShell process per tool call instead of
two), state assertions, typed parameter rendering with literal quoting, and a
uniform failure envelope { ok:false, error, errorType, errorId, category }.
Move the inline create/remove/configure/export/import/network/disk scripts out
of the MCP tool layer into VmLifecycleService, so tool methods only parse,
validate and call a service. Typed power, checkpoint and firmware results use
the same failure envelope. The version string is read from the assembly.
Add a recording mock of the Hyper-V module selected through
HYPERV_CONTROL_MODULE_PATH, a MockHyperV test helper, and lifecycle tests that
assert on the cmdlets and parameters each operation emits. Scaffold the job
runner, image services, tool partials and validation phases for the lifecycle
v2 and image work that follows.
Implement phase 3a of the Hyper-V Control plan: hyperv_image (list, fetch, register, remove, catalogs, deploy) and hyperv_jobs (list, status, wait, cancel) over a rewritten ImageCatalogService. Catalogs use Quick Create's gallery schema and are parsed as leniently as VMCreate does: UTF-16 or UTF-8 with or without a BOM, trailing commas, comments, case-insensitive names, string-or-bool secureBoot, any hash prefix treated as SHA-256, and the min/max host version filter. Each image carries warnings: the expired WinDev2407Eval evaluation (2024-10-23), other likely expired Windows evaluations, images older than three years, Ubuntu releases out of standard support, missing or invalid hashes and unsupported disk types. Listing never touches a disk URL, and recommends the newest supported image per OS family. catalogs.json starts with Microsoft's feed; custom catalogs may be URLs, files or UNC paths. The library lives under <VirtualHardDiskPath>\HyperVControl\Images (read once from Get-VMHost and cached) or HYPERV_CONTROL_IMAGE_ROOT. library.json and catalogs.json are updated under an in-process gate and a cross-process lock file, and replaced atomically. A fetch is a job. One suffix-range GET resolves redirects, the size and the zip's central directory (Zip64 aware), so free space for the download plus the extracted entry is checked before downloading. The download resumes a partial file with an HTTP Range request, hashes as it writes, and keeps the partial file when the connection drops or stalls; a SHA-256 mismatch deletes it. The archiveRelativePath entry is extracted by streaming, with magic bytes checked before and after, the entry is recorded and the archive deleted unless keepArchive. Cancelling deletes the partial download and any extraction output. Register adopts a local .vhdx or .iso in place; templates become read-only. Remove refuses while a VM attaches the file (disk or DVD) or any disk chain, including checkpoint disks and recorded clones, has it as a differencing parent. Deploy creates a differencing child (or a full copy) under the VM's folder, creates the VM through VmLifecycleService.CreateAsync, then applies the lab defaults in one script: static memory, automatic checkpoints off, Standard checkpoints, Secure Boot by OS family or off when the image says so, HvSocket for Linux, and a local key protector plus vTPM for Windows after the template. A failed deploy removes the half-created VM and disk. Unit tests drive the saved live feed, a UTF-8 custom catalog, a hand-built Zip64 archive and a loopback HTTP server (redirects, ranges, a dropped and a stalled download) against the recording mock module, which gains state-driven Get-VMHost, Get-VHD parent chains, New-VHD and checkpoint disks in images.functions.ps1. ImageLiveTests covers the images-live phase.
Add references/images.md for agents: the list, fetch, deploy and boot workflow, catalog rules shared with Quick Create (encodings, trailing commas, hash prefix, disk types, host version filter, custom galleries), what each warning means (including the Windows 11 dev environment that expired on 2024-10-23), the library layout and its in-use guard, fetch semantics for interruption, cancellation and hash mismatches, disk-space expectations, the deploy lab defaults and the error types. SKILL.md does not link it yet. Split ImageCatalogService into partial files: the core (root, list, catalogs, recommendation), fetch, library (register, remove and the in-use guard) and deploy, so the deploy path lives in one file. The move changes no code. Free-space queries now work for a library root at a drive root, which used to produce a doubled separator.
…ugin-vm-management-e74fbe
A UAC launch starts the worker with the user's profile environment, so every HYPERV_CONTROL_* override set only in the MCP client's process environment was silently lost. The parent now writes an allowlisted set of variables (HYPERV_CONTROL_*, HYPERV_GUEST_*, _NT_SYMBOL_PATH) as a length-prefixed JSON header over the private pipe before relaying MCP traffic, and the worker applies them before starting. Verified live: the worker reports the image root set in the client environment.
Add VmStateRules, the setting x VM state table from the verified Hyper-V matrix. Configure, firmware, network and disk calls render one in-script gate that refuses the whole request before any change and lists every setting that needs the VM Off; Running conditions cover static memory resize, dynamic memory bounds that may only grow or shrink, Generation 2 adapter hot add/remove, SCSI data disks and online VHDX resize. hyperv_vm_create gains sources new_disk, iso, existing_vhd, linked and none, host default paths, Default Switch by ID, OS-family Secure Boot and vTPM defaults (template before key protector before Enable-VMTPM), a duplicate-name guard, a free-space preflight and rollback of a half-made VM. hyperv_vm_remove takes a name or ID, never powers off, and with deleteDisks removes checkpoints, waits for their merge, removes the VM and deletes only files no other VM or differencing child uses; dryRun returns the plan. hyperv_vm_configure, hyperv_firmware, hyperv_network and hyperv_disk gain the planned settings and actions; hyperv_vhd and hyperv_vm_clone (linked and full) are new; hyperv_vm_info and hyperv_capabilities report more state. The recording mock gains lifecycle.functions.ps1 with disk files, chains, checkpoint merges, adapters, firmware, TPM and NAT, and LifecycleV2Tests cover the table, refusals without mutation, cmdlet order and plans. LifecycleTests follow the new create and remove signatures.
Windows: apply an ISO, an install.wim/esd or a downloaded Microsoft evaluation ISO (eval, eval-ltsc, eval-iot; resumable and cached) offline to a GPT VHDX (300 MB ESP formatted FAT32 then retyped, 16 MB MSR, NTFS), make it bootable with bcdboot /s (plus /offline where supported), and write a specialize/oobeSystem unattend.xml: local administrator, RDP and its firewall group, autologon, device-encryption prevention, BypassNRO, and first-logon deletion of the answer file. Optional offline kernel-debug BCD settings and a C:\Tools copy. Where the host's bcdboot copies the boot files but cannot create the store on another disk's ESP (BFSVC c0000035), the store is completed with bcdedit from the image's own BCD-Template. Linux: download an Ubuntu cloud image checked against its SHA256SUMS, convert it with qemu-img (HYPERV_CONTROL_QEMU_IMG, PATH or Program Files), grow it, and build a NoCloud CIDATA seed ISO with inbox IMAPI2. Every build runs as a job, writes <name>.image.json beside the VHDX for the image library, refuses existing outputs, detaches whatever it attached and deletes its outputs on failure. A generated password is returned once in the start response, never in the job result or the sidecar. Unit tests cover validation, unattend and cloud-init content, script rendering and ordering of both pipelines against the recording mock, cleanup on failure, resumable downloads over loopback HTTP and a real IMAPI2 seed ISO.
Refuse a download up front when the cache volume cannot hold the rest of the file, instead of failing when the disk fills. Copy toolsDirectory into an existing C:\Tools of a custom image, but never overwrite a file the image already has. Clarify the failed-build hint and the seed-ISO sharing note in nextSteps.
The image-build-live phase drives the published EXE without downloading Windows media or creating VMs. A synthetic WIM (placeholder hives, the host's boot files, BCD template and bootstr.dll, and a marker) in an IMAPI2 ISO runs the whole Windows pipeline into a 4 GB VHDX under D:\HyperVControl-validation\imagebuild, which is then mounted read-only to check the GPT layout, applied files, Panther unattend, C:\Tools and a copy of the offline BCD store (boot manager, winload, debug on, KDNET). A WIM without boot files must fail with bcdboot's reason and leave no outputs. For Linux, the missing-qemu-img refusal is checked with an unreachable URL, and a .vhdx source is copied, grown with Resize-VHD and paired with a seed ISO that Windows mounts as CIDATA. Elevated helpers prepare, verify and clean up; the run directory is detached and deleted in every outcome, and secrets are redacted from the evidence. references/image-build.md tells agents when to build, which parameters exist, what the Windows image contains, the one-time password rule, the Ubuntu seed workflow, the qemu-img dependency and evaluation expiry.
…ugin-vm-management-e74fbe # Conflicts: # tests/HyperVControl.Tests/PhaseStubs.cs
Live runs on Windows 11 26200 showed that New-VM enables dynamic memory and boots the network adapter before the new disk. Create and linked clone now set the memory mode explicitly (static by default) and put the disk first; an ISO source still boots from the DVD first. The mock New-VM mirrors that host behavior so unit tests guard both fixes. COM port pipe mapping (serial_host) now also works while Running, which the live run confirmed; the result notes that a Generation 2 guest sees a newly mapped port only after a full stop and start, and the kernel configure description and error text say so. The lifecycle-live phase now covers the iso source (with a generated data ISO), existing_vhd and linked sources, and a full clone, and records the static memory resize outcome: without a booted guest OS Hyper-V refuses it and memory stays unchanged.
Rewrite the VM operations reference for lifecycle v2: what each VM state allows and the all-or-nothing refusals, create sources and defaults (including the explicit memory mode and disk-first boot order), firmware and vTPM order, media, network and NAT, virtual disk safety, linked and full clones, and what removal deletes or keeps.
…ugin-vm-management-e74fbe # Conflicts: # tests/HyperVControl.Tests/PhaseStubs.cs
Route hyperv_image deploy and hyperv_vm_create source=image through the one VM create path, so linked clones from library images get the same defaults, guards and rollback as every other VM. Run full clones, exports and imports as hyperv_jobs jobs and report a failed job as a tool error. Export-VM fails with "Access is denied" when a differencing parent in the chain carries the read-only attribute, which the plugin sets on linked parents; exports and full clones now clear it for the duration of the export and restore it afterwards (verified live on Windows 11 26200). Removal plans report chain parents the VM does not own instead of staying silent. Bump the runtime and plugin to 0.2.0, regenerate the client manifests, pin the 0.2.0 asset in runtime.json, and route the new lifecycle, image and job workflows in the skill and README.
EnableCompressionInSingleFile shrinks the self-contained executable from 121 MB to 53 MB for about 50 ms more at process start. ReadyToRun was measured too (146 MB uncompressed, no gain once compressed) and Native AOT is closed by Windows Forms, WMI and the RDP ActiveX control, so neither is used. The CI comparison now inflates each bundled entry before masking the app assembly's build identifiers, identifies the file list by inflated size, kind and path, and blanks the header offset the apphost stores.
With --elevate and no administrator rights, the server no longer starts the elevated worker at launch. It serves the MCP session itself (initialize, tools/list and ping never prompt) and a tools/call filter (McpServerOptions.Filters.Request.CallToolFilters) forwards every call for a listed tool to a same-user worker that the first call starts through UAC. Concurrent first calls share that start, so a session shows one prompt; the worker answers every later call until the session ends. ElevationProxy starts the worker as the bridge did (runas, hidden, a CurrentUserOnly pipe, process IDs verified both ways, the environment header) and talks to it with an MCP client over the pipe, returning the worker's results unchanged. The link uses the initialize handshake so no per-request metadata reaches the client. A declined prompt returns an ElevationDeclined tool error and the next call prompts again; a worker that exits is reported once as ElevatedWorkerExited with its exit code, and the next call starts a new one. Client EOF, the end of the session or the parent's exit closes the pipe; the worker is killed after 5 s. HYPERV_CONTROL_WORKER=always is a test seam: the worker starts with the parent's own token, without UAC and without the forwarded variables, so they still arrive only through the header. The worker accepts a non-elevated token only when launched that way. hyperv_capabilities reports which process answered (process.id, elevated, mode). Unit checks cover the start info of both paths, a declined prompt and its retry, one prompt for concurrent calls, cancellation while the prompt is open and a worker that exits before connecting. The smoke phase drives the published EXE through the seam.
The permission model, the plugin README and the skill now describe the UAC prompt on the first tool call instead of at server start: one prompt per MCP session, a worker that lasts until the session ends, a declined prompt that fails only that call and prompts again on the next one, the ElevatedWorkerExited error, the process block of hyperv_capabilities, and the HYPERV_CONTROL_WORKER=always test seam. The skill tells agents to ask the user before retrying after ElevationDeclined, since each new call shows another prompt.
…ugin-vm-management-e74fbe
…g guidance The elevation bridge forwards HYPERV_CONTROL_* from the client's environment, so HYPERV_CONTROL_QEMU_IMG no longer has to be a persistent user or machine variable.
The Quick Create gallery's "Windows 11 dev environment" was flagged expired
by a date heuristic (built + 90 days), and the docs claimed it boots to a
black desktop and shuts down hourly. Neither was tested; on 2026-10-14 the
user verified that Hyper-V Quick Create still downloads it and the VM works.
Feed and recommendation:
- Drop both date-based expiry heuristics and the Expired flag (catalog
images and library entries; an old library.json "expired" is ignored).
- Windows evaluations (WinDev name/URI or evaluation wording) carry one
informational "Evaluation edition" warning with the slmgr /xpr advice.
Stale (3 years) and Ubuntu end of life are unchanged.
- recommended.windows is now the newest fetchable Windows image, so
Microsoft's Windows 11 dev environment; the hint drops the expiry clause.
Default image:
- HYPERV_CONTROL_DEFAULT_IMAGE names a library entry, else a catalog image
(exact name, case-insensitive); otherwise the recommended Windows catalog
image, or the newest Windows image fetched into the library when no
catalog offers one. A catalog image already held (same SHA-256, else same
name) resolves to the library entry.
- hyperv_image list adds defaultImage {name, catalog, inLibrary, source}.
- hyperv_image deploy without imageName uses it once it is in the library.
hyperv_vm_create:
- source defaults to image; imageName may name a catalog image; new
parameters catalog and waitSeconds (0-3600, default 60).
- A library image deploys in-call. A catalog image runs one job,
"quick create <vm>", that fetches it (joining a running fetch of the same
image instead of downloading twice) and then deploys it; the job's result
is the VM, and a still-running job comes back with next.
- The VM name rules, target disk, duplicate name and switch are checked
before any download (VmLifecycleService.CheckCreateAsync shares create's
guards). Parameters only other sources use are refused under
source=image; allowDuplicateName now reaches the image path.
- Fix image-build next steps that called hyperv_vm_create with vhdPath but
no source.
Tests: feed assertions, default image resolution with and without the
variable, the quick-create job end to end against a loopback HTTP fixture
and the mock module, catalog imageName, a joined fetch (one download),
refusals before any job, CheckCreateAsync, and smoke checks for the new
default. Live phases pass source=new_disk explicitly.
images.md: replace the untested "expired evaluation" row and the "no current Windows image" sentence with the evaluation-edition guidance, add a Default image section (HYPERV_CONTROL_DEFAULT_IMAGE, the recommended Windows image, the library fallback, defaultImage, the quick-create job, joined fetches, sizes and the confirm-before-download rule). SKILL.md routes Windows lab VM creation to hyperv_vm_create without source and tells the agent to confirm before the first multi-GB download; operations.md describes source=image as the create default; the plugin README mentions one-call Windows VMs without versions or dates.
…ugin-vm-management-e74fbe # Conflicts: # tests/HyperVControl.Tests/Program.cs
… the compressed bundle The executable now prompts for UAC on the first tool call, creates VMs from Microsoft's Windows 11 dev environment by default and ships as a 53 MB compressed bundle, so the unpublished 0.2.0 pin moves to the new hash. The validation report gains a follow-up section with what was measured and what the seam cannot prove: the real UAC prompt and the Windows image through the plugin's own fetch and deploy.
A linked clone and a differencing hyperv_vhd demanded the parent's whole virtual size free on the target volume. A differencing child starts near empty and grows as the guest writes, so the rule refused clones on small volumes: CI's runner (about 14 GB free against the mock's 64 GB parent), and it would have refused the 44.8 GB Windows 11 dev environment on most lab volumes. Both paths now need 1 GB and warn when the volume could not hold the parent's size. Runtime 0.2.0 is re-pinned to the new build.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the approved plan for Hyper-V Control: a focused refactor of the VM-management layer, VM lifecycle v2, an image catalog and library with background jobs, golden image builds, and runtime 0.2.0. Three follow-ups from the architecture review are included: UAC on the first tool call, Quick Create's Windows 11 dev environment as the default image, and a compressed bundle.
Refactor (no contract change)
HyperVScript: one hardened module bootstrap shared with the typedHyperVService, in-script VM resolution by exact name or GUID (one PowerShell process per tool call instead of two), state assertions, typed parameter rendering with literal quoting, and one failure envelope{ ok:false, error, errorType, errorId, category }classified by error ID, never by localized text.VmLifecycleService; tool methods parse, validate and call a service.HYPERV_CONTROL_MODULE_PATH) lets the unit phase assert on every cmdlet and parameter without a Hyper-V host.HYPERV_CONTROL_*,HYPERV_GUEST_*and_NT_SYMBOL_PATHto the UAC worker; a UAC launch otherwise drops the client's process environment, so every documented override only worked at user level.Lifecycle v2
hyperv_vm_createsources: image (default), new disk, ISO, existing disk, linked (differencing), none; defaults from the host; Secure Boot template, vTPM, static memory, Standard checkpoints, Default Switch by ID; duplicate names, existing disks and free space checked before any change; rollback on failure.VmStateRules: a verified setting × state table drivesconfigure,networkanddisk; Running VMs get what Hyper-V allows and a precise refusal that names what needs Off.hyperv_vm_removewithdryRunand guardeddeleteDisks(checkpoints removed and merges awaited first; shared disks, differencing parents and ISOs kept and reported).hyperv_firmware(template, Secure Boot, vTPM, first boot device), newhyperv_vhdandhyperv_vm_clone(linked in seconds; full as a job), adapter add/remove/set and NAT switches, richerhyperv_vm_infoandhyperv_capabilities.Images and jobs
hyperv_image: Microsoft's Quick Create gallery and custom catalogs (lenient JSON), a local library, resumable fetch with SHA-256 and magic-byte checks as ahyperv_jobsjob, register/remove with in-use guards, anddeploythrough the one create path.hyperv_vm_create name=Xdeploys a linked clone of the default image, Microsoft's Windows 11 dev environment (orHYPERV_CONTROL_DEFAULT_IMAGE). When the library lacks it, one "quick create" job fetches it and creates the VM; the name, switch and target disk are checked before the download, a running fetch is joined, and parameters of other sources are refused rather than ignored. The date-based "expired" heuristic for Windows evaluation images is gone; they carry an evaluation warning instead.hyperv_image_build: a Windows ISO or evaluation media applied offline to a GPT VHDX with an unattend (local admin, RDP, no device encryption, optional kernel-debug BCD settings), or an Ubuntu cloud image with a NoCloud seed.hyperv_jobsfor anything that outlives a call; export, import, full clone and quick create run on it.Elevation on the first tool call
With
--elevateand no administrator rights, the server answers initialize, tools/list and ping itself, so starting Claude Code never prompts. The first tool call shows one UAC prompt and starts a same-user elevated worker over the existing private pipe; an MCP client inside the server forwards every tool call to it through the SDK's call-tool filter until the session ends. Concurrent first calls share the prompt. Declining returnsElevationDeclinedand the next call prompts again; a worker that exits is reported once asElevatedWorkerExitedand replaced.HYPERV_CONTROL_WORKER=alwaysstarts the worker without UAC for tests; it then holds no rights the server lacks.Bundle size
EnableCompressionInSingleFiletakes the executable from 121 MB to 53 MB for about 50 ms more per process start. ReadyToRun (146 MB, no gain once compressed) and Native AOT (refused: Windows Forms needs trimming off; WMI, the RDP ActiveX control and reflection JSON add 89 IL2026/IL3050 warnings) were measured and rejected. The CI comparison inflates bundled entries before masking build identifiers and ignores the stored header offset.Verification
hyperv_vhddemanded the parent's full virtual size free; a differencing child now needs 1 GB and the result warns when the volume could not hold the parent's size.Export-VMfails on read-only differencing parents (now cleared for the export and restored), andNew-VMdefaults to dynamic memory and network-first boot.docs/validation/hyper-v-control/runtime-v0.2.0/report.md.Release
runtime.jsonpinshyper-v-control-v0.2.0; the asset is not published yet, so CI's comparison step reports a notice. After merge: taghyper-v-control-v0.2.0, publish the four files frombuild/hyperv-control/runtime-releasebuilt with SDK 10.0.401, then verify the pinned hash.🤖 Generated with Claude Code