# 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 mdm` falls
  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 replaces `mdmPolicyPass` as Phase 2 of `provisionAndFinalize`.
  - Recovery boot uses the new `MacVMInstance.startInRecovery()`
    (`VZMacOSVirtualMachineStartOptions.startUpFromMacOSRecovery`); the fixed `csrutil` command 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-guest `csrutil 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 a `sudo` env var.
  - The `csreq` blob comes from `designatedRequirementData(forAgentAt:)` —
    `SecRequirementCopyData` on the same `SecRequirement` the PPPC rewrite already derives.
- **Gating.** `MacVMBaseStatus.policyGrantsVerified` is the new invariant; `baseStatusReadyForRecipe`
  and `baseIsUsable` are 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`/`enable` didn't take (the `SIP=` 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 one `defaults write` away.

## 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 INSERT` either 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)

1. **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 run `csrutil 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).
2. **Normal boot → write grants (SIP off):**
   - **TCC (FDA / Network Volumes / optional Screen Recording + Accessibility):** `sqlite3` INSERT
     into `/Library/Application Support/com.apple.TCC/TCC.db`, `access` table, 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** in `csreq`.
   - **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 same `SecRequirementRef` and store `SecRequirementCopyData` (the compiled blob) in
     `csreq`. 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 `ncprefs` format: neutralize `com.apple.tips` (e.g. remove/disable
     tipsd's job and set `defaults write com.apple.Tips …`) and the BTM notifier. Where an alert
     store write is unavoidable, write `~/Library/Preferences/com.apple.ncprefs.plist` flags 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 = 2` and a non-null `csreq`; the build fails here, named, if not.
3. **Recovery boot → re-enable SIP.** `csrutil enable`, reboot.
4. **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 + `csrutil` toggle 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.
  `MacVMSurfaceHost` already 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 `codesign` scraping).
- Gating moves from `mdmEnrolled` / `profilesInstalled` to a `policyGrantsVerified` invariant 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 `NucleicMDM` target 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

1. **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).
2. **Does `csrutil disable` work 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.
3. **TCC.db schema / `csreq` blob format on macOS 27.** Pin the exact `access`-table columns and the
   compiled-requirement encoding with an on-guest read-back test; Apple has changed this table
   before.
4. **Notification store shape.** `ncprefs` is undocumented and version-sensitive — prefer disabling
   the notifying source; assert the result by read-back rather than trusting the write.
