Merge nucleic/jolly-pebble-marten-kmzg into dev

This commit is contained in:
2026-07-24 00:10:40 -07:00
parent d6dc159c3d
commit a941abde7b
3 changed files with 105 additions and 22 deletions
+50 -19
View File
@@ -67,11 +67,25 @@ extension MacVMEngine {
enum UAMDMApprovalAction: Equatable {
case openEnrollmentProfile(CGRect)
case install(CGRect)
case authenticate(passwordField: CGRect)
/// `sheet` identifies WHICH authorization sheet this is, so each gets its own retry budget.
case authenticate(sheet: String, passwordField: CGRect)
case installed
case unknown
}
/// A stable slug for the authorization sheet on screen. The enroll flow raises several in
/// sequence System certificate trust, then the MDM enrollment itself and a single shared retry
/// budget spent on the first leaves the last one unanswered, which is exactly how a live run
/// stalled on "Device Management is trying to enroll you" after exhausting its attempts upstream.
static func authSheetKey(in joined: String) -> String {
func has(_ s: String) -> Bool { joined.localizedCaseInsensitiveContains(s) }
if has("Certificate Trust") || has("System Certificate") { return "certificate-trust" }
if has("remote management") || has("enroll you in") { return "mdm-enroll" }
if has("modify your system settings") { return "modify-settings" }
if has("configuration profile") || has("install a new") { return "profile-install" }
return "generic"
}
/// Levenshtein distance, bounded use for tolerating OCR substitutions in short UI labels.
static func editDistance(_ a: String, _ b: String) -> Int {
let s = Array(a), t = Array(b)
@@ -148,7 +162,8 @@ extension MacVMEngine {
if contains("Enter your password") || contains("enter password"),
let password = Self.passwordFieldBox(in: normalized)
{
return .authenticate(passwordField: password)
return .authenticate(
sheet: Self.authSheetKey(in: joined), passwordField: password)
}
// The profile-review sheet ("Are you sure you want to install this device profile?") confirms
@@ -874,8 +889,8 @@ extension MacVMEngine {
var lastLines: [(text: String, box: CGRect)] = []
var rowClicks = 0, installClicks = 0
var lastRowClickAt: Date?, lastInstallClickAt: Date?
var authAttempts = 0
var lastAuthAt: Date?
var authAttempts: [String: Int] = [:]
var lastAuthAt: [String: Date] = [:]
var paneIndex = 0
var nextPaneOpen = Date() // surface the pane on the first idle capture
var reachedEnrollmentUI = false
@@ -925,25 +940,36 @@ extension MacVMEngine {
name: target.name,
.click(x: point.x, y: point.y, button: .left, count: 1))
}
case .authenticate(let passwordField):
// Answer if attempts remain AND we did not just try (a correct answer dismisses the
// sheet within a second, so a still-present sheet means the last attempt failed a
// dropped keystroke, say and only then is a re-type warranted).
let sinceLast = lastAuthAt.map { Date().timeIntervalSince($0) }
case .authenticate(let sheet, let passwordField):
// Budget is PER SHEET: the flow raises several authorization sheets in sequence, and a
// shared counter spent retrying the first leaves the last unanswered.
let attempts = authAttempts[sheet, default: 0]
let sinceLast = lastAuthAt[sheet].map { Date().timeIntervalSince($0) }
?? .greatestFiniteMagnitude
if authAttempts < Self.maxAuthAttempts, sinceLast >= Self.reAuthDelaySeconds {
if attempts < Self.maxAuthAttempts, sinceLast >= Self.reAuthDelaySeconds {
acted = true
authAttempts += 1
lastAuthAt = Date()
// Click the CENTER of the fuzzily-located Password field (the placeholder sits
// inside it), let focus settle, then cmd+a (absorbs a dropped first keystroke and
// clears any residue) type Return. Return activates the sheet's default button,
// so no button label needs to be OCR-matched (the labels mis-OCR frequently).
authAttempts[sheet] = attempts + 1
lastAuthAt[sheet] = Date()
let field = Self.clickPoint(forNormalizedBox: passwordField)
recorder?.note("answer password sheet: click field at (\(field.x), \(field.y))")
recorder?.note(
"answer \"\(sheet)\" sheet (attempt \(attempts + 1)): "
+ "focus+click field at (\(field.x), \(field.y))")
// TWO clicks, deliberately. A SecurityAgent sheet often appears without becoming
// key, and the first click on a non-key window is consumed activating it so a
// single click leaves the caret unplaced and the keystrokes go nowhere (observed
// live: the sheet sat untouched until a human clicked it, after which the very same
// automation completed it). The first click foregrounds; the second places the
// caret in the field. Both target the field, so neither can hit another control.
await surface.send(
name: target.name,
.click(x: field.x, y: field.y, button: .left, count: 1))
try? await Task.sleep(nanoseconds: 600_000_000)
await surface.send(
name: target.name,
.click(x: field.x, y: field.y, button: .left, count: 1))
// Let focus settle, then cmd+a (absorbs a dropped first keystroke and clears any
// residue) type Return. Return activates the sheet's default button, so no
// button label needs OCR-matching (those labels mis-OCR frequently).
try? await Task.sleep(nanoseconds: 400_000_000)
await surface.send(name: target.name, .key(chord: "cmd+a"))
await surface.send(name: target.name, .text(password))
@@ -988,7 +1014,12 @@ extension MacVMEngine {
? "every approval capture recognized no text — the guest framebuffer appears blank to the "
+ "host surface rather than showing an unrecognized screen"
: "expected Device Management approval state was not reached "
+ "(password sheets answered: \(authAttempts))"
+ "(password sheets answered: "
+ (authAttempts.isEmpty
? "none"
: authAttempts.sorted { $0.key < $1.key }
.map { "\($0.key)×\($0.value)" }.joined(separator: ", "))
+ ")"
let diagnostic = await recordMDMApprovalDiagnostic(
bundle: bundle, target: target, jpeg: lastJPEG, lines: lastLines,
reason: reason, recorder: recorder)
@@ -1003,7 +1034,7 @@ extension MacVMEngine {
switch action {
case .openEnrollmentProfile: return "open-enrollment-profile"
case .install: return "install"
case .authenticate: return "authenticate"
case .authenticate(let sheet, _): return "authenticate-\(sheet)"
case .installed: return "installed"
case .unknown: return "unknown"
}
+30 -3
View File
@@ -1605,7 +1605,8 @@ import Testing
(text: "Password", box: field),
(text: "Modify Settings", box: CGRect(x: 0.7, y: 0.25, width: 0.15, height: 0.04)),
]
#expect(MacVMEngine.uamdmApprovalAction(in: install) == .authenticate(passwordField: field))
#expect(MacVMEngine.uamdmApprovalAction(in: install)
== .authenticate(sheet: "modify-settings", passwordField: field))
// No "Enter your password" phrase not an actionable sheet.
#expect(MacVMEngine.uamdmApprovalAction(in: Array(install.dropFirst(1)).filter {
!$0.text.contains("Enter your password")
@@ -1627,7 +1628,33 @@ import Testing
(text: "Peseword", box: field),
(text: "Updale Settings", box: CGRect(x: 0.6, y: 0.3, width: 0.15, height: 0.04)),
]
#expect(MacVMEngine.uamdmApprovalAction(in: mangled) == .authenticate(passwordField: field))
#expect(MacVMEngine.uamdmApprovalAction(in: mangled)
== .authenticate(sheet: "certificate-trust", passwordField: field))
}
@Test func uamdmDistinguishesEachAuthorizationSheetForItsOwnRetryBudget() {
// The flow raises several sheets in sequence; each needs its own budget, or retries spent on
// the first leave the last unanswered how a live run stalled on the final MDM enrollment
// sheet after exhausting a single shared counter upstream.
let field = CGRect(x: 0.44, y: 0.39, width: 0.03, height: 0.02)
func sheet(_ intent: String) -> String? {
let action = MacVMEngine.uamdmApprovalAction(in: [
(text: intent, box: .zero),
(text: "Enter your password to continue", box: .zero),
(text: "Password", box: field),
])
if case .authenticate(let s, _) = action { return s }
return nil
}
#expect(sheet("You are making changes to the System Certificate Trust Settings.")
== "certificate-trust")
// The exact wording of the final enrollment sheet observed on macOS 27.
#expect(sheet("Device Management is trying to enroll you in a remote management (MDM) service.")
== "mdm-enroll")
#expect(sheet("System Settings is trying to modify your system settings.") == "modify-settings")
// Distinct sheets must not collide onto one budget key.
#expect(sheet("You are making changes to the System Certificate Trust Settings.")
!= sheet("Device Management is trying to enroll you in a remote management (MDM) service."))
}
@Test func passwordFieldFuzzyMatchAndEditDistanceBounds() {
@@ -1750,7 +1777,7 @@ import Testing
let labels = [
MacVMEngine.stateLabel(.openEnrollmentProfile(.zero)),
MacVMEngine.stateLabel(.install(.zero)),
MacVMEngine.stateLabel(.authenticate(passwordField: .zero)),
MacVMEngine.stateLabel(.authenticate(sheet: "mdm-enroll", passwordField: .zero)),
MacVMEngine.stateLabel(.installed),
MacVMEngine.stateLabel(.unknown),
]
+25
View File
@@ -648,6 +648,31 @@ granted extra clicks on incidental text noise while starving a genuinely stuck s
Build clean; MacVM (142) and NucleicMDM (39) green.
### Tenth live run — sheet focus, and a shared retry budget (2026-07-24)
The certificate failure is **gone**: the serial fix plus profile signing carried the flow through the
review sheet and the Enroll confirmation. It then stalled at the last step — "Device Management is
trying to enroll you in a remote management (MDM) service" — with the password never submitted. The
operator's step-by-step named both causes precisely.
**A sheet that isn't key swallows the first click.** The trust sheet "is not consistently foregrounded…
must be manually clicked on to foreground, at which point it is automatically completed" — i.e. the
typing was always correct, but the sheet was not key, and the first click on a non-key window is
consumed activating it, so the caret never landed and the keystrokes went nowhere. The answer sequence
now sends **two** clicks at the password field, 600 ms apart: the first foregrounds the sheet, the
second places the caret. Both target the same field, so neither can stray onto another control.
**The retry budget was global, not per sheet.** `maxAuthAttempts` was one counter for the whole loop, so
while the unfocused trust sheet was retried every 6 s it consumed all five attempts — leaving nothing
for the final enrollment sheet, which is exactly where the run stalled. Attempts are now tracked per
sheet, keyed by a slug derived from the sheet's own wording (`certificate-trust`, `mdm-enroll`,
`modify-settings`, `profile-install`), so each gets its own budget and the failure summary reports them
individually (`certificate-trust×5, mdm-enroll×0` would have made this obvious at a glance).
Verified: build clean; MacVM (143) and NucleicMDM (39) green, including a test that pins the exact
macOS 27 wording of each sheet to a distinct budget key and asserts two different sheets cannot collide
onto one.
## Workstream 1 — enrollment-pass blockers
- [x] 1.1 Run the enroll script as the console user with per-step `sudo` (hosts pin, CA trust), so