Merge nucleic/mellow-drift-lemur-o6gt into dev

This commit is contained in:
2026-07-14 04:59:48 -07:00
parent 90de1b3316
commit 643ab58fd6
6 changed files with 191 additions and 34 deletions
@@ -80,7 +80,27 @@ extension MacVMEngine {
"the configured prebuilt base at \(prebuilt) is missing its VM artifacts")
}
let built = MacVMBundle(root: builtBaseDir)
if built.isComplete { return built }
if built.isComplete {
// Refuse to clone a base whose provisioning never landed the in-guest agent: the control
// plane is vsock-only, so such a clone can NEVER become reachable it would sit at
// "Booting" for the whole agent deadline and then fail opaquely. Fail fast and say why.
// Only bundles that recorded a provisioning status are judged; a pre-metadata bundle
// passes through as before. The launch staleness check auto-repairs an unusable base;
// this error covers the window until that (or a manual rebuild) completes.
let status = readBaseStatus(built)
if status.installed, !status.agentInstalled {
if baseIsBusy {
throw MacVMError.baseBusy(
"the base image is being (re)provisioned right now — retry once it finishes")
}
throw MacVMError.baseImageMissing(
"the base macOS image exists, but its provisioning pass didn't complete (the "
+ "in-guest agent is not installed), so VMs cloned from it can never be "
+ "reached. It will be re-provisioned automatically at the next app launch, "
+ "or run Settings ▸ Virtual Machines ▸ Build base image now.")
}
return built
}
throw MacVMError.baseImageMissing(
"no base macOS image has been built yet. Build it once via Settings ▸ Virtual Machines ▸ "
+ "Build base image, or `scripts/build-macos-base.sh`.")
@@ -141,33 +141,49 @@ extension MacVMEngine {
) async throws {
baseProgress = MacVMBaseProgress(
phase: declarativeFirstBoot ? .firstBootSetup : .provisioning, fraction: nil)
let result: ProvisionResult
do {
// The native agent is the sole hostguest control channel now, so it is ALWAYS installed
// (exec + computer-use ride it). Its AX *TCC grants* remain SIP-gated and are the only part
// `MacVMSettings.axAgentEnabled` affects downstream (see `axAgentReady`).
let result = try await provisionBase(
result = try await provisionBase(
bundle: bundle, installAgent: true, declarativeFirstBoot: declarativeFirstBoot,
carryAppPaths: carryAppPaths, carryPackageIDs: carryPackageIDs)
var status = base
// Reaching a shell + running the bootstrap means the account exists and auto-login works.
status.accountProvisioned = status.accountProvisioned || declarativeFirstBoot
status.provisioned = result.provisioned
status.agentInstalled = result.agentInstalled
status.sipDisabled = result.sipDisabled
status.axAgentReady = result.axAgentReady
// Stamp the recipe this pass provisioned under, so a later app update that bumps the recipe
// can detect this base as stale and force a re-provision (MacVMEngine+Reprovision).
status.provisioningRecipe = Self.macOSProvisioningRecipe
// Record what was actually staged (Settings carried-forward), so the NEXT rebuild that
// replaces this base can carry these apps/packages forward in turn.
status.installedAppPaths = result.installedAppPaths
status.installedPackageIDs = result.installedPackageIDs
writeBaseStatus(bundle, status)
baseProgress = MacVMBaseProgress(phase: .ready, fraction: 1)
} catch {
baseProgress = nil
throw MacVMError.provisionFailed(String(describing: error))
}
var status = base
// Reaching a shell + running the bootstrap means the account exists and auto-login works.
status.accountProvisioned = status.accountProvisioned || declarativeFirstBoot
status.provisioned = result.provisioned
status.agentInstalled = result.agentInstalled
status.sipDisabled = result.sipDisabled
status.axAgentReady = result.axAgentReady
// Stamp the recipe this pass provisioned under ONLY when the pass actually landed the
// in-guest agent. The agent is the sole hostguest control plane, so a pass without it left
// the base unusable for exec/computer-use; stamping it anyway is what once wedged bases at
// "recipe up to date" + `provisioned: false` the launch staleness check saw nothing to
// redo, and every clone booted agentless, pinned at "Booting" until the vsock deadline.
// Leaving the old stamp keeps the base detectably stale so the next launch retries.
if result.agentInstalled {
status.provisioningRecipe = Self.macOSProvisioningRecipe
}
// Record what was actually staged (Settings carried-forward), so the NEXT rebuild that
// replaces this base can carry these apps/packages forward in turn. Written even for a failed
// pass the settings panel shows the partial flags rather than pretending nothing ran.
status.installedAppPaths = result.installedAppPaths
status.installedPackageIDs = result.installedPackageIDs
writeBaseStatus(bundle, status)
guard result.agentInstalled else {
baseProgress = nil
throw MacVMError.provisionFailed(
"the pass finished without installing the in-guest agent"
+ (result.provisioned ? "" : " (the provision script exited nonzero)")
+ " — clones of this base would be unreachable. Check the build monitor / "
+ "provision log and run Settings ▸ Virtual Machines ▸ Build base image again.")
}
baseProgress = MacVMBaseProgress(phase: .ready, fraction: 1)
}
/// Provision the base **entirely host-side + VirtioFS, no network** (docs/MACOS_VM.md §4.4):
@@ -44,6 +44,18 @@ extension MacVMEngine {
}
}
/// Whether a base's recorded status marks its last provisioning pass as having landed everything
/// clones depend on. For macOS that is the toolchain pass **and** the in-guest agent (the vsock
/// control plane rides it without the agent no clone is ever reachable); a Linux base bakes its
/// agent into the rootfs during the provision build, so `provisioned` alone is the signal. Pure,
/// unit-tested; drives the launch staleness check's retry-on-failed-pass behavior.
static func baseIsUsable(_ status: MacVMBaseStatus, os: GuestOS) -> Bool {
switch os {
case .macOS: return status.provisioned && status.agentInstalled
case .linux: return status.provisioned
}
}
/// The outcome of a staleness check, for logging/telemetry.
public enum BaseRecipeCheck: Sendable, Equatable {
case noBase // nothing built to check
@@ -76,15 +88,35 @@ extension MacVMEngine {
var status = readBaseStatus(bundle)
status.os = os
// Grandfather a pre-tracking base: record the current recipe, don't reprovision.
// A recipe stamp matching or absent proves nothing for an unusable base: a failed pass
// must be retried regardless, or every clone boots unreachable and sits at "Booting" until
// the vsock deadline.
let usable = Self.baseIsUsable(status, os: os)
// Grandfather a pre-tracking base: record the current recipe, don't reprovision. Only for a
// usable base grandfathering a half-provisioned one (e.g. a fresh install whose provisioning
// pass failed before ever stamping a recipe) would permanently wedge it as "up to date".
guard let recorded = status.provisioningRecipe else {
guard usable else {
return await reprovision(os: os, bundle: bundle, status: status)
}
status.provisioningRecipe = current
writeBaseStatus(bundle, status)
return .grandfathered
}
if recorded == current { return .upToDate }
if recorded == current, usable { return .upToDate }
// Stale. Don't stomp an in-flight build/Recovery; the next launch will catch it.
return await reprovision(os: os, bundle: bundle, status: status)
}
/// Run the actual re-provision for a base found stale (recipe behind) or unusable (last pass never
/// landed the control plane). Split from ``reprovisionStaleBaseIfNeeded(for:)`` so both triggers
/// share the busy guard and the per-OS build path.
private func reprovision(
os: GuestOS, bundle: MacVMBundle, status: MacVMBaseStatus
) async -> BaseRecipeCheck {
var status = status
// Don't stomp an in-flight build/Recovery; the next launch will catch it.
guard !baseIsBusy else { return .busy }
#if arch(arm64)
@@ -93,11 +125,10 @@ extension MacVMEngine {
case .macOS:
// The reentrant `buildBaseImage` path skips the multi-GB reinstall on a complete base
// and re-runs only the provisioning pass, which re-stages the *current* bundled script
// + agent and re-stamps the recipe (see `provisionAndFinalize`). It needs the account
// already provisioned; a base that never got that far can't be reprovisioned unattended.
guard status.accountProvisioned else {
return .reprovisionFailed("the base has no provisioned agent account yet")
}
// + agent and re-stamps the recipe (see `provisionAndFinalize`). A base with no
// provisioned agent account falls through to `buildBaseImage`'s clean-reinstall path
// (the macOS-27 declarative first-boot slot was consumed; reinstall is the only
// unattended fix, and the cached IPSW makes it a reinstall, not a re-download).
try await buildBaseImage()
case .linux:
// A Linux base bakes the agent + toolchain into the rootfs at build time, so a genuine
+8 -4
View File
@@ -306,11 +306,14 @@ public actor MacVMEngine {
@discardableResult
public func ensureRunning(_ spec: MacVMSpec) async throws -> (name: String, ipAddress: String) {
lastSpec[spec.name] = spec
if let existing = live[spec.name], let ip = existing.ipAddress {
// Reuse keys on `ready` NOT on `ipAddress`. The control plane is vsock-only, so a fully
// usable guest routinely has no NAT lease for its whole life; gating reuse on the IP would
// send every follow-up call for a live VM into `performBoot`, double-booting its own disk.
if let existing = live[spec.name], existing.ready {
// Transparently thaw a VM the agent suspended (`*_vm_control suspend`) before handing it
// back, so exec/computer-use never has to know it was frozen.
if existing.paused { _ = await resume(name: spec.name) }
return (spec.name, ip)
return (spec.name, existing.ipAddress ?? "")
}
// Join an in-flight boot for the same name rather than starting a second one.
if let inFlight = boots[spec.name] {
@@ -338,9 +341,10 @@ public actor MacVMEngine {
while live.count + boots.count >= limit {
// Another call may have booted or joined this exact VM while we were parked re-honor the
// dedup/reuse fast paths before consuming a slot for a duplicate of a VM that now exists.
if let existing = live[spec.name], let ip = existing.ipAddress {
// Same rule as the fast path above: reuse keys on `ready`, never on the NAT IP.
if let existing = live[spec.name], existing.ready {
if existing.paused { _ = await resume(name: spec.name) }
return (spec.name, ip)
return (spec.name, existing.ipAddress ?? "")
}
if let inFlight = boots[spec.name] { return try await inFlight.value }
// Reclaim the stalest done chat's VM if the policy offers one. Prefer **suspend-to-disk**:
+81
View File
@@ -1134,4 +1134,85 @@ import Testing
#expect(MacVMSettings.selectedPackageIDs == ["chrome"])
}
}
// MARK: - Failed-provisioning wedge (base without the in-guest agent)
/// Regression: on a macOS beta guest `softwareupdate -l` lists no Command Line Tools, so the CLT
/// label lookup's `grep` matches nothing. Under the script's `set -euo pipefail` an unguarded
/// pipeline there aborts the ENTIRE provision run before the on-demand-installer fallback
/// which is exactly how the 2026-07-14 base ended up published with no in-guest agent (every
/// clone then sat at "Booting" until the vsock deadline). The `|| true` guards are load-bearing.
@Test func macOSGuestProvisionerSurvivesEmptyCLTCatalog() throws {
let repoRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
let provisioner = repoRoot.appendingPathComponent("scripts/provision-macos-guest.sh")
let script = try String(contentsOf: provisioner, encoding: .utf8)
#expect(script.contains("set -euo pipefail")) // the hazard the guards exist for
// CLT label lookup: no match must yield an empty label, not a dead script.
#expect(script.contains("sort -V | tail -1 || true"))
// codesign designated-requirement extraction: an unsigned bundle degrades to csreq NULL.
#expect(script.contains("sed -n 's/^designated => //p' || true"))
}
/// The launch staleness check must treat a base whose last pass failed as needing re-provisioning
/// even when its recipe stamp matches a stamp proves nothing about a pass that didn't land the
/// control plane. macOS needs the toolchain AND the in-guest agent (exec/computer-use are
/// vsock-only); Linux bakes its agent during the provision build, so `provisioned` is the signal.
@Test func baseUsabilityRequiresTheControlPlane() {
func status(provisioned: Bool, agent: Bool) -> MacVMBaseStatus {
var s = MacVMBaseStatus()
s.installed = true
s.accountProvisioned = true
s.provisioned = provisioned
s.agentInstalled = agent
return s
}
#expect(MacVMEngine.baseIsUsable(status(provisioned: true, agent: true), os: .macOS))
#expect(!MacVMEngine.baseIsUsable(status(provisioned: false, agent: false), os: .macOS))
#expect(!MacVMEngine.baseIsUsable(status(provisioned: true, agent: false), os: .macOS))
#expect(!MacVMEngine.baseIsUsable(status(provisioned: false, agent: true), os: .macOS))
#expect(MacVMEngine.baseIsUsable(status(provisioned: true, agent: false), os: .linux))
#expect(!MacVMEngine.baseIsUsable(status(provisioned: false, agent: false), os: .linux))
}
/// A complete-on-disk base whose provisioning never installed the in-guest agent must be refused
/// at clone time with an actionable error a clone of it can never answer the vsock readiness
/// probe, so booting it would burn the whole agent deadline stuck at "Booting". Once the status
/// records the agent, the same bundle resolves normally.
@Test func ensureBaseBundleRefusesAgentlessBase() async throws {
// A fresh defaults suite so a developer machine's `basePrebuiltPath` can't reroute the lookup.
let suite = UserDefaults(suiteName: "macvm-base-\(UUID().uuidString)")!
try await ContainerServiceSettings.withDefaults(suite) {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("macvm-base-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let engine = MacVMEngine(storageRoot: root)
let baseDir = root.appendingPathComponent("base", isDirectory: true)
try FileManager.default.createDirectory(at: baseDir, withIntermediateDirectories: true)
let bundle = MacVMBundle(root: baseDir)
for artifact in [
bundle.hardwareModelURL, bundle.machineIdentifierURL,
bundle.auxiliaryStorageURL, bundle.diskImageURL,
] {
try Data().write(to: artifact)
}
func writeStatus(agentInstalled: Bool) throws {
let json: [String: Any] = [
"installed": true, "accountProvisioned": true, "provisioned": false,
"agentInstalled": agentInstalled, "provisioningRecipe": 4,
]
try JSONSerialization.data(withJSONObject: json).write(to: bundle.metadataURL)
}
try writeStatus(agentInstalled: false)
await #expect(throws: MacVMError.self) {
_ = try await engine.ensureBaseBundle(for: .macOS)
}
try writeStatus(agentInstalled: true)
let resolved = try await engine.ensureBaseBundle(for: .macOS)
#expect(resolved.root == baseDir)
}
}
}
+9 -4
View File
@@ -37,7 +37,7 @@ set -euo pipefail
# The account Nucleic logs in as. Must match MacVMSettings.defaultSSHUser; also the account you
# created in Setup Assistant. Provision under this user even if invoked via sudo.
AGENT_USER="agent"
AGENT_HOME="$(/usr/bin/dscl . -read "/Users/$AGENT_USER" NFSHomeDirectory 2>/dev/null | awk '{print $2}')"
AGENT_HOME="$(/usr/bin/dscl . -read "/Users/$AGENT_USER" NFSHomeDirectory 2>/dev/null | awk '{print $2}' || true)"
AGENT_HOME="${AGENT_HOME:-/Users/$AGENT_USER}"
# Nucleic's host PUBLIC key to authorize. Prefer the script argument; else a copy dropped next to the
@@ -143,11 +143,14 @@ else
# Find the CLT label — each query bounded so a wedged softwareupdate can't hang us. Match the
# "* Label: Command Line Tools …" line specifically (NOT the "Title:" line), newest version last.
# The trailing `|| true` is load-bearing under `set -euo pipefail`: on a beta/VM guest whose SU
# catalog lists no CLT at all, `grep` exits 1 and would otherwise abort the WHOLE provision run
# right here — before the on-demand-installer fallback below ever gets a chance.
CLT_LABEL=""
for attempt in 1 2 3; do
CLT_LABEL="$(run_timeout 180 softwareupdate -l 2>/dev/null \
| grep -E 'Label:.*Command Line Tools' \
| sed -E 's/^.*Label: *//' | sort -V | tail -1)"
| sed -E 's/^.*Label: *//' | sort -V | tail -1 || true)"
[ -n "$CLT_LABEL" ] && break
echo " (softwareupdate hasn't listed the CLT package yet — attempt $attempt) …"
done
@@ -497,7 +500,7 @@ else
grant_tcc() { # $1=service $2=abs-binary-path
local req hexreq
# Extract the designated requirement; on some platform binaries this yields nothing usable.
req=$(codesign -d -r- "$2" 2>&1 | sed -n 's/^designated => //p')
req=$(codesign -d -r- "$2" 2>&1 | sed -n 's/^designated => //p' || true)
if [ -n "$req" ] && echo "$req" | csreq -r- -b /tmp/csreq.bin 2>/dev/null; then
hexreq="X'$(xxd -p /tmp/csreq.bin | tr -d '\n')'"
echo "$1$2 (with code requirement)"
@@ -609,7 +612,9 @@ PLIST
else
TCC_DB="/Library/Application Support/com.apple.TCC/TCC.db"
AGENT_HEXREQ="NULL"
AGENT_REQ=$(codesign -d -r- "$AGENT_APP_DEST" 2>&1 | sed -n 's/^designated => //p')
# `|| true`: an unsigned/odd bundle makes codesign exit nonzero, which under pipefail would
# abort the whole run; an empty AGENT_REQ already degrades gracefully to csreq NULL below.
AGENT_REQ=$(codesign -d -r- "$AGENT_APP_DEST" 2>&1 | sed -n 's/^designated => //p' || true)
if [ -n "$AGENT_REQ" ] && echo "$AGENT_REQ" | csreq -r- -b /tmp/csreq-vmagent.bin 2>/dev/null; then
AGENT_HEXREQ="X'$(xxd -p /tmp/csreq-vmagent.bin | tr -d '\n')'"
else