Introduction
Accepted lab; v1 live acceptance complete The LabZ1 SQL/MECM/client baseline and one-device marker are accepted. The first exact-source run refreshed only stale authenticated evidence; the immediate second run skipped all five stages with zero action. Start with Current LabZ1 Status before operating the accepted environment.
setup-cm automates a repeatable, evidence-backed Microsoft Configuration Manager (MECM, formerly SCCM) primary-site deployment for an isolated lab.
It installs and validates a single Windows Server that hosts SQL Server, a MECM primary site, Management Point, and Distribution Point. The baseline also verifies that a dedicated test client can use the site.
Warning This repository is for lab automation, not production MECM deployment. Do not use its default topology, example configuration, or unattended workflow against production infrastructure.
What this documentation covers
This book is the complete operator and developer reference for setup-cm:
- Getting Started — what the tool does, what you need, and how to run your first deployment.
- Configuration — the YAML contract, field-by-field reference, and safety guardrails.
- Operations — the runbook, stage lifecycle, evidence interpretation, and recovery procedures.
- Architecture — why the tool is designed the way it is, and how the pieces fit together.
- Reference — command syntax, configuration schema, and evidence format details.
- Development — design documents, implementation plans, testing strategy, and handoff notes.
Where to start
If you are new to setup-cm, read the Overview & Quick Start first, then the Prerequisites, then the Configuration Reference. Once your configuration is validated, follow the Operator Runbook.
If you are contributing or extending the tool, start with Why setup-cm Exists and Design Principles, then review the Implementation Plans.
Overview & Quick Start
setup-cm treats a lab MECM deployment as a sequence of small, verifiable
stages. The stage engine supports Test, Apply, Verify, and structured evidence.
The complete release-candidate workflow now has real read-only probes and
bounded repair. The LabZ1 live gate is accepted; routine operation should use
its read-only restart procedure, while the
complete workflow remains available for proven owned-state repair.
Architecture at a glance
ProxmoxVEAutopilot / Autopilot Agent
└─ provisions the isolated, domain-joined Windows hosts
└─ setup-cm
Acquire → Sql → Mecm → Marker → Health
└─ run evidence in the configured evidence root
setup-cm owns Windows-side MECM configuration and validation. Virtual-machine lifecycle, operating-system deployment, networking, storage, and domain joining belong to the provisioning layer (for example, ProxmoxVEAutopilot).
Quick start
These steps describe a new isolated lab. They are not the restart procedure for the already accepted LabZ1 site.
-
Copy
config/lab.example.yamltoconfig/lab.local.yaml. The local file is ignored by Git. -
Replace every placeholder with your isolated-lab details, approved source location, byte length, SHA-256 checksum, version, architecture, and license acknowledgement. See the Configuration Reference.
-
Import the module and check readiness:
Import-Module ./src/SetupCm/SetupCm.psd1 -Force Test-SetupCmPreflight -ConfigPath ./config/lab.local.yaml -
Run the guided workflow. Marker-enabled runs require the full source commit:
pwsh ./scripts/Invoke-SetupCm.ps1 ` -ConfigPath ./config/lab.local.yaml ` -Mode Guided ` -SourceCommit '<FULL_40_CHARACTER_GIT_COMMIT>'
For unattended execution, stage private configuration separately, then set:
$env:SETUPCM_CONFIG = 'C:\ProgramData\SetupCm\config\lab.local.yaml'
$env:SETUPCM_SOURCE_COMMIT = '<FULL_40_CHARACTER_GIT_COMMIT>'
pwsh ./scripts/Invoke-SetupCm.ps1 -Mode Unattended
See the Operator Runbook before operating the workflow.
Stages
| Stage | Purpose |
|---|---|
Acquire | Obtains and verifies SQL Server and MECM installation media in the configured cache. |
Sql | Installs ODBC Driver 18, SQL Server prerequisites, SQL Server, and SQL network configuration. |
Mecm | Installs MECM prerequisites, ADK, Windows PE, and the primary site; it re-verifies ODBC Driver 18. |
Marker | Reconciles and verifies the fixed one-device LabZ1 marker deployment. |
Health | Rechecks SQL, MECM, Management Point, Distribution Point, and active-client state without repair. |
Each selected stage records a JSON result named stage-<stage>.json in a
unique run directory beneath evidenceRoot. Exact compliance is Skipped
with no Apply; conflicts stop before mutation. Preserve the whole directory
when investigating or resuming a failed run.
Prerequisites
Before running setup-cm, ensure the following are in place.
Environment
- An isolated, domain-joined lab with a Windows Server host and a separate test client.
- PowerShell 7.0 or later on the server for SetupCm operations.
- PowerShell 7.4 or later on hosts that run the Pester 6 test suite.
- Git for Windows with
git.exeonPATH, used to verify the commit embedded in the reviewed source archive before extraction. - The
powershell-yamlmodule to read the YAML configuration. - Approved SQL Server, MECM, Windows ADK, Windows PE add-on, ODBC Driver 18, and VC++ x64/x86 media, checksums, and accepted licenses.
- Sufficient local disk space for the configured cache, SQL installation directory, MECM installation directory, and evidence.
Install dependencies locally
Run these test-dependency commands from PowerShell 7.4 or later. Pester 6 also supports Windows PowerShell 5.1, but this repository’s module requires PowerShell 7.
Install-Module Pester -RequiredVersion 6.0.0 -Scope CurrentUser
Install-Module powershell-yaml -RequiredVersion 0.4.12 -Scope CurrentUser
Media and licensing
MECM, SQL Server, and related installers can be licensed, authenticated, customer-specific, large, or subject to vendor distribution rules. The repository stores only source metadata, integrity values, versions, and an explicit license acknowledgement. Operators keep actual media in an approved vendor location, private installer vault, or local cache.
The same separation applies to product keys, credentials, certificates, and generated evidence that may contain environment-specific information.
Safety guardrail
Set safety.isolatedLab: true for the intended use case. If it is false, safety.allowProductionTarget must be explicitly true or configuration validation stops the run. That explicit acknowledgement is a guardrail, not approval to use this project for production deployment.
Getting Media
setup-cm does not download licensed Microsoft media automatically. The operator must obtain each installer, verify its integrity, and place it in the configured cache or private vault before the Acquire stage runs. This page walks through every required item.
Media checklist
| Media | Source | Public URL? | License required? |
|---|---|---|---|
| SQL Server 2022 | VLSC / Visual Studio Subscription / Evaluation Center | No (evaluation available) | Yes |
| MECM Current Branch | VLSC / Visual Studio Subscription | No | Yes |
| Windows ADK | Microsoft Download Center | Yes | Accept EULA |
| Windows PE add-on | Microsoft Download Center | Yes | Accept EULA |
| ODBC Driver 18 | Microsoft Download Center | Yes | Accept EULA |
| VC++ Redistributable x64 | aka.ms | Yes | Accept EULA |
| VC++ Redistributable x86 | aka.ms | Yes | Accept EULA |
Public downloads
These items can be downloaded directly without a subscription.
Windows ADK and Windows PE add-on
The example pins version 10.1.26100.2454 (Windows 11 24H2 ADK).
# ADK
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=2289980" `
-OutFile "adksetup.exe"
# Windows PE add-on
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=2289981" `
-OutFile "adkwinpesetup.exe"
Note: The
fwlinkURLs above are Microsoft’s stable download links. They redirect to the current ADK build. If Microsoft releases a newer ADK, the link may serve a different version than the example pins. Verify the product version after download and updatelab.local.yamlif needed.
ODBC Driver 18 for SQL Server
Download from the Microsoft ODBC Driver 18 download page. The example pins 18.4.1.1 (x64 MSI).
Direct link pattern:
https://go.microsoft.com/fwlink/?linkid=2242886
Save as msodbcsql18-x64.msi.
VC++ Redistributables
Download both architectures from Microsoft’s evergreen links:
# x64
Invoke-WebRequest -Uri "https://aka.ms/vc14/vc_redist.x64.exe" `
-OutFile "vc_redist.x64.exe"
# x86
Invoke-WebRequest -Uri "https://aka.ms/vc14/vc_redist.x86.exe" `
-OutFile "vc_redist.x86.exe"
The example pins 14.44.35211.0. The minimum required by setup-cm is 14.34. The aka.ms links always serve the latest VC++ v14 build, so the version will advance over time. Record the actual version in your lab.local.yaml.
Subscription / VLSC downloads
These items require a Microsoft Volume License Service Center (VLSC) account, Visual Studio Subscription, or evaluation download.
SQL Server 2022
- Evaluation: SQL Server 2022 Evaluation Center — 180-day trial, ISO download.
- VLSC / Visual Studio Subscription: Download the ISO from your subscriber portal.
The example pins 16.0.1000.6 (SQL Server 2022 RTM). Save as sql-server.iso.
Important: The evaluation ISO is suitable for lab use. Do not use evaluation media for production.
MECM Current Branch
- Evaluation: Microsoft Configuration Manager Evaluation Center — 180-day trial.
- VLSC / Visual Studio Subscription: Download from your subscriber portal.
The example uses Current Branch 2509. Its signed
SMSSETUP\BIN\X64\setup.exe reports native ProductVersion
5.00.9141.1002; record that value in sources.mecm.version and save the ISO
as mecm-current-branch-2509.iso. A branch label such as 2509 is not a file
version and fails configuration validation.
Important: MECM evaluation media is lab-appropriate. The
productId: Evalsetting in the example configuration matches evaluation media.
Verifying downloads
Every source entry in lab.local.yaml requires a SHA-256 checksum. Compute it after downloading.
Windows (PowerShell)
Get-FileHash -Path "C:\Downloads\sql-server.iso" -Algorithm SHA256
macOS / Linux
shasum -a 256 sql-server.iso
Record the 64-character hexadecimal string in the sha256 field. The Acquire stage will reject the file if the hash does not match exactly.
Placing media
You have two options for each installer:
Option A: Local cache (recommended for public downloads)
Place the file directly in the configured cacheRoot with the filename matching cacheFile:
cacheRoot: C:\ProgramData\SetupCm\cache
sources:
adk:
cacheFile: adksetup.exe
# ...
So the ADK installer goes to:
C:\ProgramData\SetupCm\cache\adksetup.exe
The Acquire stage finds the cached file, verifies its SHA-256, and skips the download.
Option B: Private vault / file share (recommended for licensed media)
Place licensed ISOs on a secure file share or vault that the lab server can reach:
sources:
sqlServer:
uri: \\LAB-FS01\Vault\SQLServer\sql-server.iso
# or
uri: https://installer-vault.internal/sql-server.iso
# ...
The Acquire stage downloads from the URI to the local cache, then verifies integrity.
Security: Never commit actual installer files, product keys, or internal vault paths to Git. The
lab.local.yamlfile is ignored by Git for this reason.
License acceptance
Before the Acquire stage runs, every source must have licenseAccepted: true in lab.local.yaml. This is an explicit operator acknowledgement that you have accepted the applicable license terms.
sources:
sqlServer:
licenseAccepted: true # You accept the SQL Server license terms
mecm:
licenseAccepted: true # You accept the MECM license terms
# ... repeat for all sources
Preflight validation (Test-SetupCmPreflight) will report any source where licenseAccepted is still false.
Complete acquisition checklist
Before running Invoke-SetupCm:
- SQL Server 2022 ISO downloaded, SHA-256 recorded, placed in cache/vault
- MECM Current Branch ISO downloaded, SHA-256 recorded, placed in cache/vault
- ADK downloaded, SHA-256 recorded, placed in cache
- Windows PE add-on downloaded, SHA-256 recorded, placed in cache
- ODBC Driver 18 MSI downloaded, SHA-256 recorded, placed in cache
- VC++ x64 downloaded, SHA-256 recorded, placed in cache
- VC++ x86 downloaded, SHA-256 recorded, placed in cache
- All
licenseAcceptedfields set totrueinlab.local.yaml - All
sha256fields updated with actual computed hashes -
Test-SetupCmPreflightreturnsReady: True
Configuration Reference
Start with config/lab.example.yaml and save the working copy as
config/lab.local.yaml. The latter is ignored by Git. Keep the private file
outside any source archive used for review or acceptance, stage it separately
on the server, and restrict its file permissions. Do not commit a working
configuration that contains internal hostnames, vault paths, credentials,
product keys, or other environment-specific data.
Top-level settings
| Setting | Purpose |
|---|---|
topology | Deployment shape. The supplied template uses single-box. |
template | Keeps placeholder values valid in the committed example. Remove it or set it to false in a real configuration. |
lab | Identifies the lab name and domain. |
safety | Records whether the target is isolated and whether production targeting was explicitly acknowledged. |
evidenceRoot | Stores one sanitized directory per run; keep it outside the repository. |
cacheRoot | Stores verified installer artifacts; keep it outside the repository. |
sources | Declares each approved installer and its integrity metadata. |
sql | Configures the SQL Server instance, service identity, administrators, and install directory. |
mecm | Configures the primary-site identity, SQL endpoint, site server, product identifier, and install paths. |
testClient | Identifies the client used by the health stage. |
markerAcceptance | Enables the fixed, lab-only marker boundary; disabled in the template. |
Safety
Set safety.isolatedLab: true for the intended use case. If it is false, safety.allowProductionTarget must be explicitly true or configuration validation stops the run. That explicit acknowledgement is a guardrail, not approval to use this project for production deployment.
evidenceRoot is where each execution creates a unique run directory. cacheRoot is where verified installer media is retained. Place both on storage with enough space and appropriate access control. They should not be inside the repository.
Private inputs and source revision
The working YAML and installer sources are runtime inputs, not release
artifacts. Do not include them in git archive, commits, evidence bundles, or
PR output. Set only the private configuration path in the environment:
$env:SETUPCM_CONFIG = 'C:\ProgramData\SetupCm\config\lab.local.yaml'
Any run containing Marker also requires the exact full commit used to build
the staged source archive:
$env:SETUPCM_SOURCE_COMMIT = '<FULL_40_CHARACTER_GIT_COMMIT>'
Abbreviated or missing commits fail before evidence creation or provider
mutation. The validated lowercase value is recorded in run.json and
marker-state.json; it does not belong in the YAML.
Installer sources
The template includes sqlServer, mecm, adk, adkWinPe, odbcDriver18, vcRedistX64, and vcRedistX86. For each source, provide:
| Field | Purpose |
|---|---|
uri | Approved vendor or private-vault location for the installer or media. |
sha256 | Expected SHA-256 checksum for the downloaded artifact. |
publisher | Expected publisher for signature validation where applicable. |
version | The approved installer version. |
architecture | Approved target architecture: x64, x86, or neutral. |
sizeBytes | Exact approved artifact length in bytes. |
licenseAccepted | An explicit acknowledgement that the operator has accepted the applicable license. |
cacheFile | Filename used under cacheRoot. |
signatureRelativePath | Required for ISO media: executable inside the ISO whose signature and native identity are checked. |
architectureRelativePath | Optional ISO-only executable whose separate valid publisher signature and PE header prove the declared payload architecture. Do not combine it with architectureVerification. |
architectureVerification | Optional executable-bootstrapper mode. The only accepted value is signedVersionResource, which requires the signed original filename and signed product identity to name the declared architecture. Do not combine it with architectureRelativePath. |
For signed executables and ISO media, version is the native ProductVersion
of the file selected by signatureRelativePath. Human release labels are not
interchangeable with that value: the accepted Current Branch 2509 media uses
5.00.9141.1002, not 2509.
Some signed installers use an x86 launcher for an x64 payload. Keep the target
architecture declared as x64: SQL Server media uses the separately signed
x64\ScenarioEngine.exe payload as architectureRelativePath, while the x64
VC++ redistributable uses architectureVerification: signedVersionResource.
The latter is accepted only when Authenticode is valid and both the signed
original filename and signed product name or description unambiguously name
x64; a generic x86 bootstrapper remains a conflict. Architecture proofs may
only be selected for sources declared as x64 or x86.
Replace all REPLACE_WITH_* values and example URLs before a real run. Every
non-template source requires an exact SHA-256, positive sizeBytes, expected
publisher, native product version, architecture, and cache filename. SQL Server
and MECM must also have a usable approved URI or vault location when the cache
is absent. Source URIs and vault paths are never copied into evidence.
The SQL stage uses sources.odbcDriver18 to bootstrap Microsoft ODBC Driver 18
before its first database query when the driver is absent. The MECM stage
re-verifies the same prerequisite and uses the same installer path; it does not
maintain a second copy or provider implementation.
Marker acceptance
markerAcceptance.enabled defaults to false. When enabled, configuration
validation requires safety.isolatedLab: true, labOnly: true, site LAB,
server LABZ1-CM01.test.gell.one, target
RING0IVY24-01.test.gell.one, and resource ID 16777219. Any mismatch fails
before a provider mutation. The Marker stage adds the remaining object, hash,
collection, and assignment gates.
The fixed feature owns only Setup-CM Phase 1 Marker, deployment type
Install Setup-CM Phase 1 Marker, and collection
Setup-CM Phase 1 Marker - RING0IVY24-01 Only. The collection must contain one
direct member with resource ID 16777219, no other rule, and no marker
assignment may target another collection. The payload marker SHA-256 is
3F44AA70B40C9E9095E69F1C57E98F6ACC06900788A2054E251BCC58179B6254.
Conflicting same-name objects, broader membership, another assignment, or
payload/detector drift fails closed; the command does not delete historical
objects to resolve a conflict.
The evidence channel is code-fixed and has no YAML override. It is the hidden
share SetupCmMarkerEvidence$ on
C:\ProgramData\SetupCm\MarkerEvidence\RING0IVY24-01, exposed only to
TEST\RING0IVY24-01$ (Change) and BUILTIN\Administrators (Full). NTFS
inheritance is disabled; Administrators and SYSTEM retain full control, while
the target computer receives only the exact directory/file rights required to
publish marker-evidence.json. Any broad, inherited, unknown, excessive, or
wrong-path access is a conflict rather than a configuration value to relax.
The published record is limited to 2,048 ASCII-compatible bytes and exactly six
properties: schemaVersion, computerName, markerPath, markerSha256,
markerLength, and verificationMethod. It has no client timestamp. CM01’s
file receipt time is authoritative, must not be more than two minutes in the
future, and is fresh for 30 minutes. The successful verification routes are
DirectAuthenticatedFileRead and DirectAuthenticatedClientEvidence; a
ConfigMgr server projection alone is never direct marker proof.
SQL and MECM settings
Use Windows identities for sql.sysAdminAccounts; at least one is required. Confirm that sql.installDirectory, mecm.smsInstallDir, and mecm.prerequisitePath exist on adequately sized storage before starting.
Set mecm.siteCode, mecm.siteName, mecm.sqlServer, and mecm.siteServerFqdn to match the isolated lab. The testClient name and domain must identify a separate client that can be used to validate the completed site.
Validate before installing
After saving the local configuration, run:
Import-Module ./src/SetupCm/SetupCm.psd1 -Force
Test-SetupCmPreflight -ConfigPath ./config/lab.local.yaml
Resolve every item in Missing before proceeding. Preflight confirms the required SQL Server and MECM license acknowledgements and that each has a configured URI, vault location, or cached file. The full workflow also requires the ADK, Windows PE add-on, ODBC Driver 18, and both VC++ redistributable entries.
lab.example.yaml Annotated
The committed config/lab.example.yaml is a complete, safe template. Every value that must be replaced is marked REPLACE_WITH_* or uses a placeholder URL.
topology: single-box
template: true
lab:
name: lab
domain: lab.local
safety:
isolatedLab: true
allowProductionTarget: false
evidenceRoot: C:\ProgramData\SetupCm\artifacts
cacheRoot: C:\ProgramData\SetupCm\cache
sources:
sqlServer:
uri: https://installer-vault.example.invalid/sql-server.iso
sha256: REPLACE_WITH_SHA256
publisher: Microsoft Corporation
signatureRelativePath: setup.exe
architectureRelativePath: x64\ScenarioEngine.exe
version: "16.0.1000.6"
architecture: x64
sizeBytes: REPLACE_WITH_SIZE_BYTES
licenseAccepted: false
cacheFile: sql-server.iso
mecm:
uri: https://installer-vault.example.invalid/mecm.iso
sha256: REPLACE_WITH_SHA256
publisher: Microsoft Corporation
signatureRelativePath: SMSSETUP\BIN\X64\setup.exe
version: "5.00.9141.1002"
architecture: x64
sizeBytes: REPLACE_WITH_SIZE_BYTES
licenseAccepted: false
cacheFile: mecm-current-branch-2509.iso
adk:
uri: https://go.microsoft.com/fwlink/?linkid=2289980
sha256: REPLACE_WITH_SHA256
publisher: Microsoft Corporation
version: "10.1.26100.2454"
architecture: neutral
sizeBytes: REPLACE_WITH_SIZE_BYTES
licenseAccepted: false
cacheFile: adksetup.exe
adkWinPe:
uri: https://go.microsoft.com/fwlink/?linkid=2289981
sha256: REPLACE_WITH_SHA256
publisher: Microsoft Corporation
version: "10.1.26100.2454"
architecture: neutral
sizeBytes: REPLACE_WITH_SIZE_BYTES
licenseAccepted: false
cacheFile: adkwinpesetup.exe
odbcDriver18:
uri: https://installer-vault.example.invalid/msodbcsql18-x64.msi
sha256: REPLACE_WITH_SHA256
publisher: Microsoft Corporation
version: "18.4.1.1"
architecture: x64
sizeBytes: REPLACE_WITH_SIZE_BYTES
licenseAccepted: false
cacheFile: msodbcsql18-x64.msi
vcRedistX64:
uri: https://aka.ms/vc14/vc_redist.x64.exe
sha256: REPLACE_WITH_SHA256
publisher: Microsoft Corporation
version: "14.44.35211.0"
architecture: x64
architectureVerification: signedVersionResource
sizeBytes: REPLACE_WITH_SIZE_BYTES
licenseAccepted: false
cacheFile: vc_redist.x64.exe
vcRedistX86:
uri: https://aka.ms/vc14/vc_redist.x86.exe
sha256: REPLACE_WITH_SHA256
publisher: Microsoft Corporation
version: "14.44.35211.0"
architecture: x86
sizeBytes: REPLACE_WITH_SIZE_BYTES
licenseAccepted: false
cacheFile: vc_redist.x86.exe
prerequisites: []
sql:
instanceName: MSSQLSERVER
serviceAccount: LAB\svc_sql
sysAdminAccounts:
- LAB\CMSetupAdmins
installDirectory: D:\SQL
mecm:
siteCode: LAB
siteName: Lab Primary
sqlServer: CM01.lab.local
siteServerFqdn: CM01.lab.local
productId: Eval
smsInstallDir: D:\ConfigMgr
prerequisitePath: D:\Sources\Redist
testClient:
name: CMCLIENT01
domain: lab.local
markerAcceptance:
enabled: false
labOnly: true
siteCode: LAB
siteServerFqdn: LABZ1-CM01.test.gell.one
targetFqdn: RING0IVY24-01.test.gell.one
targetResourceId: 16777219
Key annotations
template: true— keeps the committed example valid even with placeholders. Set tofalseor remove in a real configuration.safety.isolatedLab: true— required for the intended lab use case.licenseAccepted: false— you must explicitly set this totrueafter accepting the license terms for each installer.REPLACE_WITH_SHA256— must be replaced with the actual SHA-256 checksum of your approved media.REPLACE_WITH_SIZE_BYTES— must be replaced with the exact approved byte length.evidenceRootandcacheRoot— should be on local, adequately sized storage outside the repository.markerAcceptance.enabled: false— the template cannot deploy the marker. Enabling it requires the fixed LabZ1 identities shown and a full source commit supplied at runtime.
Version guidance
The example pins the following versions. Update them to match your approved media:
| Source | Example version | Notes |
|---|---|---|
| SQL Server | 16.0.1000.6 | SQL Server 2022 RTM |
| MECM | 5.00.9141.1002 | Native setup.exe ProductVersion for the accepted Current Branch 2509 media |
| ADK / WinPE | 10.1.26100.2454 | Windows 11 24H2 ADK |
| ODBC Driver 18 | 18.4.1.1 | Microsoft ODBC Driver 18 for SQL Server |
| VC++ Redist | 14.44.35211.0 | VC++ v14 latest; minimum 14.34 required |
Current LabZ1 Status
The LabZ1 baseline and one-device marker deployment are accepted. All five v1 stages have real desired-state probes, and applicable mutating stages have bounded repair. The reviewed first exact-source run refreshed only stale authenticated evidence; the immediate second run skipped all five stages with zero action. Neither run invoked an installer or changed a ConfigMgr object.
Accepted current state
| Role | Identity |
|---|---|
| MECM server, provider, MP, and DP | LABZ1-CM01.test.gell.one |
| Site and database | LAB / CM_LAB |
| Only accepted client | RING0IVY24-01.test.gell.one |
| Marker application | Setup-CM Phase 1 Marker |
The older LABZ1-CMCLIENT01 plan is obsolete for current acceptance. Typed
client-install Task 5 completed through the signed Autopilot Agent path.
Accepted evidence
- The repository root’s
docs/PHASE0-2026-08-29-LAB-INVENTORY.mdis the authoritative baseline and safe-restart record. docs/PHASE1-2026-08-29-MARKER-DEPLOYMENT.mdproves the required marker is compliant exclusively onRING0IVY24-01.docs/V1-ACCEPTANCE-2026-08-30.mdfixes the live-tested source, native and provider gate counts, two run IDs, and all acceptance hashes.docs/HANDOFF-2026-08-02-agent-mecm-client-install.mdis preserved and prominently marked as superseded historical context.
Safe restart
Confirm the LabZ1 resources, controller, Agents, provider, SQL inventory, and
one-device marker collection through deterministic remote checks. On the
currently accepted source, run only Health unless a specific missing
component is proven. Do not replay the full bootstrap solely for new
timestamps. The full Acquire, SQL, MECM, Marker, and Health workflow is now an
accepted no-op path from exact reviewed source, but routine validation should
remain the smaller read-only Health command.
Live-tested commit 33535c6a0e47bb0bb0f838eb990b0e9e9cb2ac95 passed 12
applicable CM01 Windows tests with five client-only skips, all 10
detector/publication tests on RING0IVY24-01, and the read-only post-migration
provider test. Formal run 20260830-203712-76f13d67 refreshed only the stale
authenticated receipt; run 20260830-204052-d3c589a6 then skipped all five
stages with zero action. Each produced 11 sanitized artifacts with all six
Health checks true. The marker evidence directory contains only the exact
target-owned receipt. See the v1 acceptance record for
the hashes and BITS ambient-state note.
Production, co-management, Patch My PC, reporting expansion, distributed roles, extra clients, tenant integrations, and client-wide security-policy changes are outside v1.
v1 Hands-Off Rerun Acceptance
Operator date: 2026-08-30
Status: Accepted — the exact release-candidate implementation passed the complete LabZ1 workflow twice without installation or ConfigMgr object change; the first run refreshed only stale authenticated evidence and the immediate second run was zero-action.
This record closes the live gate for the first setup-cm release. It records
only sanitized identities, hashes, test counts, and state results. It contains
no credentials, tokens, private URLs, generated configuration, raw policies,
or log bodies.
Accepted scope
| Role | Accepted identity |
|---|---|
| MECM server, provider, MP, and DP | LABZ1-CM01.test.gell.one (VM 107) |
| Site and database | LAB / CM_LAB |
| Only accepted client | RING0IVY24-01.test.gell.one (VM 135) |
| Required application | Setup-CM Phase 1 Marker |
| Controller | autopilot-docker (CT 500) |
The marker remains a required deployment to one direct member only. No other device or collection is an accepted v1 target. Production, additional clients, distributed MECM, co-management, Patch My PC, reporting expansion, tenant integrations, VM rebuilds, and client-wide security-policy changes remain outside this release.
Exact execution source
| Evidence | Accepted value |
|---|---|
| Pull request | #5 |
| Branch | codex/hands-off-rerun-v1 |
| Live-tested commit | 33535c6a0e47bb0bb0f838eb990b0e9e9cb2ac95 |
| Live-tested tree | b0d1329b1fb7fa8479b0b0754cbe168f60fa702b |
| Git archive size | 1,075,200 bytes |
| Git archive SHA-256 | 85c4efb0c36a3575227a078164e35a87e71f777e6bf913e560181779a7b65062 |
| Embedded archive commit | 33535c6a0e47bb0bb0f838eb990b0e9e9cb2ac95 |
| Extracted 96-file manifest SHA-256 | 5DCC6E72371F3797F2DF34E6CFAA978F70DBC46E8B80FC2B12D2C9B84354E8DD |
| Read-only acquisition evidence SHA-256 | 10078B52E8002429CB6BAB35D5F4C6B2649A1606F0BE0F7E23CE11CE03A787C8 |
| Ordered pre-run boundary SHA-256 | 5149AFDC75348A4EE28A98F8EB242993BAA0FC62891B7750B1A7C1E06B378727 |
The archive was generated with git archive, transferred through a temporary
lab-only listener, checked by SHA-256 on both Windows targets, and checked on
CM01 with git get-tar-commit-id before extraction. The listener was stopped
after staging.
The acceptance-record commit is documentation-only relative to the live-tested implementation. Before tagging, all non-documentation release paths must remain identical to this commit. Any executable, configuration, workflow, or test-tree difference requires renewed acceptance rather than a documentation exception.
Quality gates
| Gate | Result |
|---|---|
| Portable Pester suite | 391 passed, 0 failed, 0 skipped |
| PowerShell parse | 45 files, 0 errors |
| PSScriptAnalyzer error severity | 0 findings |
| Markdown links | 47 files and 97 local links passed |
| mdBook and workflow lint | passed |
| Final focused CodeRabbit review | 0 findings |
| CM01 native Windows integration | 12 passed, 0 failed, 5 client-only skips |
RING0IVY24-01 native Windows integration | 10 passed, 0 failed, 0 skipped |
| ConfigMgr provider post-migration integration | 1 passed, 0 failed, no mutation |
| PR Pester CI | passed at the final PR head before merge |
| PR mdBook build | passed at the final PR head before merge |
| PR CodeRabbit status | passed at the final PR head before merge |
The native evidence-channel tests include a real Windows proof that a newly created file inherits target-computer Modify rights. This test caught and closed the earlier directory-only inheritance defect before formal acceptance.
Bounded repair lineage
The lab entered the final acceptance window with all installation and fixed
ConfigMgr object state valid. The authenticated receipt is deliberately fresh
for only 30 minutes, so the pre-run boundary reported exactly
Client/ClientEvidencePending; SQL, MECM, acquisition, marker objects, content,
distribution, targeting, and server compliance were already compliant.
The first run therefore permitted only RequestClientPolicy and
WaitForConvergence. It requested the supported paired client evaluation and
waited for a new target-owned receipt. No acquisition, installer, SQL/MECM
repair, application/content/distribution, collection, membership, assignment,
or detector mutation was permitted or observed.
An earlier candidate closed the stage-output contract defect. Final review then
found that the signed bootstrapper architecture exception was broader than its
known x86-PE/x64-product case and that Health used an empty repair path before
failing. Commit 33535c6a0e47bb0bb0f838eb990b0e9e9cb2ac95 narrows that
exception and makes Health a single check-only evaluation with named failed
checks. Both changes have failing-before and passing-after regression proof.
Exact marker and target identity
| Object | Accepted identity |
|---|---|
| Application CI / revision | 16777532 / 5 |
| Deployment type CI / revision | 16777533 / 4 |
| Package | LAB00008 |
| Collection | LAB00016 |
| Sole member resource | 16777219 (RING0IVY24-01) |
| Assignment / policy revision | 16777217 / 5 |
| Marker length | 78 bytes |
| Marker SHA-256 | 3F44AA70B40C9E9095E69F1C57E98F6ACC06900788A2054E251BCC58179B6254 |
| Authenticated receipt length | 254 bytes |
| Authenticated receipt SHA-256 | B21E6A787FBA6004D28C69094941F44FC123A835554F24971C565BA1615F1AB7 |
| Receipt owner | TEST\RING0IVY24-01$ |
Final inspection found exactly one item in the bounded evidence directory: the
accepted marker-evidence.json. The application, deployment type, package,
distribution, collection, direct membership, and assignment identities did
not change across either formal run.
Formal run one
| Evidence | Accepted value |
|---|---|
| Run ID | 20260830-203712-76f13d67 |
| Summary SHA-256 | 15D7C91447D18CA4A63D7A1F56774C8889DE9A211E192F1BAE164FBE6FBE7085 |
| Artifact count | 11 |
| Health SHA-256 | 2BA005D6D36572C1F864694B0227635F938C8AD7A27FB0EC4E8C2D50B9404593 |
| Stage states | Acquire, SQL, MECM, Health: Skipped; Marker: Succeeded |
| Allowed actions | RequestClientPolicy, WaitForConvergence |
| Object or installer mutations | 0 |
| Installer processes | 0 |
All six Health values were true: SQL, MECM, Management Point, Distribution Point, client reachability, and active non-obsolete client registration.
Formal run two
| Evidence | Accepted value |
|---|---|
| Run ID | 20260830-204052-d3c589a6 |
| Summary SHA-256 | 2938B54FAE5ED15B0AC720873F41AA48E434C05F2BD12AD64329DD59A332E66A |
| Artifact count | 11 |
| Health SHA-256 | 2BA005D6D36572C1F864694B0227635F938C8AD7A27FB0EC4E8C2D50B9404593 |
| Stage states | Acquire, SQL, MECM, Marker, Health: Skipped |
| Mutation actions | 0 |
| Installer processes | 0 |
The second run used the same command and exact source revision. Mutation mocks would have failed the run if acquisition, SQL repair, MECM repair, marker repair, content distribution, collection membership, deployment, policy, or installer paths had been invoked.
Independent before/after snapshots
| Snapshot | SHA-256 |
|---|---|
| Server baseline | C606C8DC3CF3B5C39F9B4C21DF0AA17BBA68CE01F2826F3257C1CD7DAA54860C |
| Server after run one | A7AC8CA3A172ED5D9B13CA4AD320F61EB1EDB147D087A4B1826D12DA5F740AC7 |
| Server after run two | B8B7A291384EE813340FEE9E21FF005956E8D1CE1073B941DCDA0DA38D337D40 |
| Client baseline | 4B0D5C1782377A8D3DE48CC2EA1F640191E718EEF0184A92CCD17FAC32DC6591 |
| Client after run one | 9E277628CDDF43EEF4B6A9450C40298753AD702C4FDABF21C7EEEC17491B360E |
| Client after run two | 955D56BC279AA6E5B9EE8B61F4FAB2C314D4D6062BE2C283CA2E066DAE061317 |
Snapshot hashes differ because each file includes its observation time. The comparison gate separately normalized and compared the owned values. Server application, deployment-type, distribution, collection, rule, membership, and assignment properties were identical. Client marker, Agent, firewall, execution-policy, trust-store, scheduled-task, and scoped service hashes were identical.
The formal snapshots hash all Windows service definitions except OS-managed
BITS and record BITS separately. The stable 296-service scoped SHA-256 was
682863423E9B331756C8025FDE4592A4E140F9480AA9587483FF105C975BEDB0;
BITS was Manual/Stopped at baseline and Auto/Running after the
bounded policy-refresh window and second run. No setup-cm-owned service value,
service definition in the scoped hash, or other client configuration changed.
Independent infrastructure and controller proof
- Proxmox node
pve2reported the two-node cluster quorate. - VMs 107, 111, 115, and 135 plus CT 500 were all running.
- Controller
/healthzreturned{"ok":true}. - Controller release
2026.08.48reported source8ba1c80. - Accepted Agents
agent-labz1-cm01/ VM 107 andagent-ring0ivy24-01/ VM 135 checked in currently at version2026.8.48.0and were not revoked. - A stale historical
osd-fulloscontroller identity also names CM01. It is not the accepted Agent and was left untouched because historical-object deletion is explicitly outside v1.
Evidence hygiene and cleanup
- Both accepted run directories parsed as JSON and produced zero sensitive keys, credentials, bearer values, private URLs, or raw policy patterns.
- A stalled direct SFTP attempt created only two exact zero-byte staging placeholders. The two transfer-only SFTP processes were stopped and those empty placeholders were removed before the proven temporary lab-only listener was used; no source had been extracted from them.
- The temporary listener was stopped immediately after both archive hashes
matched. The two verified staging archives and two generated
testResults.xmlfiles were removed after acceptance; both extracted source roots then matched the exact 96-file manifest again. - Earlier acceptance work removed two exact target-owned failed-attempt
.tmpreceipts, one rejected source-stager helper, three superseded client helpers, and hash-only BITS diagnostic state only after identity checks. All were temporary and regenerable. No user document, ConfigMgr object, source media, accepted evidence, or unrelated lab file was deleted.
Safe restart
Routine validation should stay read-only. From the accepted source root on CM01, run:
$commit = '33535c6a0e47bb0bb0f838eb990b0e9e9cb2ac95'
pwsh "C:\ProgramData\SetupCm\source\$commit\scripts\Invoke-SetupCm.ps1" `
-ConfigPath 'C:\ProgramData\SetupCm\config\lab.local.yaml' `
-Mode Unattended -Stage Health -SourceCommit $commit
Use the complete five-stage command only from an exact reviewed source when a full no-op confirmation or a proven owned-component repair is required. Do not replay bootstrap installers merely for newer timestamps, broaden the marker collection, weaken client policy, or delete historical objects as recovery.
Once published, the v1.0.0 tag and GitHub release will be the authoritative
publication identity for this accepted implementation. The release notes must
retain this topology, evidence boundary, test counts, limitations, and safe
restart rule.
Operator Runbook
Use this runbook only after a provisioning layer has created an isolated, domain-joined Windows Server and a separate test client. Read Overview & Quick Start and Configuration Reference first.
Current LabZ1 note: The accepted baseline is already installed. The first reviewed run refreshed only stale authenticated evidence; the immediate second run skipped all five stages with zero action. Routine validation should still use the smaller read-only restart command below. See v1 Hands-Off Acceptance.
Required boundary
| Role | Required identity |
|---|---|
| Server, provider, MP, and DP | LABZ1-CM01.test.gell.one |
| Site and database | LAB / CM_LAB |
| Only marker target | RING0IVY24-01.test.gell.one, resource 16777219 |
| Marker application | Setup-CM Phase 1 Marker |
Marker acceptance must be explicitly enabled and lab-only. A different host, site, resource, same-name object, collection rule, member, or assignment is a conflict and stops before mutation. Production, additional clients, broad collections, VM lifecycle, trust changes, and client-wide policy changes are outside this runbook.
Prepare the server
-
Install PowerShell 7.0 or later for SetupCm operations, plus Git for Windows and
powershell-yaml. Use PowerShell 7.4 or later on any host that will run the Pester 6 test suite:Install-Module powershell-yaml -RequiredVersion 0.4.12 -Scope CurrentUser Install-Module Pester -RequiredVersion 6.0.0 -Scope CurrentUserPowerShell 7.0 is the deliberate minimum.
ProcessStartInfo.ArgumentListis available in .NET Core 3.1, the archive verifier writes bytes directly to the child process stream, and acquisition uses the compatibleTimeoutSecon PowerShell 7.0-7.3 before switching to split timeouts on 7.4 and later. The pinned Pester 6.0.0 test dependency requires PowerShell 7.4 or later on PowerShell Core. Pester also supports Windows PowerShell 5.1, but this repository imports the PowerShell 7-only SetupCm module; its manifest value does not imply Pester support for Core 7.0-7.3. -
On the review host, create and hash a source archive from one exact commit:
$sourceCommit = (git rev-parse HEAD).Trim() if ($sourceCommit -notmatch '\A[0-9a-f]{40}\z') { throw 'Commit is not exact.' } $archive = "setup-cm-$sourceCommit.tar" git archive --format=tar --output=$archive $sourceCommit Get-FileHash -LiteralPath $archive -Algorithm SHA256Stage that archive on CM01 and verify its byte hash before extraction. Do not add generated or private files to it.
-
Stage the non-template configuration separately at a protected path such as
C:\ProgramData\SetupCm\config\lab.local.yaml. Never commit or copy it into evidence. From the SetupCm operator account in an elevated PowerShell session, replace inherited access with read-and-execute access for exactly that operator, Local System, and local Administrators, then verify the ACL:$configPath = 'C:\ProgramData\SetupCm\config\lab.local.yaml' if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { throw "Private configuration does not exist: $configPath" } $operatorSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User $allowedSids = @( [System.Security.Principal.SecurityIdentifier]::new('S-1-5-18') [System.Security.Principal.SecurityIdentifier]::new('S-1-5-32-544') $operatorSid ) $configAcl = Get-Acl -LiteralPath $configPath $configAcl.SetOwner($operatorSid) $configAcl.SetAccessRuleProtection($true, $false) foreach ($identity in @($configAcl.Access.IdentityReference | Sort-Object Value -Unique)) { $configAcl.PurgeAccessRules($identity) } foreach ($sid in $allowedSids) { $rule = [System.Security.AccessControl.FileSystemAccessRule]::new( $sid, [System.Security.AccessControl.FileSystemRights]::ReadAndExecute, [System.Security.AccessControl.AccessControlType]::Allow ) $configAcl.AddAccessRule($rule) | Out-Null } Set-Acl -LiteralPath $configPath -AclObject $configAcl $verifiedAcl = Get-Acl -LiteralPath $configPath $verifiedOwnerSid = $verifiedAcl.GetOwner( [System.Security.Principal.SecurityIdentifier] ).Value $verifiedRules = @($verifiedAcl.Access) $expectedSids = @($allowedSids | ForEach-Object Value | Sort-Object -Unique) $actualSids = @($verifiedRules | ForEach-Object { $_.IdentityReference.Translate( [System.Security.Principal.SecurityIdentifier] ).Value } | Sort-Object -Unique) $readExecuteMask = [int][System.Security.AccessControl.FileSystemRights]::ReadAndExecute $writeMask = [int]( [System.Security.AccessControl.FileSystemRights]::WriteData -bor [System.Security.AccessControl.FileSystemRights]::AppendData -bor [System.Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor [System.Security.AccessControl.FileSystemRights]::WriteAttributes -bor [System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor [System.Security.AccessControl.FileSystemRights]::Delete -bor [System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor [System.Security.AccessControl.FileSystemRights]::TakeOwnership ) $invalidRules = @($verifiedRules | Where-Object { $rights = [int]$_.FileSystemRights $_.IsInherited -or $_.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or ($rights -band $readExecuteMask) -ne $readExecuteMask -or ($rights -band $writeMask) -ne 0 }) if ($verifiedOwnerSid -ne $operatorSid.Value -or -not $verifiedAcl.AreAccessRulesProtected -or $invalidRules.Count -gt 0 -or @(Compare-Object -ReferenceObject $expectedSids -DifferenceObject $actualSids).Count -gt 0) { throw 'Private configuration ACL verification failed.' } -
Confirm the approved SQL, MECM, ADK, Windows PE, ODBC, and x64/x86 VC++ artifacts are present in the configured cache or available through the approved source policy.
-
Set the private path and exact archive commit recorded on the review host:
$env:SETUPCM_CONFIG = 'C:\ProgramData\SetupCm\config\lab.local.yaml' $env:SETUPCM_SOURCE_COMMIT = '<FULL_40_CHARACTER_GIT_COMMIT>' if ($env:SETUPCM_SOURCE_COMMIT -notmatch '\A[0-9a-fA-F]{40}\z') { throw 'SETUPCM_SOURCE_COMMIT must identify the exact staged commit.' }
Any run containing Marker rejects a missing or abbreviated source commit.
Extract and enter the reviewed source
On CM01, bind the staged archive to the exact commit and SHA-256 recorded on the review host, extract it into a new commit-specific directory, and verify the expected source layout before using any relative command:
if ([string]::IsNullOrWhiteSpace($env:SETUPCM_SOURCE_COMMIT) -or
$env:SETUPCM_SOURCE_COMMIT -notmatch '\A[0-9a-fA-F]{40}\z') {
throw 'SETUPCM_SOURCE_COMMIT must identify the exact staged commit.'
}
$archivePath = Join-Path 'C:\ProgramData\SetupCm\staging' `
"setup-cm-$($env:SETUPCM_SOURCE_COMMIT).tar"
$expectedArchiveHash = '<RECORDED_64_CHARACTER_SHA256>'
if ($expectedArchiveHash -notmatch '\A[0-9a-fA-F]{64}\z') {
throw 'The recorded archive SHA-256 is missing or invalid.'
}
$actualArchiveHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash
if ($actualArchiveHash -ine $expectedArchiveHash) {
throw 'The staged source archive hash does not match the reviewed archive.'
}
$git = Get-Command git.exe -ErrorAction Stop
# This writes bytes directly to the child process stream and does not depend on
# the native byte-pipeline behavior added after PowerShell 7.0.
$archiveHeader = [byte[]]::new(1024)
$archiveStream = [IO.File]::OpenRead($archivePath)
try {
$offset = 0
while ($offset -lt $archiveHeader.Length) {
$bytesRead = $archiveStream.Read(
$archiveHeader,
$offset,
$archiveHeader.Length - $offset
)
if ($bytesRead -eq 0) { throw 'The staged source archive has no complete tar header.' }
$offset += $bytesRead
}
}
finally {
$archiveStream.Dispose()
}
$gitStartInfo = [Diagnostics.ProcessStartInfo]::new()
$gitStartInfo.FileName = $git.Source
$gitStartInfo.ArgumentList.Add('get-tar-commit-id')
$gitStartInfo.UseShellExecute = $false
$gitStartInfo.RedirectStandardInput = $true
$gitStartInfo.RedirectStandardOutput = $true
$gitStartInfo.RedirectStandardError = $true
$gitProcess = [Diagnostics.Process]::new()
$gitProcess.StartInfo = $gitStartInfo
try {
if (-not $gitProcess.Start()) { throw 'Git archive identity verifier did not start.' }
$gitProcess.StandardInput.BaseStream.Write($archiveHeader, 0, $archiveHeader.Length)
$gitProcess.StandardInput.Close()
$embeddedArchiveCommit = $gitProcess.StandardOutput.ReadToEnd().Trim()
$gitError = $gitProcess.StandardError.ReadToEnd().Trim()
$gitProcess.WaitForExit()
if ($gitProcess.ExitCode -ne 0) {
throw "The staged archive has no readable git commit identity: $gitError"
}
}
finally {
$gitProcess.Dispose()
}
if ($embeddedArchiveCommit -notmatch '\A[0-9a-fA-F]{40}\z' -or
$embeddedArchiveCommit -ine $env:SETUPCM_SOURCE_COMMIT) {
throw 'The commit embedded in the staged archive does not match SETUPCM_SOURCE_COMMIT.'
}
$sourceRoot = Join-Path 'C:\ProgramData\SetupCm\source' $env:SETUPCM_SOURCE_COMMIT
if (Test-Path -LiteralPath $sourceRoot) {
throw "The commit-specific source directory already exists: $sourceRoot"
}
$sourceParent = Split-Path -Parent $sourceRoot
$temporarySourceRoot = "$sourceRoot.partial-$PID"
if (Test-Path -LiteralPath $temporarySourceRoot) {
throw "The temporary source directory already exists: $temporarySourceRoot"
}
New-Item -ItemType Directory -Path $sourceParent -Force | Out-Null
New-Item -ItemType Directory -Path $temporarySourceRoot | Out-Null
$requiredSourcePaths = @(
'./src/SetupCm/SetupCm.psd1'
'./scripts/Invoke-SetupCm.ps1'
'./tests/Unit'
'./docs/gitbook/book.toml'
)
try {
tar.exe -xf $archivePath -C $temporarySourceRoot
if ($LASTEXITCODE -ne 0) { throw 'Source archive extraction failed.' }
$missingSourcePaths = @($requiredSourcePaths | Where-Object {
-not (Test-Path -LiteralPath (Join-Path $temporarySourceRoot $_))
})
if ($missingSourcePaths.Count -gt 0) {
throw "The extracted source root is incomplete: $($missingSourcePaths -join ', ')"
}
Move-Item -LiteralPath $temporarySourceRoot -Destination $sourceRoot
}
catch {
Remove-Item -LiteralPath $temporarySourceRoot -Recurse -Force -ErrorAction SilentlyContinue
throw
}
Set-Location -LiteralPath $sourceRoot
Run preflight
Import-Module ./src/SetupCm/SetupCm.psd1 -Force
Test-SetupCmPreflight -ConfigPath $env:SETUPCM_CONFIG
Continue only when Ready is True.
Read-only accepted-lab restart
pwsh ./scripts/Invoke-SetupCm.ps1 `
-ConfigPath $env:SETUPCM_CONFIG `
-Mode Unattended `
-Stage Health
Health reads SQL, MECM, MP, DP, and active-client state, writes a fresh
health.json, and has no repair action.
Run the complete workflow
pwsh ./scripts/Invoke-SetupCm.ps1 `
-ConfigPath $env:SETUPCM_CONFIG `
-Mode Unattended `
-Stage Acquire,Sql,Mecm,Marker,Health `
-SourceCommit $env:SETUPCM_SOURCE_COMMIT
When marker acceptance is enabled, lab-only, fixed to the validated LabZ1
identities, and supplied a full source commit, omitting -Stage selects the
same five stages in that order. A marker-disabled configuration omits Marker;
an enabled marker configuration without the commit fails before stage
execution. Keep -Stage explicit for release acceptance and recovery evidence.
The first accepted run may repair only proven missing state. Run the identical
command from the identical commit a second time. The second run must report all
five stages Skipped, execute no installer, redistribute no content, and
create no ConfigMgr object, assignment, or membership rule.
To evaluate only the bounded marker feature:
pwsh ./scripts/Invoke-SetupCmMarkerAcceptance.ps1 `
-ConfigPath $env:SETUPCM_CONFIG `
-SourceCommit $env:SETUPCM_SOURCE_COMMIT
Operate the authenticated marker evidence channel
Run live SQL, SMS provider, and Marker acceptance from CM01 as the authorized domain operator in an elevated PowerShell session. A successful SSH connection as CM01’s local Administrator is not an equivalent acceptance context because that identity may not have site-database or secured-provider access.
The Marker channel is fixed in code:
| Item | Exact contract |
|---|---|
| Hidden share | SetupCmMarkerEvidence$ on LABZ1-CM01.test.gell.one |
| Local target | C:\ProgramData\SetupCm\MarkerEvidence\RING0IVY24-01 |
| Final file | marker-evidence.json, maximum 2,048 bytes |
| Only client writer | TEST\RING0IVY24-01$ |
| Schema | Six exact fields, version 1, ASCII-compatible JSON |
| Freshness | 30 minutes, with at most two minutes of future clock tolerance |
The provider first attempts an authenticated direct C$ marker read. A
successful read is authoritative. Only when that transport is unavailable does
it accept the locally received, target-computer-owned evidence file through the
DirectAuthenticatedClientEvidence route. Missing or stale otherwise valid
evidence is repairable pending state. Malformed, foreign-owned, future-dated,
wrong-identity, wrong-path, wrong-hash, or contradictory evidence is a conflict.
On the accepted pre-migration LabZ1 state, the only permitted first-run actions are, in order:
- Create or safely complete the exact protected directory and hidden share.
- Replace only the exact approved predecessor detector through
Set-CMScriptDeploymentType; do not alter content or runtime properties. - Wait up to five minutes for the current application and assignment policy revisions/scope to agree and remain observed unchanged for 60 seconds. Request machine policy once, wait a 30-second processing interval, then request application evaluation once.
- Poll read-only state every 15 seconds for up to 15 minutes.
Each publication snapshot runs in a disposable read-only PowerShell process bounded by the lesser of 90 seconds and the time remaining in that one five-minute deadline; a hung ConfigMgr/CIM query is terminated and fails the run before notification. The publication wait also fails before notification if assignment scope or object cardinality changes. The UTC timestamp captured immediately after the one machine-policy request completes becomes the minimum acceptable CM01 receipt time. The 30-second interval is sequencing margin, not evidence of client policy receipt; only authenticated detector evidence and matching current client/server revision can complete convergence. Conflict stops immediately. Timeout fails with the final component state and does not make a second mutation attempt. Detector-only migration must not copy content, redistribute the application, recreate objects, change membership, or replace the assignment.
Run the identical five-stage command immediately afterward, while the first
record is still fresh. That release-acceptance second run must call no mutation
or side-effect adapter, including no policy/evaluation request, and all five
stages must be Skipped. A later routine run after the 30-minute freshness
window may make one bounded policy/evaluation request and wait for refreshed
evidence; that later refresh is not the v1 zero-action second-run proof and must
still cause no infrastructure or ConfigMgr object churn.
For a read-only provider gate, use a fresh PowerShell process and select the state expected at that point:
$env:SETUPCM_LAB_PROVIDER_INTEGRATION = '1'
$env:SETUPCM_MARKER_PROVIDER_MODE = 'PreMigration' # or PostMigration
Invoke-Pester ./tests/Integration/MarkerAcceptance.Provider.Tests.ps1 `
-Output Detailed -CI
PreMigration requires exactly the three repairable states listed above.
PostMigration requires every component compliant and installs throwing
adapters for every mutation and side effect, so any attempted action fails the
test.
Understand stage behavior
| Probe result | Behavior |
|---|---|
Compliant | Write a Skipped stage result. Do not call Apply. |
NotCompliant | Repair only owned missing state, verify independently, then write Succeeded. |
Conflict | Write Failed and stop before mutation. |
Verification not Compliant | Write Failed even when Apply returned successfully. |
Acquire reacquires only invalid artifacts. Sql and Mecm repair only
owned missing components and do not reinstall an exact instance or site.
Marker reuses exact objects and changes only its fixed deployment chain.
Health is always read-only. See Stages & Evidence
for component detail.
Inspect evidence
A complete run directory contains run.json, one stage-<name>.json per
stage, and the component artifacts acquire-state.json, sql-state.json,
mecm-state.json, marker-state.json, and health.json.
acquisition.json appears only when Acquire Apply ran; it includes every
evaluated artifact, distinguishing reused Verified entries from newly
acquired AcquiredAndVerified entries.
Evidence omits source URIs, vault paths, credentials, tokens, private keys, raw policies, and raw log bodies. Before accepting a run, independently compare the marker collection and assignment with the exact client marker hash and installed application state. See Evidence Format.
Resume or stop
-
Preserve the failed run directory.
-
Read
stage-<name>.jsonand its component-state artifact. -
Correct only the proven prerequisite, source, or owned component.
-
Rerun the failed stage and only its later dependent stages from the same commit, preserving canonical relative order:
Failed stage Rerun stages AcquireAcquire,Sql,Mecm,Marker,HealthSqlSql,Mecm,Marker,HealthMecmMecm,Marker,HealthMarkerMarker,HealthHealthHealthThe first four rows include
Markerand therefore require the same exactSourceCommit; aHealth-only recovery does not. For example, after aSqlfailure:pwsh ./scripts/Invoke-SetupCm.ps1 ` -ConfigPath $env:SETUPCM_CONFIG ` -Mode Unattended ` -Stage Sql,Mecm,Marker,Health ` -SourceCommit $env:SETUPCM_SOURCE_COMMIT
Stop when identity is inconsistent, state is ambiguous, or repair would require reset, reinstall, authentication weakening, trust changes, broader targeting, historical-object deletion, or unavailable media/credentials. See Recovery & Resume.
Validate the source
Invoke-Pester ./tests/Unit -Output Detailed -CI
./scripts/Test-MarkdownLinks.ps1
mdbook build ./docs/gitbook
Run Windows detector, core-stage, and provider integration suites on the accepted CM01 host. They are intentionally excluded from portable CI.
Stages & Evidence
setup-cm executes five desired-state stages. Each reads current state before
deciding whether any owned action is necessary. The accepted LabZ1 two-run
gate proved every exact stage skips without mutation when already compliant;
see v1 Hands-Off Acceptance before operating the live
site.
Stage lifecycle
Every stage follows:
- Test — collect structured, read-only component state.
- Apply — run only for
NotCompliant, and only for owned components. - Verify — independently collect state again.
- Evidence — write a sanitized component artifact and stage result.
A Compliant test produces Skipped without invoking Apply. A Conflict
stops before mutation. Apply success is not acceptance: Verify must return
Compliant. Health has an empty Apply and is always read-only.
The five stages
| Stage | Read-only test | Bounded apply |
|---|---|---|
Acquire | Verifies every configured artifact’s license, byte length, SHA-256, version, architecture, publisher, and cache identity. | Acquires only missing or invalid artifacts under the approved source policy. |
Sql | Verifies ODBC Driver 18, Windows prerequisites, instance and services, startup state, TCP/listener/firewall, strict-TLS query reachability, owned configuration, VC++ x64/x86, and conditional CM_LAB reachability. | Installs a missing ODBC provider before the first database probe, installs an absent instance, or repairs only owned missing components; conflicting instance identity fails closed. |
Mecm | Verifies site/provider/database/role identity, services, prerequisites, ADK/WinPE/ODBC/VC++, content library, MP/DP, and the active non-obsolete client. | Installs an absent site or repairs only owned missing prerequisites or roles; it shares the ODBC installer with SQL, and an exact site never opens media or reruns setup. |
Marker | Verifies the fixed LabZ1 boundary, source and detector hashes, protected one-client evidence channel, authenticated direct marker proof, application, deployment type, content, distribution, one-device direct collection, required assignment, and per-device compliance. | Creates only a missing safe channel, performs only the approved predecessor detector update, and requests one bounded policy/evaluation cycle when direct evidence or current server state is pending. |
Health | Re-reads SQL, MECM, MP, DP, and active-client state. | None. |
Marker safety contract
Marker acceptance is fixed to:
- site
LABonLABZ1-CM01.test.gell.one; - target
RING0IVY24-01.test.gell.one, resource16777219; - application
Setup-CM Phase 1 Marker; - deployment type
Install Setup-CM Phase 1 Marker; - collection
Setup-CM Phase 1 Marker - RING0IVY24-01 Only; - marker SHA-256
3F44AA70B40C9E9095E69F1C57E98F6ACC06900788A2054E251BCC58179B6254.
The collection must have exactly one direct rule and one member, both for the
accepted resource. No marker assignment may target another collection.
Same-name conflicts, unexpected rules or files, broader membership, another
assignment, or hash drift are Conflict, not repair opportunities.
The hidden SetupCmMarkerEvidence$ share points directly to the exact target
directory and grants client write access only to TEST\RING0IVY24-01$.
Published evidence has six exact fields, no client timestamp, a 2,048-byte
limit, a 30-minute CM01 receipt freshness window, and two-minute future clock
tolerance. Authenticated direct C$ bytes are authoritative when available;
otherwise a valid target-computer-owned receipt is reported as
DirectAuthenticatedClientEvidence. A ConfigMgr server projection is not a
direct-proof route.
Stage result files
Every selected stage writes stage-<stage>.json:
{
"name": "Marker",
"state": "Skipped",
"startedAt": "2026-08-30T06:00:00.0000000Z",
"finishedAt": "2026-08-30T06:00:05.0000000Z",
"message": "Already compliant."
}
| State | Meaning |
|---|---|
Succeeded | Apply ran and independent Verify returned Compliant. |
Skipped | Test returned Compliant; Apply did not run. |
Failed | Test found conflict, Apply failed, or Verify did not return Compliant. |
Component evidence
| File | Contents |
|---|---|
run.json | Run ID, UTC start, and exact source commit when provided. Marker runs require it. |
acquire-state.json | Artifact component state without source URI or vault location. |
acquisition.json | Bounded identities and hashes for every artifact evaluated during Acquire Apply, distinguishing reused Verified entries from AcquiredAndVerified entries. |
sql-state.json | SQL component state and reasons. |
mecm-state.json | MECM component state and reasons. |
marker-state.json | Sanitized evidence-channel identity, direct-proof route, validated marker hash/length and receipt metadata, application/DT revisions, content/package/distribution, collection and membership, assignment/policy, per-device server compliance, evaluated time, and exact source commit. Raw ACL descriptors and policies are excluded. |
health.json | Fresh Boolean results for each read-only health check. |
Marker provider state proves the exact detector and per-device compliance row.
Release acceptance must independently corroborate the client’s application
revision and authenticated marker bytes. The successful routes are
DirectAuthenticatedFileRead and DirectAuthenticatedClientEvidence; a
server projection is not a substitute for either direct route.
Sanitization boundary
Evidence serialization recursively removes sensitive keys and redacts credential-like values and private locations in strings. Never add source URLs, vault paths, credentials, tokens, private keys, generated policy XML, raw configuration, raw log bodies, or installer media to an evidence bundle.
Interpreting a no-op run
A successful process exit is not sufficient. A genuine second-run no-op has:
- all five stage results
Skipped; - a fresh run ID and timestamps tied to the same exact source commit;
- all component states
Compliant; - no installer process, object creation, membership change, deployment creation, policy repair, or content redistribution;
- the same ConfigMgr identities and exact client marker hash;
- all Health values
true.
This zero-action claim is for the immediate second acceptance run while the first record remains within its 30-minute freshness window. A later stale-proof run may request one bounded policy/evaluation cycle and wait for a new receipt, but it still must not reinstall, recreate, redistribute, or broaden scope.
Recovery & Resume
A failed stage does not require a full restart. The stage engine and evidence format are designed for safe, targeted recovery.
When to resume vs. stop
| Scenario | Recommended action |
|---|---|
| A stage fails because a prerequisite is missing or misconfigured. | Correct the prerequisite, then rerun the failed stage and later dependent stages. |
| A stage fails because media is corrupt or the wrong version. | Replace the media in the cache or vault, update the configuration if necessary, then rerun. |
A stage reports Conflict. | Stop. Reconcile identity or scope outside the automated repair path; do not weaken the gate. |
| Marker collection, member, resource, or assignment identity differs. | Stop before mutation and preserve provider/client evidence. Never broaden membership to continue. |
| Marker evidence channel is entirely missing or a recognizable protected safe subset. | Rerun Marker from the same exact commit; it may create or complete only the fixed channel. |
| Marker evidence is missing or stale but otherwise valid. | Permit one policy/evaluation request and the bounded convergence wait; do not reinstall or recreate objects. |
| Marker share path, ACL, owner, identity, detector hash, or evidence record conflicts. | Stop and preserve the final record and component evidence. Never delete or weaken unknown state to continue. |
| A stage fails partway through and the system state is ambiguous. | Stop, preserve evidence, and hand off to the provisioning owner. Do not reset or reinstall as a setup-cm recovery action. |
The Health stage fails after a successful Mecm stage. | Investigate the specific health check in the evidence, correct the issue, then rerun Health alone. |
How to resume
-
Preserve the evidence directory from the failed run.
-
Review the failed
stage-<name>.jsonand corresponding component-state artifact to identify the immediate error. -
Correct the stated prerequisite, source, configuration, or host condition.
-
Rerun the failed stage and only its later dependent stages, preserving canonical relative order:
Failed stage Rerun stages AcquireAcquire,Sql,Mecm,Marker,HealthSqlSql,Mecm,Marker,HealthMecmMecm,Marker,HealthMarkerMarker,HealthHealthHealthThe first four rows include
Markerand therefore require the same exactSourceCommit; aHealth-only recovery does not. For example, after aSqlfailure:pwsh ./scripts/Invoke-SetupCm.ps1 ` -ConfigPath $env:SETUPCM_CONFIG ` -Mode Unattended ` -Stage Sql,Mecm,Marker,Health ` -SourceCommit $env:SETUPCM_SOURCE_COMMIT
What the stage engine protects
- Idempotency — if a stage test reports
Compliant, the stage is skipped entirely. - No partial verify — if
Applythrows,Verifydoes not run. - Evidence preservation — both successful and failed stage results are written before the engine returns or throws.
- Source pinning — any Marker run rejects a missing or abbreviated commit.
- Conflict boundary — unsupported topology, target, or same-name object state never enters Apply.
Marker evidence recovery
Channel creation proceeds from restrictive local state outward and exposes the hidden share last. If it stops partway through, the protected directory is left in place. A later run may complete it only when all existing paths are exact, inheritance is disabled, and every existing ACE is an expected safe subset. The repair never removes an unknown trustee or retargets a same-name share.
ClientEvidencePending caused by a missing or older-than-30-minute record is
repairable. The run waits up to five minutes for the current
application/assignment policy revisions and scope to agree and remain observed
unchanged for 60 seconds. Each read-only publication snapshot runs in a
disposable process bounded by the lesser of 90 seconds and the time left in that
one deadline; a hung ConfigMgr/CIM query is terminated and fails before
notification. The run then requests machine policy once, captures the UTC
minimum evidence-receipt timestamp immediately after that request completes,
waits a 30-second processing interval, and requests application evaluation
once. The interval is sequencing margin, not
client receipt proof; only fresh authenticated detector evidence and matching
current client/server revision can complete convergence. It then
probes every 15 seconds for up to 15 minutes. A scope/cardinality change fails
before notification. A conflict stops immediately. Timeout records the final
state and does not make a second mutation attempt in the same run.
Malformed, duplicate-field, oversized, foreign-owned, future-dated, wrong-path, wrong-hash, or wrong-identity evidence is preserved for diagnosis and is not deleted automatically. Do not recover by granting a broad principal, enabling client inbound SMB, weakening trust or execution policy, using VNC, recreating the marker chain, or expanding the target collection.
VM reset boundary
Stop and request a separate provisioning decision when:
- The MECM setup log shows an unrecoverable error.
- The SQL Server installation is incomplete and cannot be repaired.
- The Windows Server host is in an unknown or untrusted state.
- Marker scope cannot be reconciled across provider and direct client checks.
- Repair would change trust, authentication, client-wide policy, or target scope.
- You are unsure whether a partial installation is safe to resume.
Never guess through ambiguous state. A reset or rebuild belongs to ProxmoxVEAutopilot, is outside the setup-cm v1 acceptance boundary, and requires its own authorization and evidence. The accepted LabZ1 site must not be reset to make a rerun pass.
Why setup-cm Exists
The problem
Installing MECM in a lab is more than launching two installers. It combines licensed and version-sensitive media, SQL Server setup, Windows prerequisites, MECM prerequisite downloads, site configuration, and client validation. A successful console installation does not prove that the Management Point, Distribution Point, boundaries, or client registration work.
Ad-hoc scripts and one-time setup notes make those dependencies easy to lose. They also leave little useful information when a deployment fails halfway through.
The approach
setup-cm treats a lab deployment as a sequence of small desired-state stages:
- Test whether a stage is already compliant.
- Apply only the work required for that stage.
- Verify the resulting state and save a structured result.
This provides three practical benefits:
- Repeatability — the YAML configuration and explicitly ordered stages make the same lab intent reviewable and rerunnable.
- Diagnosability — every run gets its own evidence directory, including a result for each completed or failed stage.
- Safe recovery — an operator can correct the reported prerequisite and rerun only the affected stage rather than restart blindly.
Why media is external
MECM, SQL Server, and related installers can be licensed, authenticated, customer-specific, large, or subject to vendor distribution rules. The repository therefore stores only source metadata, integrity values, versions, and an explicit license acknowledgement. Operators keep actual media in an approved vendor location, private installer vault, or local cache.
The same separation applies to product keys, credentials, certificates, and generated evidence that may contain environment-specific information.
Why it is lab-focused
The default model is a disposable, isolated single-box primary site. It keeps the baseline affordable to operate and makes failures easier to reset and understand. The configuration requires an explicit safety decision when the target is not isolated.
Production MECM requires organization-specific architecture, capacity planning, service accounts, security review, backup and recovery design, operational ownership, and change control. Those decisions cannot be safely generalized by this repository.
Why provisioning is separate
setup-cm is not a VM or operating-system provisioning tool. A provisioning layer creates the isolated network and virtual machines, installs Windows, and joins the domain. setup-cm begins once those prerequisites exist and owns the Windows-side MECM work.
Keeping that boundary clear allows the MECM workflow to be rerun without redefining VM lifecycle, while the provisioning system can remain independent of MECM implementation details.
What comes next
The core Health stage is the baseline gate. Co-management, Patch My PC, reporting, alternate platforms, and every other optional capability remain separate projects so their failures cannot obscure baseline MECM health. See Future Projects.
Design Principles
setup-cm is built around a small set of non-negotiable design principles. Understanding them makes the codebase easier to navigate and extend.
1. Desired-state stages, not scripts
Each stage is a Test / Apply / Verify triplet. The stage engine only runs
Apply when Test reports NotCompliant, and only runs Verify after Apply
succeeds. That engine contract makes a stage idempotent only when its Test is a
real state probe. Acquire, SQL, MECM, Marker, and Health now meet that standard;
the final live gate runs the same exact source twice to prove it. See
Current LabZ1 Status.
2. Evidence is a first-class output
Every run writes structured JSON to a unique directory under evidenceRoot. Evidence is not an afterthought or a log file; it is the primary artifact that tells you what happened, when, and why. Both success and failure produce evidence.
3. Configuration is explicit and validated
The private YAML configuration declares operator-selectable deployment state.
The Marker feature adds a compiled fixed LabZ1 contract, and release-critical
evidence adds an exact source commit. Placeholder values are rejected when
template is false; target and safety guardrails are rechecked before any
provider action.
4. Media and secrets stay outside Git
The committed template contains only safe example metadata. Private source locations, actual installers, product keys, credentials, certificates, working configuration, and generated evidence never enter version control.
5. Lab-first, with explicit safety gates
The supported v1 topology is the single-box isolated LabZ1 environment. General configuration validation has an explicit production acknowledgement, but the Marker command and v1 acceptance remain fixed lab-only and do not support production targeting.
6. Provisioning is a separate concern
setup-cm does not create VMs, install Windows, or join domains. It assumes those prerequisites are met and focuses exclusively on Windows-side MECM configuration and validation. This boundary keeps the tool focused and reusable.
7. Testability at every layer
Every private function has a corresponding Pester 6 unit test. Mocks isolate Windows-specific APIs (registry, services, processes) so tests run on any platform. Integration tests validate the live lab only after unit tests pass.
8. Small, composable modules
Private functions live in src/SetupCm/Private/ and do one thing. Public entry points live in src/SetupCm/Public/ and orchestrate stages. Scripts in scripts/ are thin wrappers for agent execution. This separation keeps the module testable and the entry points predictable.
Module Layout
src/SetupCm/
├── SetupCm.psd1 # Module manifest and exported commands
├── SetupCm.psm1 # Loads private and public functions
├── Private/
│ ├── Acquisition.ps1 # Download, hash, signature, and cache verification
│ ├── Client.ps1 # MECM client installation and evidence
│ ├── Configuration.ps1 # YAML parsing, schema validation, safety checks
│ ├── Evidence.ps1 # Run-folder creation and structured stage-result writes
│ ├── Health.ps1 # Read-only SQL, MECM, MP, DP, and client checks
│ ├── MarkerApplication.ps1 # Fixed LabZ1 marker probes and bounded provider repair
│ ├── Mecm.ps1 # MECM prerequisites, ADK, WinPE, and site setup
│ ├── Media.ps1 # Media path helpers
│ ├── Odbc.ps1 # Shared ODBC Driver 18 detection and installation
│ ├── Sql.ps1 # SQL prerequisites and unattended installation
│ └── StageEngine.ps1 # Shared idempotent stage executor
└── Public/
├── Invoke-SetupCm.ps1 # Guided/unattended stage orchestrator
├── Invoke-SetupCmAcquire.ps1 # Acquisition-only command
├── Invoke-SetupCmClient.ps1 # Client-stage command
├── Invoke-SetupCmMarkerAcceptance.ps1 # Fixed marker command
└── Test-SetupCmPreflight.ps1 # Configuration readiness check
Scripts
scripts/
├── Invoke-SetupCm.ps1 # Guided/unattended entry point
├── Invoke-SetupCmClient.ps1 # Agent client-stage entry point
├── Invoke-SetupCmMarkerAcceptance.ps1 # Marker-only entry point
├── Test-MarkdownLinks.ps1 # Portable local-link gate
└── marker/ # Reviewed install/detect/uninstall payload
Tests
tests/Unit/
├── Acquisition.Tests.ps1
├── AgentEntrypoint.Tests.ps1
├── Client.Tests.ps1
├── Configuration.Tests.ps1
├── Health.Tests.ps1
├── MarkdownLinks.Tests.ps1
├── MarkerAcceptance.Tests.ps1
├── MarkerApplication.Tests.ps1
├── Mecm.Tests.ps1
├── Media.Tests.ps1
├── Module.Tests.ps1
├── Sql.Tests.ps1
└── StageEngine.Tests.ps1
tests/Integration/
├── CoreStages.Windows.Tests.ps1
├── MarkerAcceptance.Provider.Tests.ps1
└── MarkerDetection.Windows.Tests.ps1
Configuration
config/
└── lab.example.yaml # Complete, safe template with placeholders
Public Commands
setup-cm exports five public commands. Run operational workflows through
the wrappers in scripts/; files under src/SetupCm/Public/ define module
functions and are not standalone entry points.
Invoke-SetupCm
The main stage orchestrator:
Invoke-SetupCm [-ConfigPath] <string> [[-Mode] <string>]
[[-Stage] <string[]>] [[-SourceCommit] <string>]
| Parameter | Type | Default | Description |
|---|---|---|---|
ConfigPath | string | — | Path to the validated private YAML configuration. |
Mode | string | Guided | Guided pauses after each stage; Unattended does not. |
Stage | string[] | Config-dependent | Operator-selected subset of Acquire, Sql, Mecm, Marker, and Health. A validated lab-only marker configuration with the fixed LabZ1 identities defaults to all five and requires a full SourceCommit; a marker-disabled configuration omits Marker. |
SourceCommit | string | SETUPCM_SOURCE_COMMIT | Full 40-character commit for evidence provenance. Required whenever Marker is selected. |
The canonical order is Acquire → Sql → Mecm → Marker → Health.
The command does not reorder an explicit subset, so operators must preserve
that relative order. Select Marker only when markerAcceptance.enabled and
markerAcceptance.labOnly are both true, the exact fixed LabZ1 identities
validate, and a full source commit is available. An enabled marker
configuration without that commit fails before stage execution rather than
silently omitting the required Marker stage.
Operational example:
pwsh ./scripts/Invoke-SetupCm.ps1 `
-ConfigPath $env:SETUPCM_CONFIG `
-Mode Unattended `
-Stage Acquire,Sql,Mecm,Marker,Health `
-SourceCommit $env:SETUPCM_SOURCE_COMMIT
Invoke-SetupCmMarkerAcceptance
Evaluates or reconciles only the fixed LabZ1 marker chain:
Invoke-SetupCmMarkerAcceptance [-ConfigPath] <string>
[[-EvidenceRoot] <string>] [[-SourceCommit] <string>]
SourceCommit is mandatory in behavior even though PowerShell permits the
parameter to be omitted; the command resolves the environment fallback and
fails before evidence or provider mutation when no exact commit is available.
pwsh ./scripts/Invoke-SetupCmMarkerAcceptance.ps1 `
-ConfigPath $env:SETUPCM_CONFIG `
-SourceCommit $env:SETUPCM_SOURCE_COMMIT
The command refuses any boundary other than site LAB, server
LABZ1-CM01.test.gell.one, and target RING0IVY24-01.test.gell.one
resource 16777219.
Client proof comes from an authenticated C$ marker read when available, or
from the code-fixed SetupCmMarkerEvidence$ channel when the direct route is
unavailable. Missing or stale evidence permits only the bounded, lab-approved
channel, predecessor-detector policy, and client reevaluation sequence.
Malformed or contradictory evidence fails closed, and ClientProbeUnavailable
means both authenticated proof routes failed unexpectedly.
Invoke-SetupCmAcquire
Runs the acquisition apply path for artifacts identified by the outer Acquire stage:
Invoke-SetupCmAcquire [-ConfigPath] <string> [[-EvidenceRoot] <string>]
It reuses each exact artifact and applies only invalid entries. Normal
operators should select Acquire through Invoke-SetupCm so the stage’s
read-only Test and independent Verify wrap this command.
Invoke-SetupCmClient
Runs the separate typed MECM client installation workflow using a JSON manifest:
Invoke-SetupCmClient [-ManifestPath] <string>
The manifest must contain siteCode, managementPointFqdn, and
evidenceRoot. This command is not the Marker stage and does not broaden the
accepted one-device marker boundary.
Test-SetupCmPreflight
Validates configuration and reports readiness:
Test-SetupCmPreflight [-ConfigPath] <string>
It returns Ready and Missing. Continue only when Ready is True.
Configuration Schema
The YAML configuration has the following top-level structure.
topology: string
template: boolean
lab:
name: string
domain: string
safety:
isolatedLab: boolean
allowProductionTarget: boolean
evidenceRoot: string
cacheRoot: string
sources:
<source-name>:
uri: string
sha256: string
publisher: string
version: string
architecture: x64 | x86 | neutral
sizeBytes: integer
licenseAccepted: boolean
cacheFile: string
signatureRelativePath: string # required for ISO media
architectureRelativePath: string # optional signed ISO payload proof
architectureVerification: signedVersionResource # optional EXE bootstrapper proof
sql:
instanceName: string
serviceAccount: string
sysAdminAccounts: string[]
installDirectory: string
mecm:
siteCode: string
siteName: string
sqlServer: string
siteServerFqdn: string
productId: string
smsInstallDir: string
prerequisitePath: string
testClient:
name: string
domain: string
markerAcceptance:
enabled: boolean
labOnly: boolean
siteCode: string
siteServerFqdn: string
targetFqdn: string
targetResourceId: integer
Required source names
The following source names are required for a full run:
sqlServermecmadkadkWinPeodbcDriver18vcRedistX64vcRedistX86
Validation rules
safety.isolatedLabmust betrue, orsafety.allowProductionTargetmust be explicitlytrue.template: trueallows placeholder values;template: false(or omitted) requires real values.licenseAcceptedmust betruefor every source before acquisition runs.sha256must be a 64-character hexadecimal string.- Every non-template source requires a positive
sizeBytes, an expected publisher, a native product version, and anx64,x86, orneutralarchitecture.sources.mecm.versionis the four-componentProductVersionof its signedsetup.exe, not a Current Branch label. architectureRelativePathis ISO-only and validates the separate signed payload’s PE architecture.architectureVerification: signedVersionResourceis executable-only and accepts a signed x86 launcher for an x64 payload only when signed version-resource identities unambiguously namex64. A source may select at most one of these architecture proofs, and only when its declared architecture isx64orx86.mecm.siteCodemust match^[A-Z0-9]{3}$.mecm.sqlServerandmecm.siteServerFqdnmust match the lab domain.testClient.nameandtestClient.domainmust identify a separate, domain-joined client.- Enabled marker acceptance requires
labOnly: trueand these exact values:siteCode: LAB,siteServerFqdn: LABZ1-CM01.test.gell.one,targetFqdn: RING0IVY24-01.test.gell.one, andtargetResourceId: 16777219.
Runtime-only inputs
The private YAML path and source provenance are supplied outside the schema:
| Input | Contract |
|---|---|
SETUPCM_CONFIG or -ConfigPath | Path to the non-template YAML staged separately from source. |
SETUPCM_SOURCE_COMMIT or -SourceCommit | Full 40-character commit used to create the staged source archive; mandatory for Marker. |
Neither value is copied into component evidence except the validated commit. The private configuration body, source URIs, and vault locations are never evidence fields.
Evidence Format
Each invocation creates a unique directory under evidenceRoot. Evidence is
structured for comparison across reruns and sanitized before serialization.
Run metadata
run.json contains:
{
"runId": "20260830-060000-1a2b3c4d",
"startedAt": "2026-08-30T06:00:00.0000000Z",
"sourceCommit": "0123456789abcdef0123456789abcdef01234567"
}
sourceCommit is present when supplied through -SourceCommit or
SETUPCM_SOURCE_COMMIT. Any run containing Marker requires a full
40-character commit and records the validated lowercase value in both
run.json and marker-state.json.
Stage result schema
Every selected stage writes stage-<stage>.json:
{
"name": "Acquire | Sql | Mecm | Marker | Health",
"state": "Succeeded | Skipped | Failed",
"startedAt": "ISO 8601 UTC",
"finishedAt": "ISO 8601 UTC",
"message": "string"
}
Skipped means Test returned Compliant and Apply was not invoked.
Succeeded means Apply ran and independent Verify returned Compliant.
Failed covers a conflict, an Apply error, or failed verification.
Component artifacts
| File | Produced by | Purpose |
|---|---|---|
acquire-state.json | Acquire Test/Verify | Per-artifact compliance, reason, size, hash, version, and architecture. |
acquisition.json | Acquire Apply | Bounded identity and hash metadata for every artifact evaluated during Apply, distinguishing reused Verified entries from AcquiredAndVerified entries. |
sql-state.json | SQL Test/Verify | Overall state plus SQL component state and reasons. |
mecm-state.json | MECM Test/Verify | Overall state plus site, role, prerequisite, service, content-library, and client components. |
marker-state.json | Marker Test/Verify | Exact boundary, payload, application/DT, content/distribution, collection/member, assignment/policy, client projection, and per-device server compliance. |
health.json | Health Test/Verify | Fresh Boolean read-only checks in deterministic key order. |
client-install.json | Typed client command | Sanitized client-install evidence for the separate manifest workflow. |
Component entries use Compliant, NotCompliant, or Conflict and include
a bounded reason. Private source locations and source bytes are excluded.
Marker corroboration
marker-state.json records ConfigMgr object identities and revisions, content
and package identity, reviewed hashes, DP state, exact collection membership,
assignment intent, policy revision, projected client state, and the exact
per-device server compliance row. Its evaluatedAt and the corresponding
stage-Marker.json start/end times establish freshness.
Release acceptance separately checks the client application revision, installed/evaluation/error state, marker existence and SHA-256, and marker last-write time over an authenticated management channel. Store only the bounded results and hashes, never raw client logs or policy.
Sanitization
Serialization recursively omits password, secret, token, authorization, credential, private-key, source-URI, vault-path, SAS, and API-key fields. It also redacts credential assignments, bearer values, URLs, and UNC paths embedded in otherwise safe strings.
Do not put any of the following in source, evidence, commits, PR output, or release notes:
- credentials, recovery material, keys, certificates, tokens, or SAS values;
- private configuration or source URLs;
- generated unattended files or policy XML;
- raw log bodies or installer media.
Retention
Evidence is not removed automatically. Preserve failed runs until resolved.
For release acceptance, retain both complete run directories, their archive
hashes, and the final independently verified client/server summary. The second
bundle must be fresh even though every stage is Skipped.
The ProxmoxVEAutopilot Journey
This section documents how Adam built and validated setup-cm in his own lab. It is not a generic tutorial — it is the specific path that worked, the constraints that shaped the design, and the lessons that inform the tool’s future direction.
The starting point
Adam needed a repeatable way to stand up a Microsoft Configuration Manager (MECM) lab for testing Intune co-management scenarios, client deployment behaviors, and troubleshooting workflows. Manual MECM installations were time-consuming and error-prone. Existing automation either assumed production infrastructure or lacked the evidence trail needed to diagnose failures in a disposable lab.
The lab already had:
- Proxmox VE as the hypervisor
- ProxmoxVEAutopilot — Adam’s own automation layer for VM provisioning, OS deployment, and domain joining
- A private installer vault with licensed Microsoft media
- A need to tear down and rebuild the lab frequently
Why ProxmoxVEAutopilot was the right foundation
ProxmoxVEAutopilot already solved the problems that setup-cm explicitly does not:
- Creating isolated networks and VMs
- Installing Windows Server and Windows client OS
- Joining machines to the lab domain
- Running typed, validated work items on Windows hosts
Rather than duplicate that capability, setup-cm was designed to plug into it. The boundary is clean: ProxmoxVEAutopilot owns the infrastructure lifecycle; setup-cm owns the MECM configuration and validation that happens after Windows is ready.
The first working deployment
The first successful end-to-end run used this sequence:
- ProxmoxVEAutopilot OSDeploy Server created a Windows Server 2022 VM with the
baserole - The server was verified as
base_readyanddomain_joined=true - A staged
setup-cmsource bundle and a non-templatelab.local.yamlwere placed on the server - Verified SQL Server, MECM current-branch, and prerequisite media were staged in the private vault
Test-SetupCmPreflightconfirmed readiness- A separate Windows client VM was provisioned and domain-joined
Invoke-SetupCmran the fullAcquire → Sql → Mecm → Healthsequence- The
Healthstage confirmed SQL, MP, DP, boundaries, and client registration
The evidence directory from that first Health success is the baseline proof that the design works.
This is historical bring-up order. The current release-candidate workflow adds
the bounded Marker stage and performs read-before-apply desired-state checks
for all five stages.
What made it hard
Three problems consumed the most time:
VC++ redistributable detection. MECM setup fails cryptically when the x64 or x86 VC++ v14 runtime is missing or below 14.34. The registry keys are architecture-specific and the version strings are inconsistent. The fix required per-architecture registry probing and a hard gate before Setupdl.exe runs.
Client installation trust. Getting the MECM client onto a remote machine without WinRM or PSRemoting required a typed work contract in the Autopilot Agent, not a generic script. The client install work item validates the target FQDN, derives the only permitted ccmsetup.exe path, and captures sanitized evidence.
Evidence that survives failure. Early versions wrote logs but not structured state. When a stage failed halfway through, there was no machine-readable record of what had been verified. The stage engine now writes stage-<name>.json before returning or throwing, making resume decisions deterministic.
Where this leads
The ProxmoxVEAutopilot path proved that typed, validated, evidence-producing automation works for MECM lab deployment. The next sections describe how the Autopilot Agent was built to execute that path, and how setup-cm will support both agent-driven and local-terminal execution going forward.
Building the Autopilot Agent
The Autopilot Agent is the execution layer that makes unattended, validated MECM deployment possible. This page explains why it exists, how it was built, and what it enforces.
Why not just use WinRM or SSH?
Windows remote management protocols are powerful but poorly suited to this use case:
- They require network connectivity and firewall rules that may not exist in an isolated lab
- They provide no built-in validation of the work being requested
- They make it easy to run arbitrary commands, which violates the principle of constrained, typed work
- They leave no structured evidence of what was requested versus what was executed
The Autopilot Agent solves these problems by running locally on the target Windows host and accepting only typed, validated work items from a trusted controller.
Architecture
ProxmoxVEAutopilot Controller (FastAPI/Python)
└─ queues typed work items
└─ Autopilot Agent (.NET 8 Windows Service)
├─ validates work item schema and constraints
├─ stages approved archives (SHA-256 verified)
├─ executes PowerShell 7 entry points
└─ returns structured results and bounded output
The Agent is a Windows service written in C# on .NET 8. It polls the controller for work, validates each item against a strict schema, and executes only approved entry points.
Typed work kinds
The Agent does not accept arbitrary commands. It supports specific work kinds, each with its own validation rules:
| Work kind | Purpose | Key constraints |
|---|---|---|
setup_cm_acquire | Download and verify installation media | Approved URIs only; SHA-256 pinned |
setup_cm_sql | Install SQL Server | Non-template config required; isolated-lab check |
setup_cm_mecm | Install MECM primary site | VC++ 14.34+ gate; approved prerequisite path |
setup_cm_health | Run health validation | Read-only; no state changes |
setup_cm_client_install | Install MECM client on a target | FQDN suffix restricted; SHA-256-pinned module archive |
Each work kind has a dedicated request model. Unknown fields are rejected. Secrets are never accepted.
The client install work kind
The setup_cm_client_install work kind illustrates the Agent’s design philosophy:
- Constraint: The management point FQDN must end in the approved lab domain suffix. Any other domain is rejected before execution.
- Pinning: The module archive must match a 64-character SHA-256 provided at queue time. The Agent verifies the hash before extraction.
- Derivation: The only permitted installer path is
\\<managementPoint>\SMS_<siteCode>\Client\ccmsetup.exe. No arbitrary UNC paths. - Evidence: The Agent captures sanitized
ccmsetup.logandClientIDManagerStartup.logtails, redacting anyPassword=values.
This is deliberately less flexible than a generic script runner. The constraint is the feature.
Building and releasing
The Agent is built as a signed MSI using WiX. The build requires a Windows host; WiX cannot build MSIs on macOS. The release process is:
- Update the Agent version in the project file
- Build and sign the MSI on a Windows build machine
- Publish the MSI to the private distribution point
- Existing Agents self-update on their next heartbeat
The controller tracks Agent versions and capabilities through heartbeat telemetry. Before queueing a work item, the controller verifies the target Agent supports the requested kind.
Contract testing
The Agent’s validation logic is covered by C# contract tests that run without a Windows domain or live controller. These tests verify:
- Each work kind is registered
- Invalid FQDNs, malformed SHA-256 values, and unexpected fields are rejected
- The client work kind accepts only the approved domain suffix
- No secret fields are accepted in any request model
Contract tests run in CI and must pass before any Agent release.
Two Execution Models
setup-cm supports two ways to run the same stages. Both use the same PowerShell module, the same YAML configuration, and the same evidence format. The difference is who invokes the entry point and where the operator sits.
Model 1: Local terminal (PowerShell 7)
The operator runs setup-cm directly on the Windows Server in a PowerShell 7 session.
Import-Module ./src/SetupCm/SetupCm.psd1 -Force
Test-SetupCmPreflight -ConfigPath ./config/lab.local.yaml
pwsh ./scripts/Invoke-SetupCm.ps1 `
-ConfigPath ./config/lab.local.yaml `
-Mode Guided `
-SourceCommit '<FULL_40_CHARACTER_GIT_COMMIT>'
When to use this:
- Initial lab bring-up and debugging
- Learning how the stages work
- Validating a new configuration before handing it to the Agent
- One-off runs where the Agent is not yet deployed
Characteristics:
- Guided mode pauses between stages for operator inspection
- Full console output is visible immediately
- The operator controls timing and can intervene between stages
- Works through an authenticated PowerShell/SSH session; visual console access is not an acceptance dependency
Model 2: Autopilot Agent (unattended)
The operator queues a typed work item through the ProxmoxVEAutopilot controller. The Agent on the target server executes setup-cm without interactive login.
# On the server, staged by the provisioning layer:
$env:SETUPCM_CONFIG = 'C:\Path\To\lab.local.yaml'
$env:SETUPCM_SOURCE_COMMIT = '<FULL_40_CHARACTER_GIT_COMMIT>'
pwsh ./scripts/Invoke-SetupCm.ps1 -Mode Unattended
When to use this:
- Repeatable, hands-off lab rebuilds
- CI/CD-style automation where the lab is provisioned and configured without human presence
- Environments where RDP/SSH access is restricted or audited
- Client installation on remote machines where no interactive session exists
Characteristics:
- Unattended mode runs all selected stages without pausing
- The Agent validates the work item before execution
- Structured results and bounded output are returned to the controller
- Evidence is written to the same
evidenceRootas local runs
Same stages, same evidence
Regardless of the model, the stage engine behaves identically:
Test→Apply→Verify→stage-<name>.json- Idempotent skip when already compliant
- Conflict stops before mutation; verification failure still fails the stage
- Fail-fast with preserved sanitized evidence tied to the exact source commit
- Resume by rerunning the failed stage and later dependents
The operator chooses the model that fits the situation. The tool does not change.
Future direction
Both models are first-class execution paths for the supported LabZ1 topology. Other platforms or broader automation require their own design and acceptance boundary; see Future Projects.
Platform Support Roadmap
setup-cm was built and proven on Proxmox VE with ProxmoxVEAutopilot. The
architecture keeps provisioning separate, but v1 supports only the Proxmox
LabZ1 reference path. Hyper-V and VMware are future projects, not current
compatibility claims. This page records their design inputs.
Current: Proxmox VE + ProxmoxVEAutopilot
| Capability | Status |
|---|---|
| VM provisioning | ✅ ProxmoxVEAutopilot OSDeploy Server |
| OS deployment (Windows Server / Client) | ✅ base role with unattended answer files |
| Domain joining | ✅ Automated via ProxmoxVEAutopilot |
setup-cm execution (Agent) | ✅ Autopilot Agent typed work items |
setup-cm execution (local terminal) | ✅ PowerShell 7 on the VM |
This is the reference implementation. All design decisions were validated here first.
Planned: Hyper-V
Hyper-V is the most natural next platform. The Windows Server host already runs Hyper-V for nested virtualization scenarios, and the management tooling is mature.
| Capability | What’s needed |
|---|---|
| VM provisioning | PowerShell Direct or Hyper-V VM creation scripts |
| OS deployment | Existing WDS/MDT, or custom unattend.xml injection |
| Domain joining | Unattend.xml or post-deployment script |
setup-cm execution (Agent) | Autopilot Agent already runs on any Windows host; controller endpoint is platform-agnostic |
setup-cm execution (local terminal) | PowerShell 7 over an authenticated management channel |
The Autopilot Agent does not depend on Proxmox VE. It is a Windows service that polls a FastAPI controller. Running the controller on a Hyper-V host or a separate management VM is straightforward.
Planned: VMware Workstation
VMware Workstation is common for individual consultants and small labs. It lacks the API surface of vSphere but supports local automation through VMware PowerCLI and the vmrun CLI.
| Capability | What’s needed |
|---|---|
| VM provisioning | PowerCLI New-VM or vmrun with template VMs |
| OS deployment | VMware Tools + unattended installation, or template clones |
| Domain joining | Unattend.xml or post-deployment script |
setup-cm execution (Agent) | Same as Hyper-V — Agent is platform-agnostic |
setup-cm execution (local terminal) | PowerShell 7 over an authenticated management channel |
For VMware Workstation, the local terminal model may be more practical initially, since Workstation is typically used interactively. The Agent model works if the controller is reachable from the Workstation VMs.
What stays the same across platforms
- The
setup-cmPowerShell module and stage engine - The YAML configuration contract
- The evidence format and
stage-<name>.jsonresults - The Autopilot Agent work kinds and validation rules
- The requirement for isolated-lab targets unless explicitly overridden
What changes per platform
- The provisioning layer that creates VMs and installs Windows
- The mechanism for staging the
setup-cmsource bundle and configuration onto the target - The network isolation approach (Proxmox VLANs, Hyper-V virtual switches, VMware host-only networks)
Contributing a new platform
To add support for a new hypervisor or provisioning system:
- Ensure the platform can create an isolated, domain-joined Windows Server and a separate test client
- Stage the
setup-cmsource bundle and a non-templatelab.local.yamlon the server - Run
Test-SetupCmPreflightto validate the configuration - Choose your execution model: local terminal or Autopilot Agent
- Run the stages and preserve the evidence
Do not assume the module needs no modification until that platform’s tests say so. Each platform needs a separate design and two-run acceptance boundary; see Future Projects.
Design Documents
Design documents capture the architectural decisions and constraints before implementation begins. They live in docs/superpowers/specs/.
Available designs
| Document | Date | Topic |
|---|---|---|
| 2026-07-30 SCCM Lab Automation Design | 2026-07-30 | Original single-box lab automation design |
| 2026-08-01 Agent MECM Client Install Design | 2026-08-01 | Typed client work contract and manifest design |
| 2026-08-01 MECM VC++ Redist Dependency Design | 2026-08-01 | VC++ runtime detection and gating design |
| 2026-08-30 Hands-Off Rerun and v1 Release Design | 2026-08-30 | Idempotent core stages, marker automation, two-run acceptance, and v1 release |
| 2026-08-30 Authenticated Marker Client Evidence Channel Design | 2026-08-30 | Fail-closed client proof without inbound firewall changes |
2026-07-30 SCCM Lab Automation Design
Goal: Build a PowerShell 7+ project that acquires approved installation sources and deploys a verified single-box MECM primary site through reusable, testable stages.
Key decisions:
- YAML configuration as the single source of truth.
- Stage engine with
Test/Apply/Verifyfor idempotency. - Evidence directory per run for diagnosability.
- Media and secrets external to Git.
- Lab-first with explicit production-target guardrail.
See docs/superpowers/specs/2026-07-30-sccm-lab-automation-design.md in the repository for the full text.
2026-08-01 Agent MECM Client Install Design
Goal: Install and verify the Configuration Manager client on a target through a signed, typed Autopilot Agent work item.
Key decisions:
- Constrained client-install work kind with typed fields.
- SHA-256-pinned module archive validation.
- Local non-secret JSON manifest on the target.
- Server-side discovery verification after client evidence submission.
See docs/superpowers/specs/2026-08-01-agent-mecm-client-install-design.md in the repository for the full text.
2026-08-01 MECM VC++ Redist Dependency Design
Goal: Install and verify Microsoft VC++ v14 x64 and x86 runtimes before MECM downloads prerequisites or runs setup.
Key decisions:
- Per-architecture registry version checks.
- Generic verified installer runner.
- Gate
Setupdl.exeuntil both architectures meet version 14.34.
See docs/superpowers/specs/2026-08-01-mecm-vc-redist-dependency-design.md in the repository for the full text.
2026-08-30 Hands-Off Rerun and v1 Release Design
Goal: Make the accepted LabZ1 deployment rerunnable without installer or Configuration Manager churn, then publish an exact evidence-backed v1 release.
Key decisions:
- Read-only component probes before every Apply.
- Minimal owned-state reconciliation and fail-closed identity conflicts.
- A bounded, idempotent one-device marker stage.
- Sanitized evidence tied to an exact commit and two consecutive live runs.
See docs/superpowers/specs/2026-08-30-hands-off-rerun-v1-design.md in the
repository for the full text.
2026-08-30 Authenticated Marker Client Evidence Channel Design
Goal: Prove the exact marker bytes from RING0IVY24-01 without opening
inbound client firewall rules or trusting a ConfigMgr server projection as
direct client evidence.
Key decisions:
- One setup-cm-owned hidden share grants bounded write access only to the exact client computer account.
- The VBScript detector publishes fixed-schema evidence only after the exact marker hash succeeds; detection remains independent of transport success.
- CM01 validates strict content, owner SID, receipt freshness, and matching ConfigMgr state before accepting the client proof.
- Only the exact known predecessor detector permits a one-time policy update; the immediate second run performs no mutation or evaluation request.
See
docs/superpowers/specs/2026-08-30-marker-client-evidence-channel-design.md in
the repository for the full text.
Implementation Plans
Implementation plans are step-by-step, checkbox-tracked task lists used by agentic workers to implement features. They live in docs/superpowers/plans/.
Available plans
| Plan | Date | Status | Tasks |
|---|---|---|---|
| 2026-07-30 Single-Box SCCM Core | 2026-07-30 | Complete | 8 tasks |
| 2026-08-01 Agent MECM Client Install | 2026-08-01 | Complete; live Task 5 accepted | 5 tasks |
| 2026-08-01 MECM VC++ Redist Dependency | 2026-08-01 | Complete; live x64/x86 baseline accepted | 3 tasks |
| 2026-08-30 Hands-Off Rerun and v1 Release | 2026-08-30 | Tasks 1–9 complete; Task 10 release finalization | 10 tasks |
2026-07-30 Single-Box SCCM Core
Goal: Build a PowerShell 7+ project that acquires approved installation sources and deploys a verified single-box MECM primary site through reusable, testable stages.
Tasks:
- Scaffold the PowerShell 7 module and Pester 6 test harness.
- Define and validate the non-secret YAML configuration contract.
- Implement verified acquisition from vendor sources and private installer vaults.
- Build the resumable stage engine and Autopilot Agent entry script.
- Implement Windows prerequisites and the single-box SQL Server stage.
- Implement MECM primary-site and core-role stages.
- Add live health validation, client proof, and CMTrace Open fixture curation.
- Document operator flows and complete the quality gate.
See docs/superpowers/plans/2026-07-30-single-box-sccm-core.md for the full plan with checkboxes.
2026-08-01 Agent MECM Client Install
Goal: Install and verify the Configuration Manager client on RING0IVY24-01 through a signed, typed Autopilot Agent work item.
Tasks:
- Setup-CM Client stage — complete.
- Server-side registration health gate — complete.
- Typed Autopilot Agent client work — complete upstream.
- Controller queue endpoint — complete upstream.
- Verify, deploy, and prove the live client path — complete and accepted.
See docs/superpowers/plans/2026-08-01-agent-mecm-client-install.md for the full plan with checkboxes.
Accepted evidence is in
docs/PHASE0-2026-08-29-LAB-INVENTORY.md. The signed Agent completed the typed
work on RING0IVY24-01; provider and SQL registration plus a fresh Health run
agreed.
2026-08-01 MECM VC++ Redist Dependency
Goal: Install and verify Microsoft VC++ v14 x64 and x86 runtimes before MECM downloads prerequisites or runs setup.
Tasks:
- Pin runtime detection with Pester — complete.
- Pin verified installation and stage order — complete.
- Make the source contract reusable and validate the reference lab privately — complete.
See docs/superpowers/plans/2026-08-01-mecm-vc-redist-dependency.md for the full plan with checkboxes.
2026-08-30 Hands-Off Rerun and v1 Release
Goal: Replace hardcoded compliance with real probes, automate bounded marker acceptance, prove two consecutive runs, and publish v1.
Tasks:
- Reconcile current project status and historical handoffs — complete.
- Pin evidence and compliance contracts — complete.
- Make Acquire read-only before apply — complete.
- Implement SQL desired-state reconciliation — complete.
- Implement MECM and read-only Health probes — complete.
- Productize marker acceptance — complete.
- Document and package the v1 workflow — complete.
- Complete branch CI and review — complete at the live-tested head.
- Run Windows/provider integration and two live runs — complete and accepted.
- Record acceptance, merge, tag, release, publish docs, and clean up safely — acceptance recorded; release finalization.
See docs/superpowers/plans/2026-08-30-hands-off-rerun-v1.md for the complete
test-first execution plan.
Testing
setup-cm uses Pester 6 for unit tests and a live-lab integration suite for
end-to-end validation. Run this repository’s Pester suite with PowerShell 7.4
or later. Pester 6 also supports Windows PowerShell 5.1, but the SetupCm module
itself requires PowerShell 7.
Unit tests
Run all unit tests:
Invoke-Pester ./tests/Unit -Output Detailed -CI
Run a focused test file:
Invoke-Pester ./tests/Unit/Mecm.Tests.ps1 -Output Detailed
Test structure
Every private function has a corresponding *.Tests.ps1 file in tests/Unit/. Tests use mocks to isolate Windows-specific APIs (registry, services, processes, file system) so they run on any platform.
| Test file | Coverage |
|---|---|
Module.Tests.ps1 | Module load, exported commands, PowerShell version |
Configuration.Tests.ps1 | YAML parsing, schema validation, safety guardrails |
Acquisition.Tests.ps1 | Exact cache state, affected-artifact-only acquisition, source redaction |
StageEngine.Tests.ps1 | Idempotency, skip logic, failure handling, evidence writes |
Sql.Tests.ps1 | SQL component state, bounded repair, conflict and verification gates |
Mecm.Tests.ps1 | Site/component state, bounded repair, media skip and conflict gates |
MarkerAcceptance.Tests.ps1 | Fixed boundary, provider reconciliation, no-op behavior, evidence provenance |
MarkerEvidenceChannel.Tests.ps1 | Strict six-field parser, SID/ACL/share state, proof precedence, freshness, and bounded convergence |
MarkerApplication.Tests.ps1 | Exact payload/detector and safe uninstall behavior |
Health.Tests.ps1 | Read-only SQL/MECM/MP/DP/client checks and fresh evidence |
Client.Tests.ps1 | Manifest validation, install arguments, rerun logic, redaction |
Media.Tests.ps1 | Media path helpers |
AgentEntrypoint.Tests.ps1 | Script entry-point contract |
MarkdownLinks.Tests.ps1 | Portable local file/heading link validation |
Integration tests
The integration suite in tests/Integration/ has four independent boundaries:
| File | Gate |
|---|---|
CoreStages.Windows.Tests.ps1 | Real SQL, MECM, and read-only Health state on CM01, with installer/setup calls guarded. |
MarkerDetection.Windows.Tests.ps1 | Exact VBScript detection plus strict target-only authenticated publication for valid, tampered, missing, transport-failure, and test-path cases. |
MarkerEvidenceChannel.Windows.Tests.ps1 | Real Windows protected ACL construction, SID-normalized inventory, and bounded reads in temporary paths. |
MarkerAcceptance.Provider.Tests.ps1 | Exact one-device provider state in explicit PreMigration or PostMigration mode, with every mutation/side-effect adapter replaced by a throw. |
Note: Integration tests require a completed single-box lab and are not run in CI.
Run the CM01 Windows and provider suites as the authorized LabZ1 domain operator. Use a fresh PowerShell process for the provider suite so module-scope mocks from another Pester invocation cannot leak into it:
$env:SETUPCM_LAB_INTEGRATION = '1'
Invoke-Pester ./tests/Integration/CoreStages.Windows.Tests.ps1, `
./tests/Integration/MarkerDetection.Windows.Tests.ps1, `
./tests/Integration/MarkerEvidenceChannel.Windows.Tests.ps1 `
-Output Detailed -CI
$env:SETUPCM_LAB_PROVIDER_INTEGRATION = '1'
$env:SETUPCM_MARKER_PROVIDER_MODE = 'PreMigration' # or PostMigration
Invoke-Pester ./tests/Integration/MarkerAcceptance.Provider.Tests.ps1 `
-Output Detailed -CI
Build the Windows test source with git archive from the exact reviewed commit,
verify both the archive SHA-256 and embedded commit on CM01, and retain those
values with the result. The target-only detector publication cases must run on
RING0IVY24-01 after asserting its exact computer name. If only Pester 3.4 is
available there, use a temporary mechanically translated test copy; do not edit
the tracked source or production detector, and record both exact source hashes.
Quality gates
- All unit tests must pass before committing.
./scripts/Test-MarkdownLinks.ps1must resolve every local file and heading.mdbook build ./docs/gitbookmust succeed.- PowerShell files must parse and PSScriptAnalyzer must report zero errors.
- Diff/whitespace and staged-secret scans must pass.
- CodeRabbit review is requested after meaningful changes; reproduce findings before changing code and rerun affected plus full suites.
- Pull-request CI runs PowerShell 7, Pester 6, and the link checker; Pages CI repeats the link checker before mdBook.
Future Projects
The following capabilities are separate projects, not optional switches in setup-cm v1:
- production deployment;
- co-management;
- Patch My PC;
- reporting expansion;
- distributed MECM topology;
- alternate hypervisors and provisioning paths;
- additional clients or broad collections;
- tenant integrations;
- client-wide security-policy changes;
- VM rebuild or snapshot rollback;
- historical ConfigMgr-object deletion.
Each needs an independent design, explicit target and credential boundary,
rollback plan, tests, and live acceptance. None may weaken the LabZ1 identity,
hash, one-device collection, trust, or no-rebuild gates to make a v1 rerun
pass. The detailed project boundaries are maintained in
docs/FUTURE-PROJECTS.md.
Handoff Notes
Handoff documents record the state of multi-session work, especially when it spans branches or worktrees. They live in docs/.
Available handoffs
| Document | Date | Branch | Status |
|---|---|---|---|
| HANDOFF-2026-08-02-agent-mecm-client-install.md | 2026-08-02 | codex/mecm-vc-redist | Superseded; Task 5 accepted 2026-08-30 |
HANDOFF-2026-08-02-agent-mecm-client-install
Branch: codex/mecm-vc-redist
Worktree: .worktrees/codex-mecm-vc-redist
Current resolution
This handoff is preserved historical context and must not be used as the
current restart procedure. Task 5 completed through the signed typed Agent path
on RING0IVY24-01; the Phase 0 record contains the accepted work item, client
evidence, provider/SQL registration, and fresh Health proof.
| Task | Where | Status |
|---|---|---|
| 1. Setup-CM Client stage | codex/mecm-vc-redist | Done, committed |
| 2. Server-side registration health gate | codex/mecm-vc-redist | Done, committed |
| 3. Typed Autopilot Agent client work | ProxmoxVEAutopilot origin/main | Done upstream |
| 4. Controller queue endpoint | ProxmoxVEAutopilot origin/main | Done upstream |
| 5. Verify, deploy, prove live path | Accepted LabZ1 | Done; see Phase 0 evidence |
Key commits on codex/mecm-vc-redist
cef1a57 feat: install MECM VC++ prerequisites
d81d5a3 fix: acquire every configured dependency
365ba4d fix: detect x86 MECM VC++ runtime
9160598 docs: design agent MECM client install
ad156c2 docs: plan agent MECM client install
bc4a90c feat: add MECM client installation stage
457df07 feat: verify MECM client server registration
aecd918 fix: preserve empty client evidence logs
dd071e5 fix: wait for MECM client location readiness
See docs/HANDOFF-2026-08-02-agent-mecm-client-install.md in the repository for
the full preserved handoff and its prominent superseded notice. Use
docs/PHASE0-2026-08-29-LAB-INVENTORY.md for current accepted state.