Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Scripts/lab/keypath-lab
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Commands:
console-login LEASE_ID
reset-guest-password LEASE_ID
secure-console-submit LEASE_ID
console-key LEASE_ID --key PARALLELS_KEY_CODE [--modifier PARALLELS_KEY_CODE]
rfb-pointer-probe LEASE_ID --x X --y Y
desktop-bootstrap LEASE_ID [--install-tools]
nameplate LEASE_ID enable|show|hide|status
Expand Down Expand Up @@ -157,6 +158,21 @@ EOF
require_lease_id "$1"
remote secure-console-submit "$1"
;;
console-key)
lease=${1:-}
[[ -n $lease ]] || usage
require_lease_id "$lease"
shift
[[ ${1:-} == "--key" && ${2:-} =~ ^[0-9]+$ ]] || usage
key_code=$2
shift 2
modifier=0
if [[ $# -gt 0 ]]; then
[[ ${1:-} == "--modifier" && ${2:-} =~ ^[0-9]+$ && $# -eq 2 ]] || usage
modifier=$2
fi
remote console-key "$lease" "$key_code" "$modifier"
;;
rfb-pointer-probe)
lease=${1:-}
[[ -n $lease ]] || usage
Expand Down
140 changes: 140 additions & 0 deletions Scripts/lab/macos-27-selector-driver
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""Capture and validate macOS 27 System Settings selector evidence."""

import argparse
import json
import os
import pathlib
import subprocess


ROOT = pathlib.Path(__file__).resolve().parent
PEEKABOO = pathlib.Path(os.environ.get("KEYPATH_SELECTOR_PEEKABOO", ROOT / "peekaboo-ui"))
RESULT = pathlib.Path(os.environ.get("KEYPATH_SELECTOR_RESULT", ROOT / "scenario-result"))


def run(command, output=None):
if output is None:
return subprocess.run(command, check=False, capture_output=True, text=True)
with output.open("w") as handle:
return subprocess.run(command, check=False, stdout=handle, stderr=subprocess.STDOUT, text=True)


def macos_version() -> tuple[str, str]:
version = run(["sw_vers", "-productVersion"])
build = run(["sw_vers", "-buildVersion"])
if version.returncode != 0 or build.returncode != 0:
raise RuntimeError("could not read macOS version")
return version.stdout.strip(), build.stdout.strip()


def contains(value, expected: str) -> bool:
needle = expected.casefold()
if isinstance(value, str):
return needle in value.casefold()
if isinstance(value, dict):
return any(contains(item, expected) for item in value.values())
if isinstance(value, list):
return any(contains(item, expected) for item in value)
return False


def record(output: pathlib.Path, scenario: str, status: str, summary: str,
classification: str, step: str, *extra: str) -> None:
subprocess.run([
str(RESULT), "record", "--output", str(output / "result.json"),
"--scenario", scenario, "--status", status, "--summary", summary,
"--classification", classification, "--step", step, *extra,
], check=True)


def required_permissions(preflight: object) -> list[str]:
if not isinstance(preflight, dict):
return ["unknown"]
data = preflight.get("data", {})
permissions = data.get("permissions", []) if isinstance(data, dict) else []
if not isinstance(permissions, list):
return ["unknown"]
return [
str(item.get("name", "unknown"))
for item in permissions
if isinstance(item, dict) and item.get("isRequired") is True and item.get("isGranted") is not True
]


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", required=True, type=pathlib.Path)
parser.add_argument("--expect", action="append", required=True,
help="required text in fresh System Settings AX evidence")
parser.add_argument("--scenario", default="macos-27-selector-probe")
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
version, build = macos_version()
if version.split(".")[0] != "27":
record(args.output, args.scenario, "blocked",
"macOS 27 selector driver requires a macOS 27 guest.",
"unsupported-os-selector", "os-admission")
return 4
run(["sw_vers"], args.output / "sw-vers.txt")
preflight_path = args.output / "peekaboo-preflight.json"
preflight_result = run([str(PEEKABOO), "preflight"], preflight_path)
if preflight_result.returncode != 0:
record(args.output, args.scenario, "failed",
"Peekaboo preflight could not verify the macOS 27 desktop session.",
"environment-precondition-failure", "peekaboo-preflight",
"--evidence", preflight_path.name)
return preflight_result.returncode
try:
preflight = json.loads(preflight_path.read_text())
except json.JSONDecodeError:
record(args.output, args.scenario, "failed",
"Peekaboo preflight evidence was not valid JSON.",
"harness-transport-failure", "peekaboo-preflight",
"--evidence", preflight_path.name)
return 1
missing_permissions = required_permissions(preflight)
if missing_permissions:
record(args.output, args.scenario, "blocked",
"Peekaboo lacks required macOS 27 desktop permissions.",
"environment-precondition-failure", "peekaboo-permissions",
"--message", "Missing permissions: " + ", ".join(missing_permissions),
"--evidence", preflight_path.name)
return 4

snapshot = args.output / "system-settings-ax.json"
capture = run([str(PEEKABOO), "snapshot", "--app", "System Settings", "--output", str(snapshot)])
if capture.returncode != 0:
record(args.output, args.scenario, "failed",
"Could not capture current System Settings selector evidence.",
"harness-transport-failure", "ax-snapshot")
return capture.returncode
try:
evidence = json.loads(snapshot.read_text())
except json.JSONDecodeError:
record(args.output, args.scenario, "failed",
"System Settings selector evidence was not valid JSON.",
"harness-transport-failure", "ax-snapshot")
return 1
missing = [selector for selector in args.expect if not contains(evidence, selector)]
if missing:
record(args.output, args.scenario, "blocked",
"macOS 27 System Settings surface did not expose the required selector evidence.",
"unsupported-os-selector", "selector-admission",
"--message", "Missing selectors: " + ", ".join(missing),
"--evidence", snapshot.name)
return 4
(args.output / "selector-contract.json").write_text(json.dumps({
"schemaVersion": 1,
"macOSMajor": 27,
"macOSVersion": version,
"macOSBuild": build,
"expected": args.expect,
"evidence": snapshot.name,
"preflightEvidence": preflight_path.name,
}, indent=2) + "\n")
return 0


if __name__ == "__main__":
raise SystemExit(main())
27 changes: 27 additions & 0 deletions Scripts/lab/remote.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,32 @@ for ch in value: print(json.dumps([{"key":codes[ch]}],separators=(",",":")))' "$
print "credential_transport\tparallels-key-events"
}

console_key() {
local lease=$1 key_code=$2 modifier_code=${3:-0} manifest macos resource parallels_cli
manifest=$(owned_manifest "$lease")
macos=$(field "$manifest" macos)
[[ "$macos" == "26" || "$macos" == "27" ]] || die "console key requires a macOS 26 or 27 Parallels lane"
[[ "$(field "$manifest" provider)" == "parallels" ]] || die "console key requires a Parallels lease"
[[ "$(field "$manifest" desktop_enabled)" == "true" ]] || die "console key requires a desktop-enabled lease"
[[ "$key_code" == <-> && "$key_code" -ge 1 && "$key_code" -le 255 ]] || die "invalid Parallels key code"
[[ "$modifier_code" == <-> && "$modifier_code" -ge 0 && "$modifier_code" -le 255 ]] || die "invalid Parallels modifier key code"
resource=$(field "$manifest" provider_resource)
[[ "$resource" =~ '^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$' ]] || die "invalid Parallels resource id"
parallels_cli=${KEYPATH_LAB_PRLCTL:-"/Applications/Parallels Desktop.app/Contents/MacOS/prlctl"}
[[ -x "$parallels_cli" ]] || die "Parallels CLI is unavailable"
if (( modifier_code > 0 )); then
"$parallels_cli" send-key-event "$resource" --key "$modifier_code" --event press >/dev/null
fi
"$parallels_cli" send-key-event "$resource" --key "$key_code" >/dev/null
if (( modifier_code > 0 )); then
"$parallels_cli" send-key-event "$resource" --key "$modifier_code" --event release >/dev/null
fi
record_command "$lease" passed console-key --key "$key_code" --modifier "$modifier_code"
print "console_key\tpassed"
print "parallels_key_code\t$key_code"
print "parallels_modifier_code\t$modifier_code"
}

reset_guest_password() {
local lease=$1 manifest resource parallels_cli secret_file key known_hosts known_hosts_option guest_ip
local fifo account_file guest_command reset_pid fifo_ready attempt stream_exit reset_exit enrollment_account
Expand Down Expand Up @@ -1632,6 +1658,7 @@ case "$action" in
console-login) [[ $# -eq 1 ]] || die "console-login requires lease"; console_login "$1" ;;
reset-guest-password) [[ $# -eq 1 ]] || die "reset-guest-password requires lease"; reset_guest_password "$1" ;;
secure-console-submit) [[ $# -eq 1 ]] || die "secure-console-submit requires lease"; secure_console_submit "$1" ;;
console-key) [[ $# -eq 3 ]] || die "console-key requires lease, Parallels key code, and modifier code"; console_key "$1" "$2" "$3" ;;
rfb-pointer-probe) [[ $# -eq 3 ]] || die "rfb-pointer-probe requires lease, x, and y"; rfb_pointer_probe "$@" ;;
nameplate) [[ $# -eq 2 ]] || die "nameplate requires lease and action"; nameplate_control "$@" ;;
destroy) [[ $# -eq 1 ]] || die "destroy requires lease"; destroy_lease "$1" ;;
Expand Down
7 changes: 7 additions & 0 deletions Scripts/lab/tests/keypath-lab-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,13 @@ expected=[[{"key":codes[ch]}] for ch in open(sys.argv[2]).read()]
assert events == expected' "$TMP/secure-console-key-events.jsonl" "$TMP/secure-input"
! grep -Fq 'fixture-password-that-must-not-leak' "$TMP/secure-console-key-events.jsonl"
grep -q 'prlctl send-key-event 11111111-1111-1111-1111-111111111111 --key 36' "$CALLS"
console_key=$(run_remote console-key cbx_desktop27 73 37)
assert_contains "$console_key" $'console_key\tpassed'
assert_contains "$console_key" $'parallels_key_code\t73'
assert_contains "$console_key" $'parallels_modifier_code\t37'
grep -q 'prlctl send-key-event 11111111-1111-1111-1111-111111111111 --key 37 --event press' "$CALLS"
grep -q 'prlctl send-key-event 11111111-1111-1111-1111-111111111111 --key 73' "$CALLS"
grep -q 'prlctl send-key-event 11111111-1111-1111-1111-111111111111 --key 37 --event release' "$CALLS"
if grep -R -F 'fixture-password-that-must-not-leak' "$ROOT/KeyPathInstallerLab" "$CALLS" "$TMP/guest-ssh-args"; then
echo "secure console submit leaked its secret into controller logs or arguments" >&2
exit 1
Expand Down
97 changes: 97 additions & 0 deletions Scripts/lab/tests/macos-27-selector-driver-tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
import json
import os
import pathlib
import subprocess
import tempfile
import unittest

TOOL = pathlib.Path(__file__).resolve().parents[1] / "macos-27-selector-driver"


class DriverTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = pathlib.Path(self.tmp.name)
self.bin = self.root / "bin"
self.bin.mkdir()
self.output = self.root / "out"
(self.bin / "sw_vers").write_text(
'#!/bin/sh\nversion=${MACOS_TEST_VERSION:-27.0}\n'
'if [ "$1" = -productVersion ]; then echo "$version"; '
'elif [ "$1" = -buildVersion ]; then echo 26A5378j; '
'else echo "ProductVersion: $version"; echo "BuildVersion: 26A5378j"; fi\n'
)
(self.bin / "sw_vers").chmod(0o755)
self.peekaboo = self.root / "peekaboo-ui"
self.peekaboo.write_text(
'#!/bin/sh\n'
'if [ "$1" = preflight ]; then\n'
' if [ -n "${PEEKABOO_TEST_PREFLIGHT:-}" ]; then printf \'%s\\n\' "$PEEKABOO_TEST_PREFLIGHT"; '
' else printf \'%s\\n\' \'{"data":{"permissions":[{"name":"Screen Recording","isRequired":true,"isGranted":true},{"name":"Accessibility","isRequired":true,"isGranted":true}]}}\'; fi\n'
' exit 0\n'
'fi\n'
'out=\nwhile [ $# -gt 0 ]; do [ "$1" = --output ] && out=$2; shift; done\n'
'printf \'{"data":{"ui_elements":[{"identifier":"Privacy_Accessibility","label":"Accessibility"}]}}\' > "$out"\n'
)
self.peekaboo.chmod(0o755)
self.result = self.root / "scenario-result"
self.result.write_text((TOOL.parent / "scenario-result").read_text())
self.result.chmod(0o755)

def tearDown(self):
self.tmp.cleanup()

def call(self, *extra, version="27.0", preflight=None):
env = os.environ | {
"PATH": str(self.bin) + ":" + os.environ["PATH"],
"MACOS_TEST_VERSION": version,
"KEYPATH_SELECTOR_PEEKABOO": str(self.peekaboo),
"KEYPATH_SELECTOR_RESULT": str(self.result),
}
if preflight is not None:
env["PEEKABOO_TEST_PREFLIGHT"] = json.dumps(preflight, separators=(",", ":"))
return subprocess.run(
[str(TOOL), "--output", str(self.output), *extra],
env=env,
text=True,
capture_output=True,
)

def test_accepts_fresh_macos_27_selector_evidence(self):
result = self.call("--expect", "Privacy_Accessibility", "--expect", "Accessibility")
self.assertEqual(result.returncode, 0, result.stderr)
contract = json.loads((self.output / "selector-contract.json").read_text())
self.assertEqual(contract["macOSMajor"], 27)
self.assertEqual(contract["macOSBuild"], "26A5378j")

def test_requires_at_least_one_selector(self):
result = self.call()
self.assertEqual(result.returncode, 2)

def test_rejects_another_macos_version_without_preflight(self):
result = self.call("--expect", "Accessibility", version="26.6")
self.assertEqual(result.returncode, 4)
self.assertEqual(json.loads((self.output / "result.json").read_text())["failure"]["classification"], "unsupported-os-selector")
self.assertFalse((self.output / "peekaboo-preflight.json").exists())

def test_missing_permission_is_environment_precondition_failure(self):
result = self.call("--expect", "Accessibility", preflight={
"data": {"permissions": [
{"name": "Screen Recording", "isRequired": True, "isGranted": False}
]}
})
self.assertEqual(result.returncode, 4)
outcome = json.loads((self.output / "result.json").read_text())
self.assertEqual(outcome["failure"]["classification"], "environment-precondition-failure")
self.assertIn("Screen Recording", outcome["failure"]["message"])

def test_missing_selector_is_explicitly_unsupported(self):
result = self.call("--expect", "Missing Control")
self.assertEqual(result.returncode, 4)
outcome = json.loads((self.output / "result.json").read_text())
self.assertEqual(outcome["failure"]["classification"], "unsupported-os-selector")


if __name__ == "__main__":
unittest.main()
10 changes: 7 additions & 3 deletions docs/testing/installer-gui-automation-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,13 @@ The remaining core capabilities are:
5. Hardened macOS 26 and macOS 27 selectors before those OS versions can claim
the same unattended UI coverage as macOS 15. The macOS 27 console session,
secret-safe Remote Management authorization, and RFB event delivery are now
proven. The macOS 26 pointer probe is implemented and fails closed when RFB
authentication is not configured; selector capture, Remote Management base
setup, and composition remain.
proven. The macOS 27 selector driver now fails closed unless fresh
accessibility evidence matches its declared selector contract. The clean
macOS 27 source remains intentionally pristine; a separate reusable Desktop
Automation Base must carry the signed tools, desktop permissions, Remote
Management approval, and an Apple-Account-free console state. The macOS 26
pointer probe is implemented and fails closed when RFB authentication is not
configured; selector capture, desktop-base setup, and composition remain.

Clean install, repair, upgrade, reboot, uninstall, reinstall, cancellation,
nightly, and pairwise entries are consumers of those foundations. They are
Expand Down
34 changes: 34 additions & 0 deletions docs/testing/remote-installer-lab.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,23 @@ inter-key delay. Parallels can drop characters when a complete credential is
sent as one unpaced event batch. The command deliberately reports credential
transport only; the RFB probe is the required independent postcondition.

For non-secret navigation of an expected disposable-guest dialog, the
controller can send one bounded Parallels key event:

```bash
Scripts/lab/keypath-lab console-key cbx_example --key 23
Scripts/lab/keypath-lab console-key cbx_example --key 36
```

Parallels uses X11-style key codes; `23` is Tab and `36` is Return. An optional
`--modifier CODE` holds that modifier only for the duration of the key event.
The command is admitted only for an owned, desktop-enabled macOS 26 or 27
Parallels lease. It records transport, not UI success. Use it only when the
expected dialog is already visible, and verify the intended state through a
fresh screenshot, accessibility snapshot, or runtime postcondition. Never use
it for credentials; use `secure-console-submit` for the focused authorization
field.

On July 16, 2026, the supported UI flow enabled Remote Management and VNC
control for `keypathqa`, and the probe moved the guest cursor from
`684.703125 141.1875` to `80 60`. That proves event posting for this disposable
Expand Down Expand Up @@ -465,6 +482,23 @@ Scripts/lab/keypath-lab scenario cbx_example macos-27-regression
Scripts/lab/keypath-lab artifacts cbx_example
```

Before an automated macOS 27 System Settings flow relies on selectors, capture
fresh accessibility evidence in the guest:

```bash
Scripts/lab/macos-27-selector-driver \
--output artifacts/macos-27-selectors \
--expect Privacy_Accessibility \
--expect Accessibility
```

The driver admits only macOS 27, requires Peekaboo's desktop permissions, and
writes the exact OS version, build, preflight, accessibility snapshot, and
expected selector contract. Missing desktop permissions are an
`environment-precondition-failure`; a missing expected selector is an
`unsupported-os-selector`. Either result blocks the scenario instead of
guessing coordinates or attributing a harness problem to KeyPath.

The command records the exact OS build, canonical CLI system snapshot,
VirtualHID extension state, KeyPath-owned launchd jobs, signatures, Gatekeeper
and stapling results, processes, TCP readiness, and relevant logs. It also emits
Expand Down
Loading