12 KiB
Alternative to MDM for base-image policy grants
Opened: 2026-07-26
Context: the MDM lane enrolls the guest successfully but the command drain is blocked on macOS 27
by apsd/APNs (see docs/APPLE_FEEDBACK_APNS_VM.md and the repair plan's third investigation). This
document plans a policy-delivery path that does not depend on MDM, APNs, or OCR-driven GUI
navigation.
Status: implemented (provisioning recipe v9, the default lane)
Lane B is now the default base-build policy lane. Where it lands:
- Settings.
MacVMSettings.policyLane(nucleic.macvm.policyLane) selects the lane; it defaults to.datastore(Lane B).defaults write <app-domain> nucleic.macvm.policyLane -string mdmfalls back to Lane A, whose whole subsystem (NucleicMDM, the CA/queue/HTTPS server, the OCR approval loop) is retained behind that flag. - The pass.
MacVMEngine.policyGrantsPass(MacVMEngine+PolicyGrants.swift) runs the four-boot state machine below. It replacesmdmPolicyPassas Phase 2 ofprovisionAndFinalize.- Recovery boot uses the new
MacVMInstance.startInRecovery()(VZMacOSVirtualMachineStartOptions.startUpFromMacOSRecovery); the fixedcsrutilcommand is typed through the display surface by a bounded OCR loop (reusing the MDM loop's Vision + fuzzy matchers). Success is confirmed by the next normal boot's in-guestcsrutil status, never by the screen. - The writes + read-back run over the vsock exec channel (no share mount, so the run itself
can't trip the Network Volumes prompt it grants) via
scripts/macvm-policy-grants.sh, base64'd on stdin with the csreq blob passed as asudoenv var. - The
csreqblob comes fromdesignatedRequirementData(forAgentAt:)—SecRequirementCopyDataon the sameSecRequirementthe PPPC rewrite already derives.
- Recovery boot uses the new
- Gating.
MacVMBaseStatus.policyGrantsVerifiedis the new invariant;baseStatusReadyForRecipeandbaseIsUsableare lane-aware. The recipe stamp bumps to v9, forcing every v8 base through the datastore lane. - Spikes. The two load-bearing risks below are checked at build time, not assumed: the pass
fails closed if
csrutil disable/enabledidn't take (theSIP=read-back) or if a grant did not survive re-enabling SIP (the SIP-on read-back — Risk 1). Both still want a first confirmed run on real macOS 27 hardware; until then Lane A remains onedefaults writeaway.
What the base actually needs granted
| Need | Why | Silently grantable? |
|---|---|---|
Full Disk Access for NucleicVMAgent |
removes "Data Access Blocked" | PPPC (MDM) or a TCC.db row |
Network Volumes for NucleicVMAgent |
removes the virtioFS custom-mount prompt | PPPC (MDM) or a TCC.db row |
Notification suppression (com.apple.tips, com.apple.BTMNotificationAgent) |
removes Tips / "Multiple Extensions Added" | notificationsettings (MDM), an ncprefs write, or disabling the source |
| (optional) Screen Recording / Accessibility for the in-guest agent's legacy fallbacks | semantic AX / screencapture path |
Screen Recording can never be granted by any profile — only a TCC.db row or a user click |
Correcting the premise: plain configuration profiles do not work here
A manually installed (non-MDM) .mobileconfig is silently ignored for exactly the two payload
types this base needs — com.apple.TCC.configuration-profile-policy (PPPC) and
com.apple.notificationsettings. Apple honors those only when the profile is delivered by an
approved MDM. So "standard configuration profiles + HID" cannot grant FDA or suppress notifications:
the profile installs and is inert. That is the whole reason the MDM subsystem exists.
That leaves three real levers, and the naming below is used throughout:
- Lane A — MDM (current): enroll (works), deliver PPPC/notificationsettings by InstallProfile. The only broken piece is the command drain trigger, and it is fixable without APNs (reboot → login-time Idle sync, proven live) — see the repair plan, workstream D.2. Keeps SIP on. Lowest code delta to a working base, but keeps the OCR-driven enrollment approval and the beta-fragile Apple-services dependency on the critical path.
- Lane B — deterministic datastore writes under transient SIP-off (recommended alternative): no MDM, no APNs, no OCR-for-privileges. Detailed below.
- Lane C — HID grants through the real Privacy & Security GUI: keeps SIP on, no MDM, but drives the exact OCR/click automation that cost ~25 runs, and still needs SIP-off for Screen Recording. Kept only as a per-grant fallback, not a primary design.
Recommended: Lane B — deterministic datastore provisioning
Grant everything by writing the system's own datastores directly, with SIP disabled only for the duration of the write and re-enabled before the base is sealed. Every step is a deterministic file or SQL operation — no screen reading, no Apple online service, no push.
Why this is the better design
- Deterministic. A
sqlite3 INSERTeither succeeds or errors; there is nothing to OCR, no sheet focus race, no push token to wait on. This is the opposite of the failure class that dominated the MDM repair log. - Offline. Nothing contacts Apple. Immune to the 27-beta apsd/BAA regression and to future beta churn in enrollment UI.
- Strictly more capable. A TCC.db row can grant Screen Recording, which no MDM PPPC payload can ever grant silently. Lane B subsumes the SIP-off screen-recording lane the base already contemplated, so it is one mechanism for all grants instead of two.
- Final posture unchanged (pending validation). TCC rows are honored by matching the requesting binary's code requirement, not by who wrote the row, so grants written while SIP was off are expected to survive re-enabling SIP. The sealed base ships SIP-on with the grants baked in. (This is the one load-bearing assumption; it is the first thing the spike must confirm — see Risks.)
Flow (a new base-build boot sequence)
- Recovery boot → disable SIP. Boot the writable base with
VZMacOSVirtualMachineStartOptions.startUpFromMacOSRecovery = true, open Terminal from the Recovery Utilities menu via the host HID surface, and runcsrutil disable(one fixed command in a known context — not OCR navigation), then reboot. On a VM there is no firmware-password / SEP auth gate on this, but confirm on 27 (Risks). - Normal boot → write grants (SIP off):
- TCC (FDA / Network Volumes / optional Screen Recording + Accessibility):
sqlite3INSERT into/Library/Application Support/com.apple.TCC/TCC.db,accesstable, one row per service:kTCCServiceSystemPolicyAllFiles,kTCCServiceSystemPolicyNetworkVolumes,kTCCServiceScreenCapture,kTCCServiceAccessibility.client= the agent bundle id,client_type = 0(bundle id),auth_value = 2(allowed),auth_reason = 3, and the compiled code-requirement blob incsreq. - Reuse the requirement Nucleic already computes. The MDM PPPC path already derives the
agent's designated requirement from the exact staged binary via Security.framework
(
SecStaticCodeCreateWithPath→SecCodeCopyDesignatedRequirement→SecRequirementCopyString, see the repair plan's "first live provisioning retry"). For TCC, take the sameSecRequirementRefand storeSecRequirementCopyData(the compiled blob) incsreq. So Developer-ID and ad-hoc dev builds both authorize the binary actually installed — identical guarantee to the current PPPC template rewrite. - Notifications: prefer disabling the source over suppressing the alert — more robust than
the version-sensitive
ncprefsformat: neutralizecom.apple.tips(e.g. remove/disable tipsd's job and setdefaults write com.apple.Tips …) and the BTM notifier. Where an alert store write is unavoidable, write~/Library/Preferences/com.apple.ncprefs.plistflags for the two bundle ids. Pin whichever is chosen with an on-guest read-back assertion. - Verify in-guest, not by exit code: re-open TCC.db read-only and assert each expected row is
present with
auth_value = 2and a non-nullcsreq; the build fails here, named, if not.
- TCC (FDA / Network Volumes / optional Screen Recording + Accessibility):
- Recovery boot → re-enable SIP.
csrutil enable, reboot. - Confirm grants survived SIP re-enable, then continue to toolchain/readiness and seal. The recipe stamp gates on the same in-guest read-back, now under SIP-on.
Where this lands in the codebase
- The removed Phase 7c SIP/TCC branch (
scripts/provision-macos-guest.sh:510-527) is the natural home for the write step — rebuilt against the code-requirement blob rather than the old bundle-only INSERT. - The recovery-boot +
csrutiltoggle is new: a small state machine analogous to the MDM approval loop but with a fixed action (open Terminal, type one command) instead of OCR classification.MacVMSurfaceHostalready types the bootstrap launch into Terminal, so the input path exists. - The requirement-blob helper extends the existing Security.framework requirement extraction used by
the PPPC rewrite (no subprocess, no
codesignscraping). - Gating moves from
mdmEnrolled/profilesInstalledto apolicyGrantsVerifiedinvariant backed by the in-guest TCC/notification read-back; the recipe-stamp gate swaps predicates.
What it removes / demotes
- MDM enrollment, the CA, PKCS#12 identity, the HTTPS server, the command queue, and the entire
OCR-driven UAMDM approval loop leave the critical path. Keep the
NucleicMDMtarget and Mode B behind a flag for future Declarative Device Management or fleet management, but nothing in a base build depends on Apple push or on winning an OCR fight.
Comparison
| Lane A (MDM, no-APNs drain) | Lane B (datastore, transient SIP-off) | Lane C (HID GUI) | |
|---|---|---|---|
| Grants FDA / Network Volumes | yes | yes | yes |
| Grants Screen Recording | no | yes | yes (1 click) |
| Suppresses notifications | yes | yes | yes |
| Needs Apple online services | yes (apsd — broken on 27) | no | no |
| OCR / screen reading | yes (enrollment approval) | no (one fixed recovery command) | yes (heavy) |
| Determinism | medium | high | low |
| Final SIP posture | on | on (pending validation) | on (off for Screen Recording) |
| New code | small (D.2 only) | medium (recovery toggle + TCC blob) | medium (robust OCR) |
| Beta-fragility | high | low | medium |
Recommendation
- To unblock publishing immediately with the least change and SIP guaranteed on: finish Lane A's reboot/login-sync drain (repair plan D.2). The base is one reboot away from a working pass.
- As the durable design — and the "alternative" this document plans — build Lane B. It deletes the MDM/APNs/OCR dependency class outright, is the most deterministic option, and is the only one that also covers Screen Recording. Gate its adoption on the SIP-re-enable-preserves-grants spike below.
- Reject plain configuration profiles (inert for PPPC/notifications) and OCR-driven Privacy-panel automation as a primary mechanism.
Risks / spikes before committing to Lane B
- Does a TCC/ncprefs grant written under SIP-off survive
csrutil enable? The load-bearing assumption. Spike: write one FDA row SIP-off, re-enable SIP, reboot, confirm the row is honored (agent reads a protected path without prompt). If it does not survive, Lane B's final base must stay SIP-off (still viable for a disposable self-owned VM, but a posture change to accept explicitly). - Does
csrutil disablework in a macOS 27 VM Recovery without a SEP/firmware-password gate? Expected yes for VMs; confirm, since the same missing-SEP reality that breaks apsd could affect the LocalPolicy path that stores SIP state. - TCC.db schema /
csreqblob format on macOS 27. Pin the exactaccess-table columns and the compiled-requirement encoding with an on-guest read-back test; Apple has changed this table before. - Notification store shape.
ncprefsis undocumented and version-sensitive — prefer disabling the notifying source; assert the result by read-back rather than trusting the write.