Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

  1. Copy config/lab.example.yaml to config/lab.local.yaml. The local file is ignored by Git.

  2. 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.

  3. Import the module and check readiness:

    Import-Module ./src/SetupCm/SetupCm.psd1 -Force
    Test-SetupCmPreflight -ConfigPath ./config/lab.local.yaml
    
  4. 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

StagePurpose
AcquireObtains and verifies SQL Server and MECM installation media in the configured cache.
SqlInstalls ODBC Driver 18, SQL Server prerequisites, SQL Server, and SQL network configuration.
MecmInstalls MECM prerequisites, ADK, Windows PE, and the primary site; it re-verifies ODBC Driver 18.
MarkerReconciles and verifies the fixed one-device LabZ1 marker deployment.
HealthRechecks 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.exe on PATH, used to verify the commit embedded in the reviewed source archive before extraction.
  • The powershell-yaml module 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

MediaSourcePublic URL?License required?
SQL Server 2022VLSC / Visual Studio Subscription / Evaluation CenterNo (evaluation available)Yes
MECM Current BranchVLSC / Visual Studio SubscriptionNoYes
Windows ADKMicrosoft Download CenterYesAccept EULA
Windows PE add-onMicrosoft Download CenterYesAccept EULA
ODBC Driver 18Microsoft Download CenterYesAccept EULA
VC++ Redistributable x64aka.msYesAccept EULA
VC++ Redistributable x86aka.msYesAccept 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 fwlink URLs 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 update lab.local.yaml if 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

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

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: Eval setting 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:

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.

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.yaml file 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 licenseAccepted fields set to true in lab.local.yaml
  • All sha256 fields updated with actual computed hashes
  • Test-SetupCmPreflight returns Ready: 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

SettingPurpose
topologyDeployment shape. The supplied template uses single-box.
templateKeeps placeholder values valid in the committed example. Remove it or set it to false in a real configuration.
labIdentifies the lab name and domain.
safetyRecords whether the target is isolated and whether production targeting was explicitly acknowledged.
evidenceRootStores one sanitized directory per run; keep it outside the repository.
cacheRootStores verified installer artifacts; keep it outside the repository.
sourcesDeclares each approved installer and its integrity metadata.
sqlConfigures the SQL Server instance, service identity, administrators, and install directory.
mecmConfigures the primary-site identity, SQL endpoint, site server, product identifier, and install paths.
testClientIdentifies the client used by the health stage.
markerAcceptanceEnables 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:

FieldPurpose
uriApproved vendor or private-vault location for the installer or media.
sha256Expected SHA-256 checksum for the downloaded artifact.
publisherExpected publisher for signature validation where applicable.
versionThe approved installer version.
architectureApproved target architecture: x64, x86, or neutral.
sizeBytesExact approved artifact length in bytes.
licenseAcceptedAn explicit acknowledgement that the operator has accepted the applicable license.
cacheFileFilename used under cacheRoot.
signatureRelativePathRequired for ISO media: executable inside the ISO whose signature and native identity are checked.
architectureRelativePathOptional ISO-only executable whose separate valid publisher signature and PE header prove the declared payload architecture. Do not combine it with architectureVerification.
architectureVerificationOptional 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 to false or remove in a real configuration.
  • safety.isolatedLab: true — required for the intended lab use case.
  • licenseAccepted: false — you must explicitly set this to true after 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.
  • evidenceRoot and cacheRoot — 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:

SourceExample versionNotes
SQL Server16.0.1000.6SQL Server 2022 RTM
MECM5.00.9141.1002Native setup.exe ProductVersion for the accepted Current Branch 2509 media
ADK / WinPE10.1.26100.2454Windows 11 24H2 ADK
ODBC Driver 1818.4.1.1Microsoft ODBC Driver 18 for SQL Server
VC++ Redist14.44.35211.0VC++ 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

RoleIdentity
MECM server, provider, MP, and DPLABZ1-CM01.test.gell.one
Site and databaseLAB / CM_LAB
Only accepted clientRING0IVY24-01.test.gell.one
Marker applicationSetup-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.md is the authoritative baseline and safe-restart record.
  • docs/PHASE1-2026-08-29-MARKER-DEPLOYMENT.md proves the required marker is compliant exclusively on RING0IVY24-01.
  • docs/V1-ACCEPTANCE-2026-08-30.md fixes 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.md is 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

RoleAccepted identity
MECM server, provider, MP, and DPLABZ1-CM01.test.gell.one (VM 107)
Site and databaseLAB / CM_LAB
Only accepted clientRING0IVY24-01.test.gell.one (VM 135)
Required applicationSetup-CM Phase 1 Marker
Controllerautopilot-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

EvidenceAccepted value
Pull request#5
Branchcodex/hands-off-rerun-v1
Live-tested commit33535c6a0e47bb0bb0f838eb990b0e9e9cb2ac95
Live-tested treeb0d1329b1fb7fa8479b0b0754cbe168f60fa702b
Git archive size1,075,200 bytes
Git archive SHA-25685c4efb0c36a3575227a078164e35a87e71f777e6bf913e560181779a7b65062
Embedded archive commit33535c6a0e47bb0bb0f838eb990b0e9e9cb2ac95
Extracted 96-file manifest SHA-2565DCC6E72371F3797F2DF34E6CFAA978F70DBC46E8B80FC2B12D2C9B84354E8DD
Read-only acquisition evidence SHA-25610078B52E8002429CB6BAB35D5F4C6B2649A1606F0BE0F7E23CE11CE03A787C8
Ordered pre-run boundary SHA-2565149AFDC75348A4EE28A98F8EB242993BAA0FC62891B7750B1A7C1E06B378727

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

GateResult
Portable Pester suite391 passed, 0 failed, 0 skipped
PowerShell parse45 files, 0 errors
PSScriptAnalyzer error severity0 findings
Markdown links47 files and 97 local links passed
mdBook and workflow lintpassed
Final focused CodeRabbit review0 findings
CM01 native Windows integration12 passed, 0 failed, 5 client-only skips
RING0IVY24-01 native Windows integration10 passed, 0 failed, 0 skipped
ConfigMgr provider post-migration integration1 passed, 0 failed, no mutation
PR Pester CIpassed at the final PR head before merge
PR mdBook buildpassed at the final PR head before merge
PR CodeRabbit statuspassed 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

ObjectAccepted identity
Application CI / revision16777532 / 5
Deployment type CI / revision16777533 / 4
PackageLAB00008
CollectionLAB00016
Sole member resource16777219 (RING0IVY24-01)
Assignment / policy revision16777217 / 5
Marker length78 bytes
Marker SHA-2563F44AA70B40C9E9095E69F1C57E98F6ACC06900788A2054E251BCC58179B6254
Authenticated receipt length254 bytes
Authenticated receipt SHA-256B21E6A787FBA6004D28C69094941F44FC123A835554F24971C565BA1615F1AB7
Receipt ownerTEST\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

EvidenceAccepted value
Run ID20260830-203712-76f13d67
Summary SHA-25615D7C91447D18CA4A63D7A1F56774C8889DE9A211E192F1BAE164FBE6FBE7085
Artifact count11
Health SHA-2562BA005D6D36572C1F864694B0227635F938C8AD7A27FB0EC4E8C2D50B9404593
Stage statesAcquire, SQL, MECM, Health: Skipped; Marker: Succeeded
Allowed actionsRequestClientPolicy, WaitForConvergence
Object or installer mutations0
Installer processes0

All six Health values were true: SQL, MECM, Management Point, Distribution Point, client reachability, and active non-obsolete client registration.

Formal run two

EvidenceAccepted value
Run ID20260830-204052-d3c589a6
Summary SHA-2562938B54FAE5ED15B0AC720873F41AA48E434C05F2BD12AD64329DD59A332E66A
Artifact count11
Health SHA-2562BA005D6D36572C1F864694B0227635F938C8AD7A27FB0EC4E8C2D50B9404593
Stage statesAcquire, SQL, MECM, Marker, Health: Skipped
Mutation actions0
Installer processes0

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

SnapshotSHA-256
Server baselineC606C8DC3CF3B5C39F9B4C21DF0AA17BBA68CE01F2826F3257C1CD7DAA54860C
Server after run oneA7AC8CA3A172ED5D9B13CA4AD320F61EB1EDB147D087A4B1826D12DA5F740AC7
Server after run twoB8B7A291384EE813340FEE9E21FF005956E8D1CE1073B941DCDA0DA38D337D40
Client baseline4B0D5C1782377A8D3DE48CC2EA1F640191E718EEF0184A92CCD17FAC32DC6591
Client after run one9E277628CDDF43EEF4B6A9450C40298753AD702C4FDABF21C7EEEC17491B360E
Client after run two955D56BC279AA6E5B9EE8B61F4FAB2C314D4D6062BE2C283CA2E066DAE061317

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 pve2 reported the two-node cluster quorate.
  • VMs 107, 111, 115, and 135 plus CT 500 were all running.
  • Controller /healthz returned {"ok":true}.
  • Controller release 2026.08.48 reported source 8ba1c80.
  • Accepted Agents agent-labz1-cm01 / VM 107 and agent-ring0ivy24-01 / VM 135 checked in currently at version 2026.8.48.0 and were not revoked.
  • A stale historical osd-fullos controller 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.xml files 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 .tmp receipts, 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

RoleRequired identity
Server, provider, MP, and DPLABZ1-CM01.test.gell.one
Site and databaseLAB / CM_LAB
Only marker targetRING0IVY24-01.test.gell.one, resource 16777219
Marker applicationSetup-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

  1. 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 CurrentUser
    

    PowerShell 7.0 is the deliberate minimum. ProcessStartInfo.ArgumentList is available in .NET Core 3.1, the archive verifier writes bytes directly to the child process stream, and acquisition uses the compatible TimeoutSec on 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.

  2. 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 SHA256
    

    Stage that archive on CM01 and verify its byte hash before extraction. Do not add generated or private files to it.

  3. 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.'
    }
    
  4. 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.

  5. 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:

ItemExact contract
Hidden shareSetupCmMarkerEvidence$ on LABZ1-CM01.test.gell.one
Local targetC:\ProgramData\SetupCm\MarkerEvidence\RING0IVY24-01
Final filemarker-evidence.json, maximum 2,048 bytes
Only client writerTEST\RING0IVY24-01$
SchemaSix exact fields, version 1, ASCII-compatible JSON
Freshness30 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:

  1. Create or safely complete the exact protected directory and hidden share.
  2. Replace only the exact approved predecessor detector through Set-CMScriptDeploymentType; do not alter content or runtime properties.
  3. 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.
  4. 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 resultBehavior
CompliantWrite a Skipped stage result. Do not call Apply.
NotCompliantRepair only owned missing state, verify independently, then write Succeeded.
ConflictWrite Failed and stop before mutation.
Verification not CompliantWrite 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

  1. Preserve the failed run directory.

  2. Read stage-<name>.json and its component-state artifact.

  3. Correct only the proven prerequisite, source, or owned component.

  4. Rerun the failed stage and only its later dependent stages from the same commit, preserving canonical relative order:

    Failed stageRerun stages
    AcquireAcquire,Sql,Mecm,Marker,Health
    SqlSql,Mecm,Marker,Health
    MecmMecm,Marker,Health
    MarkerMarker,Health
    HealthHealth

    The first four rows include Marker and therefore require the same exact SourceCommit; a Health-only recovery does not. For example, after a Sql failure:

    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:

  1. Test — collect structured, read-only component state.
  2. Apply — run only for NotCompliant, and only for owned components.
  3. Verify — independently collect state again.
  4. 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

StageRead-only testBounded apply
AcquireVerifies 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.
SqlVerifies 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.
MecmVerifies 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.
MarkerVerifies 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.
HealthRe-reads SQL, MECM, MP, DP, and active-client state.None.

Marker safety contract

Marker acceptance is fixed to:

  • site LAB on LABZ1-CM01.test.gell.one;
  • target RING0IVY24-01.test.gell.one, resource 16777219;
  • 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."
}
StateMeaning
SucceededApply ran and independent Verify returned Compliant.
SkippedTest returned Compliant; Apply did not run.
FailedTest found conflict, Apply failed, or Verify did not return Compliant.

Component evidence

FileContents
run.jsonRun ID, UTC start, and exact source commit when provided. Marker runs require it.
acquire-state.jsonArtifact component state without source URI or vault location.
acquisition.jsonBounded identities and hashes for every artifact evaluated during Acquire Apply, distinguishing reused Verified entries from AcquiredAndVerified entries.
sql-state.jsonSQL component state and reasons.
mecm-state.jsonMECM component state and reasons.
marker-state.jsonSanitized 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.jsonFresh 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

ScenarioRecommended 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

  1. Preserve the evidence directory from the failed run.

  2. Review the failed stage-<name>.json and corresponding component-state artifact to identify the immediate error.

  3. Correct the stated prerequisite, source, configuration, or host condition.

  4. Rerun the failed stage and only its later dependent stages, preserving canonical relative order:

    Failed stageRerun stages
    AcquireAcquire,Sql,Mecm,Marker,Health
    SqlSql,Mecm,Marker,Health
    MecmMecm,Marker,Health
    MarkerMarker,Health
    HealthHealth

    The first four rows include Marker and therefore require the same exact SourceCommit; a Health-only recovery does not. For example, after a Sql failure:

    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 Apply throws, Verify does 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:

  1. Test whether a stage is already compliant.
  2. Apply only the work required for that stage.
  3. 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>]
ParameterTypeDefaultDescription
ConfigPathstring—Path to the validated private YAML configuration.
ModestringGuidedGuided pauses after each stage; Unattended does not.
Stagestring[]Config-dependentOperator-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.
SourceCommitstringSETUPCM_SOURCE_COMMITFull 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:

  • sqlServer
  • mecm
  • adk
  • adkWinPe
  • odbcDriver18
  • vcRedistX64
  • vcRedistX86

Validation rules

  • safety.isolatedLab must be true, or safety.allowProductionTarget must be explicitly true.
  • template: true allows placeholder values; template: false (or omitted) requires real values.
  • licenseAccepted must be true for every source before acquisition runs.
  • sha256 must be a 64-character hexadecimal string.
  • Every non-template source requires a positive sizeBytes, an expected publisher, a native product version, and an x64, x86, or neutral architecture. sources.mecm.version is the four-component ProductVersion of its signed setup.exe, not a Current Branch label.
  • architectureRelativePath is ISO-only and validates the separate signed payload’s PE architecture. architectureVerification: signedVersionResource is executable-only and accepts a signed x86 launcher for an x64 payload only when signed version-resource identities unambiguously name x64. A source may select at most one of these architecture proofs, and only when its declared architecture is x64 or x86.
  • mecm.siteCode must match ^[A-Z0-9]{3}$.
  • mecm.sqlServer and mecm.siteServerFqdn must match the lab domain.
  • testClient.name and testClient.domain must identify a separate, domain-joined client.
  • Enabled marker acceptance requires labOnly: true and these exact values: siteCode: LAB, siteServerFqdn: LABZ1-CM01.test.gell.one, targetFqdn: RING0IVY24-01.test.gell.one, and targetResourceId: 16777219.

Runtime-only inputs

The private YAML path and source provenance are supplied outside the schema:

InputContract
SETUPCM_CONFIG or -ConfigPathPath to the non-template YAML staged separately from source.
SETUPCM_SOURCE_COMMIT or -SourceCommitFull 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

FileProduced byPurpose
acquire-state.jsonAcquire Test/VerifyPer-artifact compliance, reason, size, hash, version, and architecture.
acquisition.jsonAcquire ApplyBounded identity and hash metadata for every artifact evaluated during Apply, distinguishing reused Verified entries from AcquiredAndVerified entries.
sql-state.jsonSQL Test/VerifyOverall state plus SQL component state and reasons.
mecm-state.jsonMECM Test/VerifyOverall state plus site, role, prerequisite, service, content-library, and client components.
marker-state.jsonMarker Test/VerifyExact boundary, payload, application/DT, content/distribution, collection/member, assignment/policy, client projection, and per-device server compliance.
health.jsonHealth Test/VerifyFresh Boolean read-only checks in deterministic key order.
client-install.jsonTyped client commandSanitized 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:

  1. ProxmoxVEAutopilot OSDeploy Server created a Windows Server 2022 VM with the base role
  2. The server was verified as base_ready and domain_joined=true
  3. A staged setup-cm source bundle and a non-template lab.local.yaml were placed on the server
  4. Verified SQL Server, MECM current-branch, and prerequisite media were staged in the private vault
  5. Test-SetupCmPreflight confirmed readiness
  6. A separate Windows client VM was provisioned and domain-joined
  7. Invoke-SetupCm ran the full Acquire → Sql → Mecm → Health sequence
  8. The Health stage 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 kindPurposeKey constraints
setup_cm_acquireDownload and verify installation mediaApproved URIs only; SHA-256 pinned
setup_cm_sqlInstall SQL ServerNon-template config required; isolated-lab check
setup_cm_mecmInstall MECM primary siteVC++ 14.34+ gate; approved prerequisite path
setup_cm_healthRun health validationRead-only; no state changes
setup_cm_client_installInstall MECM client on a targetFQDN 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:

  1. Constraint: The management point FQDN must end in the approved lab domain suffix. Any other domain is rejected before execution.
  2. Pinning: The module archive must match a 64-character SHA-256 provided at queue time. The Agent verifies the hash before extraction.
  3. Derivation: The only permitted installer path is \\<managementPoint>\SMS_<siteCode>\Client\ccmsetup.exe. No arbitrary UNC paths.
  4. Evidence: The Agent captures sanitized ccmsetup.log and ClientIDManagerStartup.log tails, redacting any Password= 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:

  1. Update the Agent version in the project file
  2. Build and sign the MSI on a Windows build machine
  3. Publish the MSI to the private distribution point
  4. 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 evidenceRoot as 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

CapabilityStatus
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.

CapabilityWhat’s needed
VM provisioningPowerShell Direct or Hyper-V VM creation scripts
OS deploymentExisting WDS/MDT, or custom unattend.xml injection
Domain joiningUnattend.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.

CapabilityWhat’s needed
VM provisioningPowerCLI New-VM or vmrun with template VMs
OS deploymentVMware Tools + unattended installation, or template clones
Domain joiningUnattend.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-cm PowerShell module and stage engine
  • The YAML configuration contract
  • The evidence format and stage-<name>.json results
  • 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-cm source 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:

  1. Ensure the platform can create an isolated, domain-joined Windows Server and a separate test client
  2. Stage the setup-cm source bundle and a non-template lab.local.yaml on the server
  3. Run Test-SetupCmPreflight to validate the configuration
  4. Choose your execution model: local terminal or Autopilot Agent
  5. 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

DocumentDateTopic
2026-07-30 SCCM Lab Automation Design2026-07-30Original single-box lab automation design
2026-08-01 Agent MECM Client Install Design2026-08-01Typed client work contract and manifest design
2026-08-01 MECM VC++ Redist Dependency Design2026-08-01VC++ runtime detection and gating design
2026-08-30 Hands-Off Rerun and v1 Release Design2026-08-30Idempotent core stages, marker automation, two-run acceptance, and v1 release
2026-08-30 Authenticated Marker Client Evidence Channel Design2026-08-30Fail-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 / Verify for 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.exe until 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

PlanDateStatusTasks
2026-07-30 Single-Box SCCM Core2026-07-30Complete8 tasks
2026-08-01 Agent MECM Client Install2026-08-01Complete; live Task 5 accepted5 tasks
2026-08-01 MECM VC++ Redist Dependency2026-08-01Complete; live x64/x86 baseline accepted3 tasks
2026-08-30 Hands-Off Rerun and v1 Release2026-08-30Tasks 1–9 complete; Task 10 release finalization10 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:

  1. Scaffold the PowerShell 7 module and Pester 6 test harness.
  2. Define and validate the non-secret YAML configuration contract.
  3. Implement verified acquisition from vendor sources and private installer vaults.
  4. Build the resumable stage engine and Autopilot Agent entry script.
  5. Implement Windows prerequisites and the single-box SQL Server stage.
  6. Implement MECM primary-site and core-role stages.
  7. Add live health validation, client proof, and CMTrace Open fixture curation.
  8. 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:

  1. Setup-CM Client stage — complete.
  2. Server-side registration health gate — complete.
  3. Typed Autopilot Agent client work — complete upstream.
  4. Controller queue endpoint — complete upstream.
  5. 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:

  1. Pin runtime detection with Pester — complete.
  2. Pin verified installation and stage order — complete.
  3. 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:

  1. Reconcile current project status and historical handoffs — complete.
  2. Pin evidence and compliance contracts — complete.
  3. Make Acquire read-only before apply — complete.
  4. Implement SQL desired-state reconciliation — complete.
  5. Implement MECM and read-only Health probes — complete.
  6. Productize marker acceptance — complete.
  7. Document and package the v1 workflow — complete.
  8. Complete branch CI and review — complete at the live-tested head.
  9. Run Windows/provider integration and two live runs — complete and accepted.
  10. 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 fileCoverage
Module.Tests.ps1Module load, exported commands, PowerShell version
Configuration.Tests.ps1YAML parsing, schema validation, safety guardrails
Acquisition.Tests.ps1Exact cache state, affected-artifact-only acquisition, source redaction
StageEngine.Tests.ps1Idempotency, skip logic, failure handling, evidence writes
Sql.Tests.ps1SQL component state, bounded repair, conflict and verification gates
Mecm.Tests.ps1Site/component state, bounded repair, media skip and conflict gates
MarkerAcceptance.Tests.ps1Fixed boundary, provider reconciliation, no-op behavior, evidence provenance
MarkerEvidenceChannel.Tests.ps1Strict six-field parser, SID/ACL/share state, proof precedence, freshness, and bounded convergence
MarkerApplication.Tests.ps1Exact payload/detector and safe uninstall behavior
Health.Tests.ps1Read-only SQL/MECM/MP/DP/client checks and fresh evidence
Client.Tests.ps1Manifest validation, install arguments, rerun logic, redaction
Media.Tests.ps1Media path helpers
AgentEntrypoint.Tests.ps1Script entry-point contract
MarkdownLinks.Tests.ps1Portable local file/heading link validation

Integration tests

The integration suite in tests/Integration/ has four independent boundaries:

FileGate
CoreStages.Windows.Tests.ps1Real SQL, MECM, and read-only Health state on CM01, with installer/setup calls guarded.
MarkerDetection.Windows.Tests.ps1Exact VBScript detection plus strict target-only authenticated publication for valid, tampered, missing, transport-failure, and test-path cases.
MarkerEvidenceChannel.Windows.Tests.ps1Real Windows protected ACL construction, SID-normalized inventory, and bounded reads in temporary paths.
MarkerAcceptance.Provider.Tests.ps1Exact 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.ps1 must resolve every local file and heading.
  • mdbook build ./docs/gitbook must 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

DocumentDateBranchStatus
HANDOFF-2026-08-02-agent-mecm-client-install.md2026-08-02codex/mecm-vc-redistSuperseded; 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.

TaskWhereStatus
1. Setup-CM Client stagecodex/mecm-vc-redistDone, committed
2. Server-side registration health gatecodex/mecm-vc-redistDone, committed
3. Typed Autopilot Agent client workProxmoxVEAutopilot origin/mainDone upstream
4. Controller queue endpointProxmoxVEAutopilot origin/mainDone upstream
5. Verify, deploy, prove live pathAccepted LabZ1Done; 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.