From 534f66ff2e3f03175e28916e37887c62f2d4a61f Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:32:37 -0700 Subject: [PATCH 01/16] Harden source runtime recovery --- README.md | 12 ++--- pyproject.toml | 2 +- script/build_and_run.sh | 97 ++++++++++++++++++++++++++++++++-- tests/test_project_metadata.py | 14 +++++ 4 files changed, 113 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8002948..ec355b7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![Latest release](https://img.shields.io/github/v/release/hideouts-io/iOS-Developer-Toolkit?display_name=tag)](https://github.com/hideouts-io/iOS-Developer-Toolkit/releases/latest) ![Platform](https://img.shields.io/badge/platform-macOS-000000?logo=apple&logoColor=white) ![Devices](https://img.shields.io/badge/device-iPhone%20%7C%20iPad-0969da) -![Python](https://img.shields.io/badge/Python-3.10%2B-3776ab?logo=python&logoColor=white) +![Python](https://img.shields.io/badge/Python-3.10%E2%80%933.13-3776ab?logo=python&logoColor=white) ![GUI](https://img.shields.io/badge/GUI-PySide6-41cd52) ![pymobiledevice3](https://img.shields.io/badge/pymobiledevice3-11.15.1-8250df) [![License](https://img.shields.io/badge/license-MIT-2da44e)](LICENSE) @@ -200,7 +200,7 @@ These layers are related but not interchangeable: ## Requirements - macOS 13 or later; -- Python 3.10 or later for this project; +- Python 3.10 through 3.13 for this project; - an unlocked iPhone or iPad you are authorized to test or examine; - a data-capable USB cable; - enough protected disk space for logs, PCAPs, backups, crash reports, and case output; @@ -211,7 +211,7 @@ Current pinned runtime: | Component | Version or path | |---|---| -| Python | `>=3.10` | +| Python | `>=3.10,<3.14` | | PySide6 Essentials | `6.9.3` | | pymobiledevice3 | `11.15.1` | | Local Xcode candidate | `/Library/Developer/CoreDevice/CandidateDDIs/iOS_DDI.dmg` | @@ -241,7 +241,7 @@ cd iOS-Developer-Toolkit The launcher: -1. creates `venv/` when needed; +1. creates `venv/` when needed, or safely rebuilds it with a compatible Python if the interpreter or pinned runtime has drifted; 2. installs the pinned project dependencies into that environment; 3. stages `dist/iOS Developer Toolkit.app`; 4. opens the staged app. @@ -338,7 +338,7 @@ python3 --version xcode-select -p ``` -If `python3` is missing or older than 3.10, install a supported Python locally before launching. Dependencies belong in the project-created `venv/`; do not install this project's pinned packages globally. +If Python 3.10 through 3.13 is unavailable, install a supported Python locally before launching. Dependencies belong in the project-created `venv/`; do not install this project's pinned packages globally. If macOS warns about downloaded content, follow [Open the ad-hoc-signed app safely](#open-the-ad-hoc-signed-app-safely). Source installations and local development wrappers are also not a substitute for Developer ID signing and notarization. @@ -1041,7 +1041,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 94 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 90-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 95 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 90-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/pyproject.toml b/pyproject.toml index a3b5cc4..1b2c01a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "ios-developer-toolkit" version = "0.3.4" description = "Safety-focused pymobiledevice3 GUI, Developer Disk Image mounter, and iOS evidence workbench" -requires-python = ">=3.10" +requires-python = ">=3.10,<3.14" license = "MIT" dependencies = [ "PySide6-Essentials==6.9.3", diff --git a/script/build_and_run.sh b/script/build_and_run.sh index 6afe042..03c7d1e 100755 --- a/script/build_and_run.sh +++ b/script/build_and_run.sh @@ -21,12 +21,99 @@ stop_existing() { done < <(pgrep -f "$PROCESS_PATTERN" || true) } -build_app() { - if [[ ! -x "$VENV_DIR/bin/python" ]]; then - python3 -m venv "$VENV_DIR" +project_runtime_digest() { + shasum -a 256 "$PROJECT_DIR/pyproject.toml" | awk '{print $1}' +} + +python_is_compatible() { + local python_command="$1" + "$python_command" -c 'import sys; raise SystemExit(not ((3, 10) <= sys.version_info[:2] < (3, 14)))' +} + +select_compatible_python() { + local candidate + local candidate_path + for candidate in python3.13 python3.12 python3.11 python3.10 python3; do + if ! candidate_path="$(command -v "$candidate")"; then + continue + fi + if python_is_compatible "$candidate_path"; then + printf '%s\n' "$candidate_path" + return 0 + fi + done + echo "Python 3.10 through 3.13 is required; no compatible interpreter was found" >&2 + return 1 +} + +runtime_matches_project() { + local environment_directory="$1" + local runtime_stamp="$environment_directory/.ios-developer-toolkit-runtime" + if [[ ! -f "$runtime_stamp" ]]; then + return 1 + fi + local recorded_digest + read -r recorded_digest < "$runtime_stamp" + if [[ "$recorded_digest" != "$(project_runtime_digest)" ]]; then + return 1 fi - if ! "$VENV_DIR/bin/python" -c 'import PySide6; import pymobiledevice3' >/dev/null 2>&1; then - "$VENV_DIR/bin/python" -m pip install --disable-pip-version-check --quiet "$PROJECT_DIR" + "$environment_directory/bin/python" -c 'import PySide6; import pymobiledevice3' + "$environment_directory/bin/python" -m pip check >/dev/null + "$environment_directory/bin/pymobiledevice3" version >/dev/null +} + +install_project_runtime() { + local environment_directory="$1" + local runtime_stamp="$environment_directory/.ios-developer-toolkit-runtime" + for legacy_distribution in PySide6 PySide6-Addons; do + if "$environment_directory/bin/python" -m pip show "$legacy_distribution" >/dev/null 2>&1; then + "$environment_directory/bin/python" -m pip uninstall --yes "$legacy_distribution" || return 1 + fi + done + "$environment_directory/bin/python" -m pip install \ + --disable-pip-version-check \ + --force-reinstall \ + --upgrade \ + "$PROJECT_DIR" || return 1 + "$environment_directory/bin/python" -m pip check || return 1 + "$environment_directory/bin/python" -c 'import PySide6; import pymobiledevice3' || return 1 + project_runtime_digest > "$runtime_stamp" +} + +replace_incompatible_environment() { + local bootstrap_python="$1" + local previous_environment="" + if [[ -d "$VENV_DIR" ]]; then + previous_environment="$(mktemp -d "$PROJECT_DIR/.toolkit-venv-previous.XXXXXX")" + rmdir "$previous_environment" + mv "$VENV_DIR" "$previous_environment" + fi + + if ! "$bootstrap_python" -m venv "$VENV_DIR"; then + if [[ -n "$previous_environment" ]]; then + mv "$previous_environment" "$VENV_DIR" + fi + return 1 + fi + if ! install_project_runtime "$VENV_DIR"; then + /usr/bin/find "$VENV_DIR" -depth -delete + if [[ -n "$previous_environment" ]]; then + mv "$previous_environment" "$VENV_DIR" + fi + return 1 + fi + if [[ -n "$previous_environment" ]]; then + /usr/bin/find "$previous_environment" -depth -delete + fi +} + +build_app() { + local bootstrap_python + bootstrap_python="$(select_compatible_python)" + if [[ ! -x "$VENV_DIR/bin/python" ]] || ! python_is_compatible "$VENV_DIR/bin/python"; then + replace_incompatible_environment "$bootstrap_python" + elif ! runtime_matches_project "$VENV_DIR"; then + install_project_runtime "$VENV_DIR" fi mkdir -p "$APP_MACOS" "$APP_RESOURCES" cp "$PROJECT_DIR/macos/Info.plist" "$APP_CONTENTS/Info.plist" diff --git a/tests/test_project_metadata.py b/tests/test_project_metadata.py index a447792..c5c76f0 100644 --- a/tests/test_project_metadata.py +++ b/tests/test_project_metadata.py @@ -5,6 +5,9 @@ import unittest from pathlib import Path +from packaging.specifiers import SpecifierSet +from packaging.version import Version + from ios_developer_toolkit import APP_VERSION @@ -12,6 +15,17 @@ class ProjectMetadataTests(unittest.TestCase): + def test_python_range_matches_the_pinned_qt_runtime(self) -> None: + pyproject = tomllib.loads((REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + project = pyproject["project"] + self.assertIsInstance(project, dict) + supported_python = SpecifierSet(str(project["requires-python"])) + + self.assertNotIn(Version("3.9"), supported_python) + self.assertIn(Version("3.10"), supported_python) + self.assertIn(Version("3.13"), supported_python) + self.assertNotIn(Version("3.14"), supported_python) + def test_source_and_packaging_metadata_match_application_version(self) -> None: pyproject = tomllib.loads((REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8")) project = pyproject["project"] From 1df15b7b94b9545cae6678a1d7d63a0b4b1fbd97 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:40:19 -0700 Subject: [PATCH 02/16] Centralize DDI action process handling --- README.md | 2 + docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- ios_developer_toolkit/app.py | 82 ++++++++++++++++------------- ios_developer_toolkit/entrypoint.py | 13 +++++ 4 files changed, 61 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index ec355b7..58323fe 100644 --- a/README.md +++ b/README.md @@ -439,6 +439,8 @@ The toolkit attaches this outer host image read-only, validates its `Restore` pa Both modern paths normally require Apple TSS access. A cached DDI payload does not guarantee that personalization can complete offline. +Developer Mode queries and DDI mount, list, unmount, install, and uninstall actions use the shared bounded operation controller. It drains both output channels at completion, reports launch failures and crashes distinctly, prevents periodic device refreshes from re-enabling conflicting controls, and stops an action that exceeds the 15-minute safety limit. + ### Device Capability Matrix ![Device Capability Matrix workspace](docs/screenshots/capability-matrix.png) diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 7b267ef..78b4603 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -113,7 +113,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P1 — turn diagnostics into a coherent workbench -* Continue migrating finite subprocess workflows to the reusable operation controller and typed `OperationResult`. Device discovery, Man Pages, and sequential command drift now share final-drain, timeout, cancellation, launch-failure, clean-relaunch, and structured completion semantics; DDI, backup, apps, and capture remain incremental migrations. +* Continue migrating finite subprocess workflows to the reusable operation controller and typed `OperationResult`. Device discovery, Man Pages, sequential command drift, and DDI/developer-image actions now share final-drain, timeout, cancellation, launch-failure, clean-relaunch, and structured completion semantics; backup, apps, and capture remain incremental migrations. * Make a contextual readiness pane for the selected action, with one-click scoped rechecks and copyable remediation. * Maintain the opt-in physical-device compatibility protocol and its explicit USB, usbmux, CoreDevice, developer-service, privacy, and state-changing test boundaries. A pre-release dual-architecture frozen-artifact smoke workflow is now present. The release builder rejects any bundled Mach-O whose minimum macOS version is newer than the advertised 13.0 floor or lacks the native release architecture. * Generate concise changelog/release notes from tested behavior. Source, bundle, citation, packaging, and third-party-source metadata drift is now covered by automated tests. @@ -169,6 +169,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-21 | Corrected the macOS compatibility gate and bounded native-build timing. | The first clean dual-architecture run proved arm64 produced a macOS 11-compatible executable, which is compatible with the advertised macOS 13 floor; Intel exceeded the original 45-minute job limit. | Re-run both native builders with reusable Nuitka caches and a 90-minute cap before merging. | | 2026-09-21 | Expanded compatibility validation from the launcher to every bundled Mach-O and pinned a genuinely compatible Qt line. | PySide6 6.11.2 wheel filenames advertise macOS 13, but direct `otool` inspection found Shiboken load commands requiring macOS 15; PySide6 6.9.3 Shiboken binaries declare macOS 12. | The dual-native CI build must pass the full-bundle architecture and deployment-floor scan before release. | | 2026-09-21 | Migrated sequential command-drift probes to the shared finite-operation controller. | A clean Python 3.13 environment passed the 94-test suite and the GUI smoke now runs the entire 49-route drift check through the real asynchronous UI path. | DDI, backup, app, and capture operations remain incremental controller migrations. | +| 2026-09-22 | Migrated DDI and Developer Mode actions to the shared finite-operation controller. | The 95-test suite and 90-action GUI smoke passed; the smoke test now executes a real bounded host-only command through the migrated path. | Backup, app, and capture operations remain incremental controller migrations. | ## Research sources diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index 7395af1..b86d953 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -210,6 +210,7 @@ COMMAND_DRIFT_HELP_TIMEOUT_MS = 5_000 RECONNECT_TIMEOUT_MS = 30_000 DEVICE_SCAN_TIMEOUT_MS = 10_000 +DDI_ACTION_TIMEOUT_MS = 15 * 60_000 PROCESS_TERMINATE_GRACE_MS = 1_500 @@ -505,9 +506,11 @@ def __init__(self) -> None: self._reconnect_timeout_timer = QTimer(self) self._reconnect_timeout_timer.setSingleShot(True) self._reconnect_timeout_timer.timeout.connect(self._reconnect_timed_out) - self._action_process: QProcess | None = None + self._action_controller = FiniteProcessController(self) + self._action_controller.stdout_received.connect(self._append_action_output) + self._action_controller.stderr_received.connect(self._append_action_output) + self._action_controller.completed.connect(self._action_completed) self._action_context = "" - self._action_buffer = bytearray() self._capability_process: QProcess | None = None self._capability_stdout_buffer = bytearray() self._capability_stderr = bytearray() @@ -2529,8 +2532,9 @@ def _update_device_fields(self, device: IOSDevice | None) -> None: else "Connect a trusted device, then refresh the inventory." ) enabled = device is not None and not self._demo_mode - self.mount_button.setEnabled(enabled) - self.remove_button.setEnabled(enabled) + action_available = enabled and not self._action_controller.is_running() + self.mount_button.setEnabled(action_available) + self.remove_button.setEnabled(action_available) self.start_collection_button.setEnabled(enabled and self._collection_process is None) self.create_case_button.setEnabled(enabled and self._collection_process is None and self._active_case_path is None) self.case_readiness_button.setEnabled(enabled and self._capability_process is None) @@ -3071,55 +3075,56 @@ def _start_action( environment: Mapping[str, str], context: str, ) -> None: - if self._action_process is not None and self._action_process.state() != QProcess.ProcessState.NotRunning: + if self._action_controller.is_running(): QMessageBox.warning(self, "Action Running", "Wait for the current DDI action to finish.") return self.action_output.appendPlainText(f"$ {command_text(program, arguments)}") - self._action_buffer.clear() - process = QProcess(self) - process.setProgram(str(program.program)) - process.setArguments(list(command_arguments(program, arguments))) - process.setProcessEnvironment(qprocess_environment(environment)) - process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) - process.readyReadStandardOutput.connect(self._read_action_output) - process.finished.connect(self._action_finished) - process.errorOccurred.connect(self._action_error) - self._action_process = process self._action_context = context self.mount_button.setEnabled(False) self.remove_button.setEnabled(False) - process.start() + self._action_controller.start( + finite_process_request( + program, + arguments, + environment, + DDI_ACTION_TIMEOUT_MS, + PROCESS_TERMINATE_GRACE_MS, + ) + ) - def _read_action_output(self) -> None: - if self._action_process is not None: - text = bytes(self._action_process.readAllStandardOutput()).decode("utf-8", errors="replace") - self._action_buffer.extend(text.encode("utf-8")) - self.action_output.moveCursor(QTextCursor.MoveOperation.End) - self.action_output.insertPlainText(text) + def _append_action_output(self, output: bytes) -> None: + self.action_output.moveCursor(QTextCursor.MoveOperation.End) + self.action_output.insertPlainText(output.decode("utf-8", errors="replace")) - def _action_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: - del exit_status + def _action_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") context = self._action_context - self.action_output.appendPlainText(f"\n[finished: exit {exit_code}]\n") - semantic_failure = output_indicates_failure(bytes(self._action_buffer)) + combined_output = result_object.stdout + result_object.stderr + semantic_failure = output_indicates_failure(combined_output) + succeeded = result_object.outcome == "succeeded" and not semantic_failure + exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) + self.action_output.appendPlainText( + f"\n[finished: {result_object.outcome}; exit {exit_label}]\n" + ) + if result_object.error_message: + self.action_output.appendPlainText(f"Process error: {result_object.error_message}") + if result_object.outcome == "timed-out": + self.action_output.appendPlainText( + "The DDI action exceeded the 15-minute safety limit and was stopped." + ) if context == "developer-mode-status": - recent_text = self.action_output.toPlainText().lower() - if exit_code == 0 and not semantic_failure and "true" in recent_text.split("$ ")[-1]: + if succeeded and b"true" in combined_output.lower(): self.developer_mode_status.setText("Developer Mode is enabled") - elif exit_code == 0 and not semantic_failure: + elif succeeded: self.developer_mode_status.setText("Developer Mode appears disabled — follow the on-device steps") else: self.developer_mode_status.setText("Could not query Developer Mode; see command output") - elif exit_code == 0 and not semantic_failure and context.startswith("mount"): + elif succeeded and context.startswith("mount"): self.developer_mode_status.setText("Developer image operation completed successfully") - self._action_process = None + self._action_context = "" self._update_device_fields(self.selected_device()) - def _action_error(self, process_error: QProcess.ProcessError) -> None: - del process_error - if self._action_process is not None: - self.action_output.appendPlainText(f"\nProcess error: {self._action_process.errorString()}") - def _apply_location_coordinates(self, coordinates: Coordinates) -> None: self.location_latitude_field.setText(format(coordinates.latitude, ".12g")) self.location_longitude_field.setText(format(coordinates.longitude, ".12g")) @@ -5609,10 +5614,10 @@ def closeEvent(self, event: QCloseEvent) -> None: if window.isVisible(): event.ignore() return + action_running = self._action_controller.is_running() critical_processes = tuple( process for process in ( - self._action_process, self._collection_process, self._sideload_process, self._apps_process, @@ -5622,7 +5627,7 @@ def closeEvent(self, event: QCloseEvent) -> None: ) if process is not None and process.state() != QProcess.ProcessState.NotRunning ) - if critical_processes: + if action_running or critical_processes: should_close = self._confirm( "Stop Active Operations?", "A DDI, evidence, app, backup, Location Lab, or Command Center operation is still running. " @@ -5635,6 +5640,7 @@ def closeEvent(self, event: QCloseEvent) -> None: self._reconnect_timeout_timer.stop() self._manpage_controller.shutdown(3000, 1000) self._command_drift_controller.shutdown(3000, 1000) + self._action_controller.shutdown(10000, 3000) capability_process = self._capability_process if capability_process is not None and capability_process.state() != QProcess.ProcessState.NotRunning: self._terminate_capability_children(capability_process) diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 7320e45..38a954c 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -10,6 +10,7 @@ INTERNAL_SMOKE_TEST_FLAG, INTERNAL_WORKER_FLAG, ToolkitWorker, + pymobiledevice3_command, ) @@ -200,6 +201,18 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError("Demo mode must disable live-device log collection") demo_mode_button.click() application.processEvents() + window._start_action(pymobiledevice3_command(), ("version",), {}, "smoke") + action_deadline = time.monotonic() + 20 + while window._action_controller.is_running() and time.monotonic() < action_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._action_controller.is_running(): + window._action_controller.cancel() + raise RuntimeError("GUI DDI action controller did not complete within its bounded smoke-test window") + action_output = window.action_output.toPlainText() + if "[finished: succeeded; exit 0]" not in action_output: + raise RuntimeError(f"GUI DDI action controller failed its host-only smoke command: {action_output}") window.close() application.processEvents() print(f"GUI smoke test passed with {len(buttons)} action buttons", flush=True) From f24ff63ab89e55e01596f8cc976c943795cd6f1a Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:44:41 -0700 Subject: [PATCH 03/16] Centralize installed app operations --- README.md | 2 + docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- ios_developer_toolkit/app.py | 87 +++++++++++++---------------- ios_developer_toolkit/entrypoint.py | 32 +++++++++++ 4 files changed, 75 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 58323fe..541240d 100644 --- a/README.md +++ b/README.md @@ -552,6 +552,8 @@ Long-running commands remain attached to a visible Stop control. Stopping a proc Refresh loads the app inventory for the selected trusted device. The table can search and sort by app name, bundle ID, version, build, type, and optional calculated size. It can copy a selected bundle ID and uninstall a selected app only after explicit confirmation. +Inventory and uninstall operations use the shared bounded controller, including terminal output draining, explicit launch/crash/timeout/cancellation results, and a 10-minute safety limit. The visible Stop control requests controller cancellation and retains the terminal result in the workspace output. + The inventory is held in memory unless it is included in an evidence collection. App names and bundle IDs can reveal sensitive usage or organizational information; do not publish them without review. An empty inventory is not proof that no apps exist. It may instead indicate device lock state, pairing, service availability, filters, command failure, or incomplete visibility. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 78b4603..47dc9a4 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -113,7 +113,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P1 — turn diagnostics into a coherent workbench -* Continue migrating finite subprocess workflows to the reusable operation controller and typed `OperationResult`. Device discovery, Man Pages, sequential command drift, and DDI/developer-image actions now share final-drain, timeout, cancellation, launch-failure, clean-relaunch, and structured completion semantics; backup, apps, and capture remain incremental migrations. +* Continue migrating finite subprocess workflows to the reusable operation controller and typed `OperationResult`. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, and Installed Apps inventory/uninstall now share final-drain, timeout, cancellation, launch-failure, clean-relaunch, and structured completion semantics; backup, IPA inspection/install, and capture remain incremental migrations. * Make a contextual readiness pane for the selected action, with one-click scoped rechecks and copyable remediation. * Maintain the opt-in physical-device compatibility protocol and its explicit USB, usbmux, CoreDevice, developer-service, privacy, and state-changing test boundaries. A pre-release dual-architecture frozen-artifact smoke workflow is now present. The release builder rejects any bundled Mach-O whose minimum macOS version is newer than the advertised 13.0 floor or lacks the native release architecture. * Generate concise changelog/release notes from tested behavior. Source, bundle, citation, packaging, and third-party-source metadata drift is now covered by automated tests. @@ -170,6 +170,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-21 | Expanded compatibility validation from the launcher to every bundled Mach-O and pinned a genuinely compatible Qt line. | PySide6 6.11.2 wheel filenames advertise macOS 13, but direct `otool` inspection found Shiboken load commands requiring macOS 15; PySide6 6.9.3 Shiboken binaries declare macOS 12. | The dual-native CI build must pass the full-bundle architecture and deployment-floor scan before release. | | 2026-09-21 | Migrated sequential command-drift probes to the shared finite-operation controller. | A clean Python 3.13 environment passed the 94-test suite and the GUI smoke now runs the entire 49-route drift check through the real asynchronous UI path. | DDI, backup, app, and capture operations remain incremental controller migrations. | | 2026-09-22 | Migrated DDI and Developer Mode actions to the shared finite-operation controller. | The 95-test suite and 90-action GUI smoke passed; the smoke test now executes a real bounded host-only command through the migrated path. | Backup, app, and capture operations remain incremental controller migrations. | +| 2026-09-22 | Migrated Installed Apps inventory and uninstall operations to the shared finite-operation controller. | The 95-test suite and 90-action GUI smoke passed; the smoke test now renders a synthetic inventory through the migrated asynchronous result path. | Backup, IPA inspection/install, and capture operations remain incremental controller migrations. | ## Research sources diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index b86d953..6abe293 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -211,6 +211,7 @@ RECONNECT_TIMEOUT_MS = 30_000 DEVICE_SCAN_TIMEOUT_MS = 10_000 DDI_ACTION_TIMEOUT_MS = 15 * 60_000 +APPS_ACTION_TIMEOUT_MS = 10 * 60_000 PROCESS_TERMINATE_GRACE_MS = 1_500 @@ -536,10 +537,10 @@ def __init__(self) -> None: self._sideload_process: QProcess | None = None self._sideload_context = "" self._sideload_buffer = bytearray() - self._apps_process: QProcess | None = None + self._apps_controller = FiniteProcessController(self) + self._apps_controller.stderr_received.connect(self._append_apps_stderr) + self._apps_controller.completed.connect(self._apps_completed) self._apps_context = "" - self._apps_stdout = bytearray() - self._apps_stderr = bytearray() self._installed_apps: tuple[InstalledApp, ...] = () self._backup_process: QProcess | None = None self._backup_action = "" @@ -4114,7 +4115,7 @@ def _sideload_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) - self._sideload_context = "" self.sideload_activity_progress.setVisible(False) self._update_sideload_controls() - if succeeded and context == "install" and self._apps_process is None: + if succeeded and context == "install" and not self._apps_controller.is_running(): QTimer.singleShot(0, self.refresh_app_inventory) def _sideload_error(self, process_error: QProcess.ProcessError) -> None: @@ -4142,49 +4143,36 @@ def _start_apps_action(self, arguments: tuple[str, ...], context: str) -> None: if device is None: self._show_no_device() return - if self._apps_process is not None: + if self._apps_controller.is_running(): QMessageBox.warning(self, "App Operation Running", "Stop or wait for the active app operation first.") return self._apps_context = context - self._apps_stdout.clear() - self._apps_stderr.clear() self.apps_output.appendPlainText(f"\n$ pymobiledevice3 {shlex.join(arguments)}\n") - process = QProcess(self) - process.setProgram(str(self._pmd3.program)) - process.setArguments(list(command_arguments(self._pmd3, arguments))) - process.setWorkingDirectory(str(Path.home())) - process.setProcessEnvironment(qprocess_environment(device_environment(device.identifier))) - process.readyReadStandardOutput.connect(self._read_apps_stdout) - process.readyReadStandardError.connect(self._read_apps_stderr) - process.finished.connect(self._apps_finished) - process.errorOccurred.connect(self._apps_error) - self._apps_process = process self.apps_status.setText(f"Running {context} operation on {device.display_name()}…") + self._apps_controller.start( + finite_process_request( + self._pmd3, + arguments, + device_environment(device.identifier), + APPS_ACTION_TIMEOUT_MS, + PROCESS_TERMINATE_GRACE_MS, + ) + ) self._update_apps_controls() - process.start() - def _read_apps_stdout(self) -> None: - if self._apps_process is not None: - self._apps_stdout.extend(bytes(self._apps_process.readAllStandardOutput())) - - def _read_apps_stderr(self) -> None: - if self._apps_process is None: - return - output = bytes(self._apps_process.readAllStandardError()) - self._apps_stderr.extend(output) + def _append_apps_stderr(self, output: bytes) -> None: self.apps_output.moveCursor(QTextCursor.MoveOperation.End) self.apps_output.insertPlainText(output.decode("utf-8", errors="replace")) - def _apps_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: - del exit_status - self._read_apps_stdout() - self._read_apps_stderr() + def _apps_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") context = self._apps_context - combined_output = bytes(self._apps_stdout + self._apps_stderr) - succeeded = exit_code == 0 and not output_indicates_failure(combined_output) + combined_output = result_object.stdout + result_object.stderr + succeeded = result_object.outcome == "succeeded" and not output_indicates_failure(combined_output) if succeeded and context == "inventory": try: - apps = parse_installed_apps_json(self._apps_stdout.decode("utf-8")) + apps = parse_installed_apps_json(result_object.stdout.decode("utf-8")) except (InstalledAppsDataError, json.JSONDecodeError, UnicodeDecodeError) as error: succeeded = False self.apps_output.appendPlainText(f"Inventory validation failed: {error}") @@ -4195,22 +4183,24 @@ def _apps_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> No elif succeeded and context == "uninstall": self.apps_status.setText("Application uninstalled successfully. Refreshing inventory…") if not succeeded: - stdout_text = self._apps_stdout.decode("utf-8", errors="replace").strip() + stdout_text = result_object.stdout.decode("utf-8", errors="replace").strip() if stdout_text: self.apps_output.appendPlainText(stdout_text) - self.apps_status.setText(f"{context.capitalize()} failed; review the output below.") - self.apps_output.appendPlainText(f"[finished: exit {exit_code}]\n") - self._apps_process = None + if result_object.outcome == "timed-out": + self.apps_status.setText("App operation exceeded the 10-minute safety limit and was stopped.") + elif result_object.outcome == "cancelled": + self.apps_status.setText("App operation was cancelled.") + else: + self.apps_status.setText(f"{context.capitalize()} failed; review the output below.") + if result_object.error_message: + self.apps_output.appendPlainText(f"Process error: {result_object.error_message}") + exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) + self.apps_output.appendPlainText(f"[finished: {result_object.outcome}; exit {exit_label}]\n") self._apps_context = "" self._update_apps_controls() if succeeded and context == "uninstall": QTimer.singleShot(0, self.refresh_app_inventory) - def _apps_error(self, process_error: QProcess.ProcessError) -> None: - del process_error - if self._apps_process is not None: - self.apps_output.appendPlainText(f"Process error: {self._apps_process.errorString()}") - def _populate_installed_apps(self, apps: tuple[InstalledApp, ...]) -> None: self.installed_apps_table.setSortingEnabled(False) self.installed_apps_table.setRowCount(len(apps)) @@ -4253,7 +4243,7 @@ def _installed_app_selection_changed(self) -> None: self._update_apps_controls() def _update_apps_controls(self) -> None: - running = self._apps_process is not None + running = self._apps_controller.is_running() device_available = self.selected_device() is not None selected = self.selected_installed_bundle_identifier() is not None self.refresh_apps_button.setEnabled(device_available and not running) @@ -4298,9 +4288,9 @@ def uninstall_selected_application(self) -> None: self._start_apps_action(("apps", "uninstall", bundle_identifier), "uninstall") def stop_apps_action(self) -> None: - if self._apps_process is not None: + if self._apps_controller.is_running(): self.apps_output.appendPlainText("Requesting app operation stop…") - self._apps_process.terminate() + self._apps_controller.cancel() def choose_backup_destination(self) -> None: selected = QFileDialog.getExistingDirectory( @@ -5615,19 +5605,19 @@ def closeEvent(self, event: QCloseEvent) -> None: event.ignore() return action_running = self._action_controller.is_running() + apps_running = self._apps_controller.is_running() critical_processes = tuple( process for process in ( self._collection_process, self._sideload_process, - self._apps_process, self._backup_process, self._console_process, self._location_process, ) if process is not None and process.state() != QProcess.ProcessState.NotRunning ) - if action_running or critical_processes: + if action_running or apps_running or critical_processes: should_close = self._confirm( "Stop Active Operations?", "A DDI, evidence, app, backup, Location Lab, or Command Center operation is still running. " @@ -5641,6 +5631,7 @@ def closeEvent(self, event: QCloseEvent) -> None: self._manpage_controller.shutdown(3000, 1000) self._command_drift_controller.shutdown(3000, 1000) self._action_controller.shutdown(10000, 3000) + self._apps_controller.shutdown(10000, 3000) capability_process = self._capability_process if capability_process is not None and capability_process.state() != QProcess.ProcessState.NotRunning: self._terminate_capability_children(capability_process) diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 38a954c..4205e07 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -4,8 +4,12 @@ import sys import time from collections.abc import Callable, Sequence +from pathlib import Path + +from ios_developer_toolkit.qt_process import finite_process_request from ios_developer_toolkit.runtime import ( + ExecutableCommand, INTERNAL_PYMOBILEDEVICE3_FLAG, INTERNAL_SMOKE_TEST_FLAG, INTERNAL_WORKER_FLAG, @@ -213,6 +217,34 @@ def run_smoke_test(arguments: Sequence[str]) -> int: action_output = window.action_output.toPlainText() if "[finished: succeeded; exit 0]" not in action_output: raise RuntimeError(f"GUI DDI action controller failed its host-only smoke command: {action_output}") + synthetic_inventory = ( + '{"com.example.toolkit-smoke": {' + '"CFBundleIdentifier": "com.example.toolkit-smoke", ' + '"CFBundleDisplayName": "Toolkit Smoke", ' + '"ApplicationType": "User"}}' + ) + window._apps_context = "inventory" + window._apps_controller.start( + finite_process_request( + ExecutableCommand(Path("/usr/bin/printf"), ()), + (synthetic_inventory,), + {}, + 5_000, + 500, + ) + ) + apps_deadline = time.monotonic() + 10 + while window._apps_controller.is_running() and time.monotonic() < apps_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._apps_controller.is_running(): + window._apps_controller.cancel() + raise RuntimeError("GUI installed-apps controller did not complete within its bounded smoke-test window") + if window.installed_apps_table.rowCount() != 1 or "Loaded 1 installed" not in window.apps_status.text(): + raise RuntimeError( + f"GUI installed-apps controller did not render its synthetic inventory: {window.apps_status.text()}" + ) window.close() application.processEvents() print(f"GUI smoke test passed with {len(buttons)} action buttons", flush=True) From 9fdc5e90c222ef444696a874d75d3273f40f08eb Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:50:59 -0700 Subject: [PATCH 04/16] Centralize IPA inspection and installation --- README.md | 4 +- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- ios_developer_toolkit/app.py | 154 ++++++++++++---------------- ios_developer_toolkit/entrypoint.py | 56 ++++++++++ 4 files changed, 128 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 541240d..10d9ab0 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one selected-device workflow; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 94 tests, embedded CLI checks, a 90-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 95 tests, embedded CLI checks, a 90-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. The README contains 17 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. @@ -645,7 +645,7 @@ Selecting an IPA starts host-side inspection before the install control can be e - reports bundle ID, display name, version, build, executable, signature status, team, certificate authorities, provisioning UUID, expiration, device count, debugging entitlement, and all-device provisioning state where available; - keeps installation disabled when the signature is missing or invalid. -After inspection, choose normal installation or **Install as developer package** and confirm the operation. The toolkit does not sign, patch, re-sign, decrypt, or repair the IPA. Stock iOS still enforces package integrity, provisioning, trust, device eligibility, entitlements, and any App Store DRM. A DDI does not bypass those policies. +After inspection, choose normal installation or **Install as developer package** and confirm the operation. Inspection and installation use the same bounded operation lifecycle as other finite toolkit actions: terminal output is drained, launch failures are explicit, installation can be cancelled, inspection is limited to five minutes, and installation is limited to 15 minutes. The toolkit does not sign, patch, re-sign, decrypt, or repair the IPA. Stock iOS still enforces package integrity, provisioning, trust, device eligibility, entitlements, and any App Store DRM. A DDI does not bypass those policies. Successful installation refreshes the Installed Apps inventory. Removal is a separate confirmed action in that workspace. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 47dc9a4..1355bc0 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -113,7 +113,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P1 — turn diagnostics into a coherent workbench -* Continue migrating finite subprocess workflows to the reusable operation controller and typed `OperationResult`. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, and Installed Apps inventory/uninstall now share final-drain, timeout, cancellation, launch-failure, clean-relaunch, and structured completion semantics; backup, IPA inspection/install, and capture remain incremental migrations. +* Continue migrating finite subprocess workflows to the reusable operation controller and typed `OperationResult`. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, Installed Apps inventory/uninstall, and IPA inspection/install now share final-drain, timeout, cancellation where user-facing cancellation is appropriate, launch-failure, clean-relaunch, and structured completion semantics; backup and capture remain specialized incremental migrations. * Make a contextual readiness pane for the selected action, with one-click scoped rechecks and copyable remediation. * Maintain the opt-in physical-device compatibility protocol and its explicit USB, usbmux, CoreDevice, developer-service, privacy, and state-changing test boundaries. A pre-release dual-architecture frozen-artifact smoke workflow is now present. The release builder rejects any bundled Mach-O whose minimum macOS version is newer than the advertised 13.0 floor or lacks the native release architecture. * Generate concise changelog/release notes from tested behavior. Source, bundle, citation, packaging, and third-party-source metadata drift is now covered by automated tests. @@ -171,6 +171,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-21 | Migrated sequential command-drift probes to the shared finite-operation controller. | A clean Python 3.13 environment passed the 94-test suite and the GUI smoke now runs the entire 49-route drift check through the real asynchronous UI path. | DDI, backup, app, and capture operations remain incremental controller migrations. | | 2026-09-22 | Migrated DDI and Developer Mode actions to the shared finite-operation controller. | The 95-test suite and 90-action GUI smoke passed; the smoke test now executes a real bounded host-only command through the migrated path. | Backup, app, and capture operations remain incremental controller migrations. | | 2026-09-22 | Migrated Installed Apps inventory and uninstall operations to the shared finite-operation controller. | The 95-test suite and 90-action GUI smoke passed; the smoke test now renders a synthetic inventory through the migrated asynchronous result path. | Backup, IPA inspection/install, and capture operations remain incremental controller migrations. | +| 2026-09-22 | Migrated local IPA inspection and device installation to the shared finite-operation controller with explicit five- and 15-minute limits. | The 95-test suite and 90-action GUI smoke passed; the smoke test now validates typed inspection metadata, streamed installation output, and structured completion. | Backup and evidence capture retain specialized worker lifecycles pending deliberate migration. | ## Research sources diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index 6abe293..26a6df5 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -212,6 +212,8 @@ DEVICE_SCAN_TIMEOUT_MS = 10_000 DDI_ACTION_TIMEOUT_MS = 15 * 60_000 APPS_ACTION_TIMEOUT_MS = 10 * 60_000 +IPA_INSPECTION_TIMEOUT_MS = 5 * 60_000 +IPA_INSTALL_TIMEOUT_MS = 15 * 60_000 PROCESS_TERMINATE_GRACE_MS = 1_500 @@ -529,14 +531,15 @@ def __init__(self) -> None: except DeviceCompatibilityError as error: self._compatibility_history_error = str(error) self._collection_process: QProcess | None = None - self._ipa_inspection_process: QProcess | None = None - self._ipa_inspection_stdout = bytearray() - self._ipa_inspection_stderr = bytearray() + self._ipa_inspection_controller = FiniteProcessController(self) + self._ipa_inspection_controller.completed.connect(self._ipa_inspection_completed) self._ipa_inspection: IPAInspection | None = None self._selected_ipa: Path | None = None - self._sideload_process: QProcess | None = None + self._sideload_controller = FiniteProcessController(self) + self._sideload_controller.stdout_received.connect(self._append_sideload_output) + self._sideload_controller.stderr_received.connect(self._append_sideload_output) + self._sideload_controller.completed.connect(self._sideload_completed) self._sideload_context = "" - self._sideload_buffer = bytearray() self._apps_controller = FiniteProcessController(self) self._apps_controller.stderr_received.connect(self._append_apps_stderr) self._apps_controller.completed.connect(self._apps_completed) @@ -3948,48 +3951,40 @@ def _start_ipa_inspection(self) -> None: selected_ipa = self._selected_ipa if selected_ipa is None: raise IPAInspectionError("No IPA path was selected for inspection") - if self._ipa_inspection_process is not None: + if self._ipa_inspection_controller.is_running(): QMessageBox.warning(self, "Inspection Running", "Wait for the current IPA inspection to finish.") return self._ipa_inspection = None - self._ipa_inspection_stdout.clear() - self._ipa_inspection_stderr.clear() self.ipa_inspection_summary.clear() self.ipa_inspection_summary.setPlainText("Inspecting archive, provisioning profile, and code signature…") self.ipa_inspection_progress.setVisible(True) worker = worker_command("ipa-inspector") - process = QProcess(self) - process.setProgram(str(worker.program)) - process.setArguments(list(command_arguments(worker, (str(selected_ipa),)))) - process.setProcessEnvironment(qprocess_environment(base_environment())) - process.readyReadStandardOutput.connect(self._read_ipa_inspection_stdout) - process.readyReadStandardError.connect(self._read_ipa_inspection_stderr) - process.finished.connect(self._ipa_inspection_finished) - process.errorOccurred.connect(self._ipa_inspection_error) - self._ipa_inspection_process = process + self._ipa_inspection_controller.start( + finite_process_request( + worker, + (str(selected_ipa),), + base_environment(), + IPA_INSPECTION_TIMEOUT_MS, + PROCESS_TERMINATE_GRACE_MS, + ) + ) self._update_sideload_controls() - process.start() - def _read_ipa_inspection_stdout(self) -> None: - if self._ipa_inspection_process is not None: - self._ipa_inspection_stdout.extend(bytes(self._ipa_inspection_process.readAllStandardOutput())) - - def _read_ipa_inspection_stderr(self) -> None: - if self._ipa_inspection_process is not None: - self._ipa_inspection_stderr.extend(bytes(self._ipa_inspection_process.readAllStandardError())) - - def _ipa_inspection_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: - del exit_status - self._read_ipa_inspection_stdout() - self._read_ipa_inspection_stderr() - stderr_text = self._ipa_inspection_stderr.decode("utf-8", errors="replace").strip() - if exit_code != 0: - message = stderr_text or f"IPA inspector exited with status {exit_code}" + def _ipa_inspection_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + stderr_text = result_object.stderr.decode("utf-8", errors="replace").strip() + if result_object.outcome != "succeeded": + exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) + message = stderr_text or result_object.error_message or f"IPA inspector exited with status {exit_label}" self.ipa_inspection_summary.setPlainText(message) - self.sideload_status.setText("IPA inspection failed. Correct the package error before installation.") + if result_object.outcome == "timed-out": + self.sideload_status.setText("IPA inspection exceeded the five-minute safety limit and was stopped.") + else: + self.sideload_status.setText("IPA inspection failed. Correct the package error before installation.") else: try: - inspection = parse_inspection_json(self._ipa_inspection_stdout.decode("utf-8")) + inspection = parse_inspection_json(result_object.stdout.decode("utf-8")) except (IPAInspectionError, UnicodeDecodeError) as error: self.ipa_inspection_summary.setPlainText(f"IPA inspection output validation failed: {error}") self.sideload_status.setText("IPA inspection failed. The inspector returned malformed data.") @@ -4004,21 +3999,13 @@ def _ipa_inspection_finished(self, exit_code: int, exit_status: QProcess.ExitSta self.sideload_status.setText( f"Installation is disabled because the extracted bundle signature is {inspection.signature.status}." ) - self._ipa_inspection_process = None self.ipa_inspection_progress.setVisible(False) self._update_sideload_controls() - def _ipa_inspection_error(self, process_error: QProcess.ProcessError) -> None: - del process_error - if self._ipa_inspection_process is not None: - self.ipa_inspection_summary.setPlainText( - f"Could not start IPA inspection: {self._ipa_inspection_process.errorString()}" - ) - def _update_sideload_controls(self) -> None: device_available = self.selected_device() is not None - action_running = self._sideload_process is not None - inspection_running = self._ipa_inspection_process is not None + action_running = self._sideload_controller.is_running() + inspection_running = self._ipa_inspection_controller.is_running() signature_valid = self._ipa_inspection is not None and self._ipa_inspection.signature.status == "valid" self.choose_ipa_button.setEnabled(not inspection_running and not action_running) self.install_ipa_button.setEnabled(device_available and signature_valid and not action_running and not inspection_running) @@ -4070,63 +4057,58 @@ def _start_sideload_action(self, arguments: tuple[str, ...], context: str) -> No if device is None: self._show_no_device() return - if self._sideload_process is not None: + if self._sideload_controller.is_running(): QMessageBox.warning(self, "App Operation Running", "Stop or wait for the active app operation first.") return - self._sideload_buffer.clear() self._sideload_context = context self.sideload_output.appendPlainText(f"\n$ pymobiledevice3 {shlex.join(arguments)}\n") - process = QProcess(self) - process.setProgram(str(self._pmd3.program)) - process.setArguments(list(command_arguments(self._pmd3, arguments))) - process.setWorkingDirectory(str(Path.home())) - process.setProcessEnvironment(qprocess_environment(device_environment(device.identifier))) - process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) - process.readyReadStandardOutput.connect(self._read_sideload_output) - process.finished.connect(self._sideload_finished) - process.errorOccurred.connect(self._sideload_error) - self._sideload_process = process self.sideload_status.setText(f"Running {context} operation on {device.display_name()}…") self.sideload_activity_progress.setVisible(True) + self._sideload_controller.start( + finite_process_request( + self._pmd3, + arguments, + device_environment(device.identifier), + IPA_INSTALL_TIMEOUT_MS, + PROCESS_TERMINATE_GRACE_MS, + ) + ) self._update_sideload_controls() - process.start() - def _read_sideload_output(self) -> None: - if self._sideload_process is None: - return - output = bytes(self._sideload_process.readAllStandardOutput()) - self._sideload_buffer.extend(output) + def _append_sideload_output(self, output: bytes) -> None: self.sideload_output.moveCursor(QTextCursor.MoveOperation.End) self.sideload_output.insertPlainText(output.decode("utf-8", errors="replace")) - def _sideload_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: - del exit_status - self._read_sideload_output() + def _sideload_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") context = self._sideload_context - semantic_failure = output_indicates_failure(bytes(self._sideload_buffer)) - succeeded = exit_code == 0 and not semantic_failure - self.sideload_output.appendPlainText(f"\n[finished: exit {exit_code}]\n") - self.sideload_status.setText( - f"{context.capitalize()} completed successfully." - if succeeded - else f"{context.capitalize()} failed; review the complete command output above." + semantic_failure = output_indicates_failure(result_object.stdout + result_object.stderr) + succeeded = result_object.outcome == "succeeded" and not semantic_failure + exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) + self.sideload_output.appendPlainText( + f"\n[finished: {result_object.outcome}; exit {exit_label}]\n" ) - self._sideload_process = None + if result_object.error_message: + self.sideload_output.appendPlainText(f"Process error: {result_object.error_message}") + if succeeded: + self.sideload_status.setText(f"{context.capitalize()} completed successfully.") + elif result_object.outcome == "timed-out": + self.sideload_status.setText("IPA installation exceeded the 15-minute safety limit and was stopped.") + elif result_object.outcome == "cancelled": + self.sideload_status.setText("IPA installation was cancelled; verify device state before retrying.") + else: + self.sideload_status.setText(f"{context.capitalize()} failed; review the complete command output above.") self._sideload_context = "" self.sideload_activity_progress.setVisible(False) self._update_sideload_controls() if succeeded and context == "install" and not self._apps_controller.is_running(): QTimer.singleShot(0, self.refresh_app_inventory) - def _sideload_error(self, process_error: QProcess.ProcessError) -> None: - del process_error - if self._sideload_process is not None: - self.sideload_output.appendPlainText(f"\nProcess error: {self._sideload_process.errorString()}") - def stop_sideload_action(self) -> None: - if self._sideload_process is not None: + if self._sideload_controller.is_running(): self.sideload_output.appendPlainText("\nRequesting app operation stop…") - self._sideload_process.terminate() + self._sideload_controller.cancel() def refresh_app_inventory(self) -> None: device = self.selected_device() @@ -5606,18 +5588,19 @@ def closeEvent(self, event: QCloseEvent) -> None: return action_running = self._action_controller.is_running() apps_running = self._apps_controller.is_running() + ipa_inspection_running = self._ipa_inspection_controller.is_running() + sideload_running = self._sideload_controller.is_running() critical_processes = tuple( process for process in ( self._collection_process, - self._sideload_process, self._backup_process, self._console_process, self._location_process, ) if process is not None and process.state() != QProcess.ProcessState.NotRunning ) - if action_running or apps_running or critical_processes: + if action_running or apps_running or ipa_inspection_running or sideload_running or critical_processes: should_close = self._confirm( "Stop Active Operations?", "A DDI, evidence, app, backup, Location Lab, or Command Center operation is still running. " @@ -5632,6 +5615,8 @@ def closeEvent(self, event: QCloseEvent) -> None: self._command_drift_controller.shutdown(3000, 1000) self._action_controller.shutdown(10000, 3000) self._apps_controller.shutdown(10000, 3000) + self._ipa_inspection_controller.shutdown(10000, 3000) + self._sideload_controller.shutdown(10000, 3000) capability_process = self._capability_process if capability_process is not None and capability_process.state() != QProcess.ProcessState.NotRunning: self._terminate_capability_children(capability_process) @@ -5645,9 +5630,6 @@ def closeEvent(self, event: QCloseEvent) -> None: if not process.waitForFinished(10000): process.kill() process.waitForFinished(3000) - for process in (self._ipa_inspection_process,): - if process is not None and process.state() != QProcess.ProcessState.NotRunning: - process.terminate() event.accept() diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 4205e07..f988225 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -245,6 +245,62 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError( f"GUI installed-apps controller did not render its synthetic inventory: {window.apps_status.text()}" ) + synthetic_inspection = ( + '{"ipa_path":"/tmp/ToolkitSmoke.ipa","app_name":"Toolkit Smoke",' + '"bundle_identifier":"com.example.toolkit-smoke","version":"1.0","build":"1",' + '"minimum_os_version":"17.0","executable_name":"ToolkitSmoke",' + '"provisioning":{"status":"present","name":"Toolkit Smoke Profile","uuid":"smoke-uuid",' + '"team_identifiers":["SMOKETEAM"],' + '"application_identifier":"SMOKETEAM.com.example.toolkit-smoke",' + '"expiration":"2030-01-01T00:00:00+00:00","provisioned_device_count":1,' + '"provisions_all_devices":false,"get_task_allow":true,' + '"developer_certificate_count":1,"detail":"Synthetic smoke-test profile"},' + '"signature":{"status":"valid","identifier":"com.example.toolkit-smoke",' + '"team_identifier":"SMOKETEAM","authorities":["Toolkit Smoke Authority"],' + '"detail":"Synthetic smoke-test signature"}}' + ) + window._ipa_inspection_controller.start( + finite_process_request( + ExecutableCommand(Path("/usr/bin/printf"), ()), + (synthetic_inspection,), + {}, + 5_000, + 500, + ) + ) + inspection_deadline = time.monotonic() + 10 + while window._ipa_inspection_controller.is_running() and time.monotonic() < inspection_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._ipa_inspection_controller.is_running(): + window._ipa_inspection_controller.cancel() + raise RuntimeError("GUI IPA inspection controller did not complete within its bounded smoke-test window") + if window._ipa_inspection is None or window._ipa_inspection.signature.status != "valid": + raise RuntimeError(f"GUI IPA inspection controller rejected typed metadata: {window.sideload_status.text()}") + window._sideload_context = "smoke" + window._sideload_controller.start( + finite_process_request( + ExecutableCommand(Path("/usr/bin/printf"), ()), + ("Synthetic IPA operation output",), + {}, + 5_000, + 500, + ) + ) + sideload_deadline = time.monotonic() + 10 + while window._sideload_controller.is_running() and time.monotonic() < sideload_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._sideload_controller.is_running(): + window._sideload_controller.cancel() + raise RuntimeError("GUI IPA installation controller did not complete within its bounded smoke-test window") + sideload_output = window.sideload_output.toPlainText() + if "Synthetic IPA operation output" not in sideload_output or "[finished: succeeded; exit 0]" not in sideload_output: + raise RuntimeError(f"GUI IPA installation controller did not preserve its output: {sideload_output}") + if window.sideload_status.text() != "Smoke completed successfully.": + raise RuntimeError(f"GUI IPA installation controller reported the wrong state: {window.sideload_status.text()}") window.close() application.processEvents() print(f"GUI smoke test passed with {len(buttons)} action buttons", flush=True) From 78f363cd3afbc1b7b1417a374c37eae981b9e72f Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 03:01:23 -0700 Subject: [PATCH 05/16] Harden backup worker lifecycle --- README.md | 6 +- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- ios_developer_toolkit/app.py | 174 +++++++---------- ios_developer_toolkit/backup_process.py | 233 +++++++++++++++++++++++ ios_developer_toolkit/backup_protocol.py | 30 +++ ios_developer_toolkit/backup_worker.py | 7 +- ios_developer_toolkit/entrypoint.py | 25 +++ tests/test_backup_process.py | 100 ++++++++++ 8 files changed, 464 insertions(+), 114 deletions(-) create mode 100644 ios_developer_toolkit/backup_process.py create mode 100644 tests/test_backup_process.py diff --git a/README.md b/README.md index 10d9ab0..96e89d4 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one selected-device workflow; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 95 tests, embedded CLI checks, a 90-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 98 tests, embedded CLI checks, a 90-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. The README contains 17 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. @@ -578,7 +578,7 @@ If encryption is currently off and **Require encrypted backup** is selected: Backup encryption is a persistent device setting. The toolkit never disables it automatically. Store the password securely: an encrypted backup cannot be restored without it. Do not reuse an account password or device passcode. -Stopping a backup asks the worker to stop and preserves visible status. Confirm the finalized backup state before depending on it for recovery or evidence. +The GUI starts the backup helper asynchronously, validates each structured progress event, drains terminal output, and converges launch failure, worker failure, cancellation, and success on one typed result. Malformed worker output stops the operation instead of silently continuing with an unreliable progress channel. Stopping a backup asks the worker to stop and preserves visible status. Confirm the finalized backup state before depending on it for recovery or evidence. #### UFADE External @@ -1045,7 +1045,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 95 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 90-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 98 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 90-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 1355bc0..64a433d 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -113,7 +113,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P1 — turn diagnostics into a coherent workbench -* Continue migrating finite subprocess workflows to the reusable operation controller and typed `OperationResult`. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, Installed Apps inventory/uninstall, and IPA inspection/install now share final-drain, timeout, cancellation where user-facing cancellation is appropriate, launch-failure, clean-relaunch, and structured completion semantics; backup and capture remain specialized incremental migrations. +* Continue migrating subprocess workflows to typed lifecycle controllers and `OperationResult`. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, Installed Apps inventory/uninstall, and IPA inspection/install use the finite-operation controller. Backup uses a specialized controller that preserves private stdin requests and validates streamed progress events without imposing an arbitrary completion timeout. Evidence capture remains a specialized incremental migration because it must finalize partial evidence and coverage records. * Make a contextual readiness pane for the selected action, with one-click scoped rechecks and copyable remediation. * Maintain the opt-in physical-device compatibility protocol and its explicit USB, usbmux, CoreDevice, developer-service, privacy, and state-changing test boundaries. A pre-release dual-architecture frozen-artifact smoke workflow is now present. The release builder rejects any bundled Mach-O whose minimum macOS version is newer than the advertised 13.0 floor or lacks the native release architecture. * Generate concise changelog/release notes from tested behavior. Source, bundle, citation, packaging, and third-party-source metadata drift is now covered by automated tests. @@ -172,6 +172,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Migrated DDI and Developer Mode actions to the shared finite-operation controller. | The 95-test suite and 90-action GUI smoke passed; the smoke test now executes a real bounded host-only command through the migrated path. | Backup, app, and capture operations remain incremental controller migrations. | | 2026-09-22 | Migrated Installed Apps inventory and uninstall operations to the shared finite-operation controller. | The 95-test suite and 90-action GUI smoke passed; the smoke test now renders a synthetic inventory through the migrated asynchronous result path. | Backup, IPA inspection/install, and capture operations remain incremental controller migrations. | | 2026-09-22 | Migrated local IPA inspection and device installation to the shared finite-operation controller with explicit five- and 15-minute limits. | The 95-test suite and 90-action GUI smoke passed; the smoke test now validates typed inspection metadata, streamed installation output, and structured completion. | Backup and evidence capture retain specialized worker lifecycles pending deliberate migration. | +| 2026-09-22 | Replaced the Backup workspace's blocking, hand-buffered process path with a password-safe typed controller. | 98 tests and the 90-action GUI smoke passed; real child-process tests cover stdin-only credentials, validated streamed events, malformed-protocol termination, and one-result cancellation. | Evidence capture still needs a lifecycle designed around partial-artifact finalization rather than a generic finite command. | ## Research sources diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index 26a6df5..5e8304d 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -70,7 +70,8 @@ confirmation_phrase, guided_action_safety, ) -from ios_developer_toolkit.backup_protocol import BackupEvent, BackupRequestError, parse_backup_event +from ios_developer_toolkit.backup_process import BackupProcessController +from ios_developer_toolkit.backup_protocol import BackupAction, BackupEvent, BackupRequest, BackupRequestError from ios_developer_toolkit.case_workflow import CaseWorkflowError, create_guided_case from ios_developer_toolkit.capability_matrix import ( CapabilityMatrixError, @@ -545,10 +546,11 @@ def __init__(self) -> None: self._apps_controller.completed.connect(self._apps_completed) self._apps_context = "" self._installed_apps: tuple[InstalledApp, ...] = () - self._backup_process: QProcess | None = None - self._backup_action = "" - self._backup_stdout = bytearray() - self._backup_stderr = bytearray() + self._backup_controller = BackupProcessController(self) + self._backup_controller.event_received.connect(self._handle_backup_event) + self._backup_controller.stderr_received.connect(self._append_backup_stderr) + self._backup_controller.completed.connect(self._backup_completed) + self._backup_action: BackupAction | None = None self._backup_encryption_state: bool | None = None self._last_backup_path: Path | None = None self._ufade_installation: UFADEInstallation | None = None @@ -4299,19 +4301,15 @@ def check_backup_encryption(self) -> None: except BackupRequestError as error: QMessageBox.critical(self, "Invalid Backup Destination", str(error)) return - request: dict[str, str | bool] = { - "udid": device.identifier, - "destination": str(destination), - "require_encryption": False, - "new_password": "", - "full": False, - } + request = BackupRequest(device.identifier, destination, False, "", False) self._start_backup_worker("status", request) def _backup_encryption_choice_changed(self, checked: bool) -> None: needs_new_password = checked and self._backup_encryption_state is not True controls_enabled = ( - needs_new_password and self._backup_process is None and self.selected_device() is not None + needs_new_password + and not self._backup_controller.is_running() + and self.selected_device() is not None ) self.backup_password_field.setEnabled(controls_enabled) self.backup_password_confirmation_field.setEnabled(controls_enabled) @@ -4367,84 +4365,41 @@ def start_backup(self) -> None: if not self._confirm_action("Start Device Backup", warning, profile, device.identifier): return self._record_action_approval(self.backup_output, "Start Device Backup", profile) - request: dict[str, str | bool] = { - "udid": device.identifier, - "destination": str(destination), - "require_encryption": require_encryption, - "new_password": password, - "full": self.full_backup_checkbox.isChecked(), - } + request = BackupRequest( + device.identifier, + destination, + require_encryption, + password, + self.full_backup_checkbox.isChecked(), + ) self._start_backup_worker("backup", request) self.backup_password_field.clear() self.backup_password_confirmation_field.clear() - def _start_backup_worker(self, action: str, request: dict[str, str | bool]) -> None: - if self._backup_process is not None: + def _start_backup_worker(self, action: BackupAction, request: BackupRequest) -> None: + if self._backup_controller.is_running(): QMessageBox.warning(self, "Backup Operation Running", "Stop or wait for the active backup operation first.") return self._backup_action = action - self._backup_stdout.clear() - self._backup_stderr.clear() worker = worker_command("backup") - process = QProcess(self) - process.setProgram(str(worker.program)) - process.setArguments(list(command_arguments(worker, (action,)))) - process.setProcessEnvironment(qprocess_environment(base_environment())) - process.readyReadStandardOutput.connect(self._read_backup_stdout) - process.readyReadStandardError.connect(self._read_backup_stderr) - process.finished.connect(self._backup_finished) - process.errorOccurred.connect(self._backup_error) - self._backup_process = process self.backup_output.appendPlainText( "Checking backup encryption…" if action == "status" else "Starting device backup…" ) if action == "backup": self.backup_progress.setValue(0) + self._backup_controller.start( + worker, + action, + request, + base_environment(), + PROCESS_TERMINATE_GRACE_MS, + ) self._update_backup_controls() - process.start() - if not process.waitForStarted(3000): - self.backup_output.appendPlainText(f"Could not start backup helper: {process.errorString()}") - self._backup_process = None - self._backup_action = "" - self._update_backup_controls() - return - request_payload = json.dumps(request).encode("utf-8") - accepted_bytes = process.write(request_payload) - if accepted_bytes != len(request_payload): - process.kill() - process.waitForFinished(3000) - self._backup_process = None - self._backup_action = "" - self._update_backup_controls() - message = f"Backup helper accepted {accepted_bytes} of {len(request_payload)} request bytes." - self.backup_output.appendPlainText(message) - QMessageBox.critical( - self, - "Backup Request Failed", - f"{message}\nNo backup operation was started.", - ) - return - process.closeWriteChannel() - def _read_backup_stdout(self) -> None: - if self._backup_process is None: - return - self._backup_stdout.extend(bytes(self._backup_process.readAllStandardOutput())) - while b"\n" in self._backup_stdout: - line, _, remainder = self._backup_stdout.partition(b"\n") - self._backup_stdout = bytearray(remainder) - if line.strip(): - self._handle_backup_event_line(line) - - def _handle_backup_event_line(self, line: bytes) -> None: - try: - event = parse_backup_event(line.decode("utf-8")) - except (BackupRequestError, json.JSONDecodeError, UnicodeDecodeError) as error: - self.backup_output.appendPlainText(f"Invalid backup helper event: {error}") - return - self._handle_backup_event(event) - - def _handle_backup_event(self, event: BackupEvent) -> None: + def _handle_backup_event(self, event_object: object) -> None: + if not isinstance(event_object, BackupEvent): + raise TypeError(f"Expected BackupEvent, received {type(event_object).__name__}") + event = event_object self.backup_output.appendPlainText(event.message) if event.percent is not None: self.backup_progress.setValue(max(0, min(100, event.percent))) @@ -4456,40 +4411,38 @@ def _handle_backup_event(self, event: BackupEvent) -> None: if event.path is not None: self._last_backup_path = event.path - def _read_backup_stderr(self) -> None: - if self._backup_process is None: - return - output = bytes(self._backup_process.readAllStandardError()) - self._backup_stderr.extend(output) + def _append_backup_stderr(self, output: bytes) -> None: self.backup_output.moveCursor(QTextCursor.MoveOperation.End) self.backup_output.insertPlainText(output.decode("utf-8", errors="replace")) - def _backup_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: - del exit_status - self._read_backup_stdout() - self._read_backup_stderr() - if self._backup_stdout.strip(): - self._handle_backup_event_line(bytes(self._backup_stdout)) - self._backup_stdout.clear() + def _backup_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") action = self._backup_action - if exit_code == 0: + if result_object.outcome == "succeeded": self.backup_output.appendPlainText( "Encryption status check completed." if action == "status" else "Backup operation completed successfully." ) + elif result_object.outcome == "cancelled": + self.backup_output.appendPlainText( + "Encryption status check stopped." + if action == "status" + else "Backup operation stopped. Any partial destination remains incomplete and must be reviewed before reuse." + ) else: - self.backup_output.appendPlainText(f"{action.capitalize()} failed with exit code {exit_code}.") - self._backup_process = None - self._backup_action = "" + action_label = "Backup" if action is None else action.capitalize() + exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) + self.backup_output.appendPlainText( + f"{action_label} failed ({result_object.outcome}; exit {exit_label})." + ) + if result_object.error_message: + self.backup_output.appendPlainText(f"Process error: {result_object.error_message}") + self._backup_action = None self._update_backup_controls() self._backup_encryption_choice_changed(self.require_encryption_checkbox.isChecked()) - def _backup_error(self, process_error: QProcess.ProcessError) -> None: - del process_error - if self._backup_process is not None: - self.backup_output.appendPlainText(f"Process error: {self._backup_process.errorString()}") - def _update_backup_controls(self) -> None: - running = self._backup_process is not None + running = self._backup_controller.is_running() device_available = self.selected_device() is not None self.start_backup_button.setEnabled(device_available and not running) self.check_encryption_button.setEnabled(device_available and not running) @@ -4503,11 +4456,14 @@ def _update_backup_controls(self) -> None: self._backup_encryption_choice_changed(self.require_encryption_checkbox.isChecked()) def stop_backup(self) -> None: - if self._backup_process is not None: - self.backup_output.appendPlainText( - "Stopping the backup. The partial destination may be incomplete and will not be treated as valid incremental state." - ) - self._backup_process.terminate() + if self._backup_controller.is_running(): + if self._backup_action == "status": + self.backup_output.appendPlainText("Stopping the encryption status check…") + else: + self.backup_output.appendPlainText( + "Stopping the backup. The partial destination may be incomplete and will not be treated as valid incremental state." + ) + self._backup_controller.cancel() def open_backup_folder(self) -> None: try: @@ -5590,17 +5546,24 @@ def closeEvent(self, event: QCloseEvent) -> None: apps_running = self._apps_controller.is_running() ipa_inspection_running = self._ipa_inspection_controller.is_running() sideload_running = self._sideload_controller.is_running() + backup_running = self._backup_controller.is_running() critical_processes = tuple( process for process in ( self._collection_process, - self._backup_process, self._console_process, self._location_process, ) if process is not None and process.state() != QProcess.ProcessState.NotRunning ) - if action_running or apps_running or ipa_inspection_running or sideload_running or critical_processes: + if ( + action_running + or apps_running + or ipa_inspection_running + or sideload_running + or backup_running + or critical_processes + ): should_close = self._confirm( "Stop Active Operations?", "A DDI, evidence, app, backup, Location Lab, or Command Center operation is still running. " @@ -5617,6 +5580,7 @@ def closeEvent(self, event: QCloseEvent) -> None: self._apps_controller.shutdown(10000, 3000) self._ipa_inspection_controller.shutdown(10000, 3000) self._sideload_controller.shutdown(10000, 3000) + self._backup_controller.shutdown(10000, 3000) capability_process = self._capability_process if capability_process is not None and capability_process.state() != QProcess.ProcessState.NotRunning: self._terminate_capability_children(capability_process) diff --git a/ios_developer_toolkit/backup_process.py b/ios_developer_toolkit/backup_process.py new file mode 100644 index 0000000..b2c9218 --- /dev/null +++ b/ios_developer_toolkit/backup_process.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone +from typing import Literal, Mapping + +from PySide6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, Signal + +from ios_developer_toolkit.backup_protocol import ( + BackupAction, + BackupEvent, + BackupRequest, + BackupRequestError, + parse_backup_event, + serialize_backup_request, +) +from ios_developer_toolkit.qt_process import OperationResult, ProcessOutcome +from ios_developer_toolkit.runtime import ExecutableCommand, command_arguments, command_argv + + +class BackupProcessController(QObject): + """Own one backup worker while keeping credentials out of process arguments.""" + + event_received = Signal(object) + stderr_received = Signal(bytes) + completed = Signal(object) + + def __init__(self, parent: QObject) -> None: + super().__init__(parent) + self._process: QProcess | None = None + self._command: ExecutableCommand | None = None + self._arguments: tuple[str, ...] = () + self._terminate_grace_milliseconds = 0 + self._request_payload = b"" + self._stdout = bytearray() + self._stdout_line = bytearray() + self._stderr = bytearray() + self._started_at = "" + self._error_message: str | None = None + self._stop_outcome: Literal["cancelled"] | None = None + self._protocol_failed = False + self._completed = False + self._kill_timer = QTimer(self) + self._kill_timer.setSingleShot(True) + self._kill_timer.timeout.connect(self._kill) + + def is_running(self) -> bool: + return self._process is not None + + def start( + self, + command: ExecutableCommand, + action: BackupAction, + request: BackupRequest, + environment: Mapping[str, str], + terminate_grace_milliseconds: int, + ) -> None: + if self.is_running(): + raise RuntimeError("Cannot start a backup process while another backup process is running") + if action not in ("status", "backup"): + raise ValueError(f"Unsupported backup action: {action}") + if terminate_grace_milliseconds <= 0: + raise ValueError(f"Backup termination grace period must be positive: {terminate_grace_milliseconds}") + self._command = command + self._arguments = (action,) + self._terminate_grace_milliseconds = terminate_grace_milliseconds + self._request_payload = serialize_backup_request(request) + self._stdout.clear() + self._stdout_line.clear() + self._stderr.clear() + self._started_at = datetime.now(timezone.utc).isoformat() + self._error_message = None + self._stop_outcome = None + self._protocol_failed = False + self._completed = False + + process = QProcess(self) + process.setProgram(str(command.program)) + process.setArguments(list(command_arguments(command, self._arguments))) + process_environment = QProcessEnvironment.systemEnvironment() + for key, value in sorted(environment.items()): + process_environment.insert(key, value) + process.setProcessEnvironment(process_environment) + process.started.connect(self._write_request) + process.readyReadStandardOutput.connect(self._drain_output) + process.readyReadStandardError.connect(self._drain_output) + process.errorOccurred.connect(self._process_error) + process.finished.connect(self._finished) + self._process = process + process.start() + + def cancel(self) -> None: + if not self.is_running(): + return + self._stop_outcome = "cancelled" + self._terminate() + + def shutdown(self, terminate_timeout_milliseconds: int, kill_timeout_milliseconds: int) -> None: + if terminate_timeout_milliseconds <= 0: + raise ValueError(f"Shutdown termination timeout must be positive: {terminate_timeout_milliseconds}") + if kill_timeout_milliseconds <= 0: + raise ValueError(f"Shutdown kill timeout must be positive: {kill_timeout_milliseconds}") + process = self._process + if process is None: + return + if process.state() == QProcess.ProcessState.NotRunning: + self._finish_once("cancelled", process.exitCode()) + return + self._stop_outcome = "cancelled" + self._kill_timer.stop() + process.terminate() + if not process.waitForFinished(terminate_timeout_milliseconds): + process.kill() + if not process.waitForFinished(kill_timeout_milliseconds): + raise RuntimeError(f"Backup process did not stop after terminate and kill: {process.program()}") + + def _write_request(self) -> None: + process = self._process + if process is None: + raise RuntimeError("Backup process started without an active process") + accepted_bytes = process.write(self._request_payload) + if accepted_bytes != len(self._request_payload): + self._protocol_failure( + f"Backup helper accepted {accepted_bytes} of {len(self._request_payload)} request bytes" + ) + return + process.closeWriteChannel() + self._request_payload = b"" + + def _drain_output(self) -> None: + process = self._process + if process is None: + return + stdout = bytes(process.readAllStandardOutput()) + stderr = bytes(process.readAllStandardError()) + if stdout: + self._stdout.extend(stdout) + self._stdout_line.extend(stdout) + self._consume_complete_lines() + if stderr: + self._stderr.extend(stderr) + self.stderr_received.emit(stderr) + + def _consume_complete_lines(self) -> None: + while b"\n" in self._stdout_line and not self._protocol_failed: + line, _, remainder = self._stdout_line.partition(b"\n") + self._stdout_line = bytearray(remainder) + if line.strip(): + self._consume_event_line(line) + + def _consume_event_line(self, line: bytes) -> None: + try: + event = parse_backup_event(line.decode("utf-8")) + except (BackupRequestError, json.JSONDecodeError, UnicodeDecodeError) as error: + self._protocol_failure(f"Invalid backup helper event: {error}") + return + self.event_received.emit(event) + + def _protocol_failure(self, message: str) -> None: + if self._protocol_failed: + return + self._protocol_failed = True + self._error_message = message + process = self._process + if process is not None and process.state() != QProcess.ProcessState.NotRunning: + self._terminate() + + def _process_error(self, process_error: QProcess.ProcessError) -> None: + process = self._process + if process is None: + raise RuntimeError("Backup process reported an error without an active process") + if self._error_message is None: + self._error_message = process.errorString() + if process_error == QProcess.ProcessError.FailedToStart: + self._finish_once("launch-failed", None) + + def _finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: + self._drain_output() + if self._stdout_line.strip() and not self._protocol_failed: + line = bytes(self._stdout_line) + self._stdout_line.clear() + self._consume_event_line(line) + if self._stop_outcome is not None: + outcome: ProcessOutcome = self._stop_outcome + elif self._protocol_failed: + outcome = "failed" + elif exit_status == QProcess.ExitStatus.CrashExit: + outcome = "crashed" + elif exit_code == 0: + outcome = "succeeded" + else: + outcome = "failed" + self._finish_once(outcome, exit_code) + + def _terminate(self) -> None: + process = self._process + if process is None: + raise RuntimeError("Cannot terminate a backup process without an active process") + if self._terminate_grace_milliseconds <= 0: + raise RuntimeError("Backup process has no valid termination grace period") + process.terminate() + self._kill_timer.start(self._terminate_grace_milliseconds) + + def _kill(self) -> None: + process = self._process + if process is not None and process.state() != QProcess.ProcessState.NotRunning: + process.kill() + + def _finish_once(self, outcome: ProcessOutcome, exit_code: int | None) -> None: + if self._completed: + return + command = self._command + if command is None: + raise RuntimeError("Backup process completed without a command") + self._drain_output() + self._completed = True + self._kill_timer.stop() + self._request_payload = b"" + result = OperationResult( + command_argv(command, self._arguments), + outcome, + self._started_at, + datetime.now(timezone.utc).isoformat(), + exit_code, + self._error_message, + bytes(self._stdout), + bytes(self._stderr), + ) + process = self._process + self._process = None + if process is not None: + process.deleteLater() + self.completed.emit(result) diff --git a/ios_developer_toolkit/backup_protocol.py b/ios_developer_toolkit/backup_protocol.py index 3767978..510354f 100644 --- a/ios_developer_toolkit/backup_protocol.py +++ b/ios_developer_toolkit/backup_protocol.py @@ -3,6 +3,10 @@ import json from dataclasses import dataclass from pathlib import Path +from typing import Literal + + +BackupAction = Literal["status", "backup"] class BackupRequestError(ValueError): @@ -27,6 +31,32 @@ class BackupEvent: path: Path | None +def serialize_backup_request(request: BackupRequest) -> bytes: + if not isinstance(request.udid, str) or not request.udid.strip(): + raise BackupRequestError("udid must be a non-empty string") + if not isinstance(request.destination, Path): + raise BackupRequestError("destination must be a Path") + if not request.destination.is_absolute(): + raise BackupRequestError("destination must be an absolute path") + if not isinstance(request.require_encryption, bool): + raise BackupRequestError("require_encryption must be a boolean") + if not isinstance(request.new_password, str): + raise BackupRequestError("new_password must be a string") + if not isinstance(request.full, bool): + raise BackupRequestError("full must be a boolean") + return json.dumps( + { + "udid": request.udid, + "destination": str(request.destination), + "require_encryption": request.require_encryption, + "new_password": request.new_password, + "full": request.full, + }, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + + def required_string(value: object, field_name: str) -> str: if not isinstance(value, str) or not value.strip(): raise BackupRequestError(f"{field_name} must be a non-empty string") diff --git a/ios_developer_toolkit/backup_worker.py b/ios_developer_toolkit/backup_worker.py index aae53ec..ca0fce1 100644 --- a/ios_developer_toolkit/backup_worker.py +++ b/ios_developer_toolkit/backup_worker.py @@ -4,17 +4,14 @@ import asyncio import json import sys -from typing import TYPE_CHECKING, Literal, TextIO +from typing import TYPE_CHECKING, TextIO -from ios_developer_toolkit.backup_protocol import BackupRequest, BackupRequestError, parse_backup_request +from ios_developer_toolkit.backup_protocol import BackupAction, BackupRequest, BackupRequestError, parse_backup_request if TYPE_CHECKING: from pymobiledevice3.lockdown import LockdownClient -BackupAction = Literal["status", "backup"] - - def emit_event( event: str, message: str, diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index f988225..da5afe3 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -83,6 +83,7 @@ def run_smoke_test(arguments: Sequence[str]) -> int: from PySide6.QtWidgets import QApplication, QLabel, QPushButton from ios_developer_toolkit.app import MainWindow + from ios_developer_toolkit.backup_protocol import BackupRequest application = QApplication(["ios-developer-toolkit-smoke-test"]) window = MainWindow() @@ -301,6 +302,30 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError(f"GUI IPA installation controller did not preserve its output: {sideload_output}") if window.sideload_status.text() != "Smoke completed successfully.": raise RuntimeError(f"GUI IPA installation controller reported the wrong state: {window.sideload_status.text()}") + backup_smoke_program = ( + 'BEGIN { delete ARGV[1] } END { print "{\\"event\\":\\"encryption-state\\",' + '\\"message\\":\\"Synthetic encryption status.\\",\\"encrypted\\":true}" }' + ) + window._backup_action = "status" + window._backup_controller.start( + ExecutableCommand(Path("/usr/bin/awk"), (backup_smoke_program,)), + "status", + BackupRequest("toolkit-smoke-device", Path("/tmp"), False, "", False), + {}, + 500, + ) + backup_deadline = time.monotonic() + 10 + while window._backup_controller.is_running() and time.monotonic() < backup_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._backup_controller.is_running(): + window._backup_controller.cancel() + raise RuntimeError("GUI backup controller did not complete within its bounded smoke-test window") + if window._backup_encryption_state is not True: + raise RuntimeError(f"GUI backup controller did not apply its typed event: {window.backup_output.toPlainText()}") + if "Encryption status check completed." not in window.backup_output.toPlainText(): + raise RuntimeError(f"GUI backup controller reported the wrong completion: {window.backup_output.toPlainText()}") window.close() application.processEvents() print(f"GUI smoke test passed with {len(buttons)} action buttons", flush=True) diff --git a/tests/test_backup_process.py b/tests/test_backup_process.py new file mode 100644 index 0000000..297afc3 --- /dev/null +++ b/tests/test_backup_process.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import sys +import time +import unittest +from collections.abc import Callable +from pathlib import Path + +from PySide6.QtCore import QCoreApplication + +from ios_developer_toolkit.backup_process import BackupProcessController +from ios_developer_toolkit.backup_protocol import BackupEvent, BackupRequest +from ios_developer_toolkit.qt_process import OperationResult +from ios_developer_toolkit.runtime import ExecutableCommand + + +class BackupProcessControllerTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.application = QCoreApplication.instance() or QCoreApplication(["backup-process-tests"]) + + def test_sends_private_request_over_stdin_and_emits_typed_event(self) -> None: + controller = BackupProcessController(self.application) + events: list[BackupEvent] = [] + results: list[OperationResult] = [] + controller.event_received.connect(events.append) + controller.completed.connect(results.append) + password = "private-smoke-password" + script = ( + "import json,sys; request=json.load(sys.stdin); " + "print(json.dumps({'event':'encryption-state','message':'checked','encrypted':" + "request['require_encryption']}))" + ) + + controller.start( + ExecutableCommand(Path(sys.executable), ("-c", script)), + "status", + BackupRequest("test-device", Path("/tmp"), True, password, False), + {}, + 500, + ) + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(results[0].outcome, "succeeded") + self.assertEqual(events, [BackupEvent("encryption-state", "checked", None, True, None)]) + self.assertNotIn(password, results[0].argv) + self.assertNotIn(password.encode("utf-8"), results[0].stdout) + self.assertFalse(controller.is_running()) + + def test_rejects_malformed_worker_event_and_stops_process(self) -> None: + controller = BackupProcessController(self.application) + results: list[OperationResult] = [] + controller.completed.connect(results.append) + script = "import sys,time; sys.stdin.read(); print('not-json', flush=True); time.sleep(10)" + + controller.start( + ExecutableCommand(Path(sys.executable), ("-c", script)), + "backup", + BackupRequest("test-device", Path("/tmp"), False, "", False), + {}, + 500, + ) + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(results[0].outcome, "failed") + self.assertIsNotNone(results[0].error_message) + self.assertIn("Invalid backup helper event", results[0].error_message or "") + self.assertFalse(controller.is_running()) + + def test_cancels_long_running_worker_once(self) -> None: + controller = BackupProcessController(self.application) + results: list[OperationResult] = [] + controller.completed.connect(results.append) + script = "import sys,time; sys.stdin.read(); time.sleep(10)" + + controller.start( + ExecutableCommand(Path(sys.executable), ("-c", script)), + "backup", + BackupRequest("test-device", Path("/tmp"), False, "", False), + {}, + 500, + ) + self._wait_for(controller.is_running, 1) + controller.cancel() + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].outcome, "cancelled") + self.assertFalse(controller.is_running()) + + def _wait_for(self, predicate: Callable[[], bool], timeout_seconds: int) -> None: + deadline = time.monotonic() + timeout_seconds + while not predicate() and time.monotonic() < deadline: + self.application.processEvents() + time.sleep(0.01) + self.application.processEvents() + + +if __name__ == "__main__": + unittest.main() From 133f5b90103ea1ae9badadd899c5bf4886e1e0dc Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 03:13:33 -0700 Subject: [PATCH 06/16] Preserve evidence capture finalization --- README.md | 13 +- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- ios_developer_toolkit/app.py | 141 ++++++++---- ios_developer_toolkit/backup_process.py | 3 +- ios_developer_toolkit/collection_process.py | 221 +++++++++++++++++++ ios_developer_toolkit/collection_protocol.py | 64 ++++++ ios_developer_toolkit/entrypoint.py | 24 ++ tests/test_collection_process.py | 166 ++++++++++++++ 8 files changed, 581 insertions(+), 54 deletions(-) create mode 100644 ios_developer_toolkit/collection_process.py create mode 100644 ios_developer_toolkit/collection_protocol.py create mode 100644 tests/test_collection_process.py diff --git a/README.md b/README.md index 96e89d4..bba677c 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one selected-device workflow; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 98 tests, embedded CLI checks, a 90-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 102 tests, embedded CLI checks, a 90-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. The README contains 17 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. @@ -685,7 +685,9 @@ Optional scope: - current device screenshot; - complete crash-report pull. -The collector retries failed snapshots once, keeps the final artifact and a complete per-attempt command log, records semantic validation failures, and distinguishes required identification failures from optional coverage gaps. Stop/Finalize ends streams and still finalizes the case where possible. +The collector retries failed snapshots once, keeps the final artifact and a complete per-attempt command log, records semantic validation failures, and distinguishes required identification failures from optional coverage gaps. Its typed GUI controller reassembles JSON events across arbitrary output chunks, drains terminal output, and records launch, protocol, cancellation, crash, and exit outcomes explicitly. + +**Stop/Finalize** sends a graceful stop request and gives the collector up to two minutes to close streams, write `manifest.json`, and regenerate `SHA256SUMS.txt`. Closing the application while a collection is active waits for that finalization instead of immediately killing the worker. If finalization is not confirmed, the guided case remains active for review or retry rather than being labeled complete. ### Man Pages @@ -996,7 +998,8 @@ Install or update Xcode if the candidate is absent. The toolkit requires the exp ├── ios_developer_toolkit/ │ ├── app.py # PySide6 workbench and workflow orchestration │ ├── action_safety.py # typed confirmation policy for state-changing actions -│ ├── backup_protocol.py # dependency-free backup request/event schema +│ ├── backup_process.py # password-safe backup-worker lifecycle controller +│ ├── backup_protocol.py # dependency-free backup request/event schema │ ├── backup_worker.py # MobileBackup2 worker and password-input protocol │ ├── capability_matrix.py # typed readiness catalog, probes, and result validation │ ├── capability_matrix_worker.py # bounded NDJSON capability worker @@ -1004,6 +1007,8 @@ Install or update Xcode if the candidate is absent. The toolkit requires the exp │ ├── catalog.py # evidence snapshot catalog and mutation classification │ ├── collector.py # case creation, streams, retries, manifest, hashes │ ├── command_catalog.py # guided presets and live-help routes +│ ├── collection_process.py # evidence-worker lifecycle and graceful finalization +│ ├── collection_protocol.py # validated collector JSON-line events │ ├── device_compatibility.py # redacted local real-device readiness history │ ├── gui_pages.py # stateless Home, Live Logs, Safety pages and styling │ ├── installed_apps.py # app inventory validation and formatting @@ -1045,7 +1050,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 98 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 90-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 102 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 90-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 64a433d..7954ada 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -113,7 +113,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P1 — turn diagnostics into a coherent workbench -* Continue migrating subprocess workflows to typed lifecycle controllers and `OperationResult`. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, Installed Apps inventory/uninstall, and IPA inspection/install use the finite-operation controller. Backup uses a specialized controller that preserves private stdin requests and validates streamed progress events without imposing an arbitrary completion timeout. Evidence capture remains a specialized incremental migration because it must finalize partial evidence and coverage records. +* Maintain typed subprocess lifecycles and `OperationResult` across the workbench. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, Installed Apps inventory/uninstall, and IPA inspection/install use the finite-operation controller. Backup preserves private stdin requests and validated progress events without an arbitrary completion timeout. Evidence Capture reassembles validated JSON-line events and reserves a graceful finalization window for partial artifacts, coverage, manifests, and hashes. Long-running interactive streams retain their purpose-built lifecycle and explicit Stop controls. * Make a contextual readiness pane for the selected action, with one-click scoped rechecks and copyable remediation. * Maintain the opt-in physical-device compatibility protocol and its explicit USB, usbmux, CoreDevice, developer-service, privacy, and state-changing test boundaries. A pre-release dual-architecture frozen-artifact smoke workflow is now present. The release builder rejects any bundled Mach-O whose minimum macOS version is newer than the advertised 13.0 floor or lacks the native release architecture. * Generate concise changelog/release notes from tested behavior. Source, bundle, citation, packaging, and third-party-source metadata drift is now covered by automated tests. @@ -173,6 +173,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Migrated Installed Apps inventory and uninstall operations to the shared finite-operation controller. | The 95-test suite and 90-action GUI smoke passed; the smoke test now renders a synthetic inventory through the migrated asynchronous result path. | Backup, IPA inspection/install, and capture operations remain incremental controller migrations. | | 2026-09-22 | Migrated local IPA inspection and device installation to the shared finite-operation controller with explicit five- and 15-minute limits. | The 95-test suite and 90-action GUI smoke passed; the smoke test now validates typed inspection metadata, streamed installation output, and structured completion. | Backup and evidence capture retain specialized worker lifecycles pending deliberate migration. | | 2026-09-22 | Replaced the Backup workspace's blocking, hand-buffered process path with a password-safe typed controller. | 98 tests and the 90-action GUI smoke passed; real child-process tests cover stdin-only credentials, validated streamed events, malformed-protocol termination, and one-result cancellation. | Evidence capture still needs a lifecycle designed around partial-artifact finalization rather than a generic finite command. | +| 2026-09-22 | Added a typed Evidence Capture controller and close-safe graceful finalization. | 102 tests and the 90-action GUI smoke passed; real child-process tests cover fragmented JSON events, final-drain parsing, cancellation through `case-finished`, malformed-protocol finalization, and forced stop after the finalization deadline. | Physical-device collection remains opt-in; review each case manifest and hash inventory before relying on it. | ## Research sources diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index 5e8304d..e8db25a 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -96,6 +96,8 @@ process_error_connection_diagnostic, timed_out_connection_diagnostic, ) +from ios_developer_toolkit.collection_process import CollectionProcessController +from ios_developer_toolkit.collection_protocol import CollectionEvent from ios_developer_toolkit.device_compatibility import ( DeviceCompatibilityError, DeviceCompatibilityObservation, @@ -215,6 +217,7 @@ APPS_ACTION_TIMEOUT_MS = 10 * 60_000 IPA_INSPECTION_TIMEOUT_MS = 5 * 60_000 IPA_INSTALL_TIMEOUT_MS = 15 * 60_000 +COLLECTION_FINALIZATION_TIMEOUT_MS = 2 * 60_000 PROCESS_TERMINATE_GRACE_MS = 1_500 @@ -531,7 +534,13 @@ def __init__(self) -> None: self._compatibility_observations = load_observations(self._compatibility_history_path) except DeviceCompatibilityError as error: self._compatibility_history_error = str(error) - self._collection_process: QProcess | None = None + self._collection_controller = CollectionProcessController(self) + self._collection_controller.stdout_received.connect(self._append_collection_output) + self._collection_controller.stderr_received.connect(self._append_collection_output) + self._collection_controller.event_received.connect(self._collection_event_received) + self._collection_controller.completed.connect(self._collection_completed) + self._collection_case_finished = False + self._close_after_collection = False self._ipa_inspection_controller = FiniteProcessController(self) self._ipa_inspection_controller.completed.connect(self._ipa_inspection_completed) self._ipa_inspection: IPAInspection | None = None @@ -2518,7 +2527,7 @@ def _displayed_device(self) -> IOSDevice | None: def _update_device_fields(self, device: IOSDevice | None) -> None: identifier = device.identifier if device is not None else None if identifier != self._active_device_identifier: - if self._active_case_path is not None: + if self._active_case_path is not None and not self._collection_controller.is_running(): previous_case_path = self._active_case_path self._active_case_path = None self.case_status.setText( @@ -2541,8 +2550,10 @@ def _update_device_fields(self, device: IOSDevice | None) -> None: action_available = enabled and not self._action_controller.is_running() self.mount_button.setEnabled(action_available) self.remove_button.setEnabled(action_available) - self.start_collection_button.setEnabled(enabled and self._collection_process is None) - self.create_case_button.setEnabled(enabled and self._collection_process is None and self._active_case_path is None) + self.start_collection_button.setEnabled(enabled and not self._collection_controller.is_running()) + self.create_case_button.setEnabled( + enabled and not self._collection_controller.is_running() and self._active_case_path is None + ) self.case_readiness_button.setEnabled(enabled and self._capability_process is None) self._update_live_log_controls() self._update_apps_controls() @@ -3806,7 +3817,7 @@ def create_guided_case(self) -> None: if device is None: self._show_no_device() return - if self._collection_process is not None: + if self._collection_controller.is_running(): QMessageBox.warning(self, "Collection Running", "Wait for the active collection to finish before creating another case.") return if self._active_case_path is not None: @@ -3849,7 +3860,7 @@ def start_collection(self) -> None: if device is None: self._show_no_device() return - if self._collection_process is not None: + if self._collection_controller.is_running(): QMessageBox.warning(self, "Collection Running", "A collection is already running.") return selected_streams = self.include_syslog.isChecked() or self.include_oslog.isChecked() or self.include_pcap.isChecked() @@ -3860,7 +3871,12 @@ def start_collection(self) -> None: "The case will contain identifiers and potentially sensitive device data. " "PCAP does not decrypt TLS, but unencrypted payloads may be recorded." ) - if not self._confirm("Start Evidence Collection", warning): + if not self._confirm_action( + "Start Evidence Collection", + warning, + guided_action_safety("host-write"), + device.identifier, + ): return arguments = ["--udid", device.identifier] if self._active_case_path is None: @@ -3878,57 +3894,74 @@ def start_collection(self) -> None: if enabled: arguments.append(flag) worker = worker_command("collector") - process = QProcess(self) - process.setProgram(str(worker.program)) - process.setArguments(list(command_arguments(worker, arguments))) - process.setProcessEnvironment(qprocess_environment(base_environment())) - process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) - process.readyReadStandardOutput.connect(self._read_collection_output) - process.finished.connect(self._collection_finished) - process.errorOccurred.connect(self._collection_error) - self._collection_process = process self.collection_output.clear() + self._record_action_approval( + self.collection_output, + "Start Evidence Collection", + guided_action_safety("host-write"), + ) + self._collection_case_finished = False self.start_collection_button.setEnabled(False) self.stop_collection_button.setEnabled(True) - process.start() + self._collection_controller.start( + worker, + arguments, + base_environment(), + COLLECTION_FINALIZATION_TIMEOUT_MS, + ) - def _read_collection_output(self) -> None: - if self._collection_process is None: - return - text = bytes(self._collection_process.readAllStandardOutput()).decode("utf-8", errors="replace") + def _append_collection_output(self, output: bytes) -> None: self.collection_output.moveCursor(QTextCursor.MoveOperation.End) - self.collection_output.insertPlainText(text) - for line in text.splitlines(): - try: - record: object = json.loads(line) - except json.JSONDecodeError: - continue - if isinstance(record, dict) and record.get("event") == "case-created" and isinstance(record.get("path"), str): - self._last_case_path = Path(record["path"]) + self.collection_output.insertPlainText(output.decode("utf-8", errors="replace")) + + def _collection_event_received(self, event_object: object) -> None: + if not isinstance(event_object, CollectionEvent): + raise TypeError(f"Expected CollectionEvent, received {type(event_object).__name__}") + event = event_object + if event.event in ("case-created", "case-attached") and event.path is not None: + self._last_case_path = event.path + self.open_case_button.setEnabled(True) + if event.event == "case-finished": + self._collection_case_finished = True + if event.path is not None: + self._last_case_path = event.path self.open_case_button.setEnabled(True) - def _collection_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: - del exit_status - self.collection_output.appendPlainText(f"\nCollection process finished with exit code {exit_code}.") - self._collection_process = None + def _collection_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) + self.collection_output.appendPlainText( + f"\nCollection process finished: {result_object.outcome}; exit {exit_label}." + ) + if result_object.error_message: + self.collection_output.appendPlainText(f"Process error: {result_object.error_message}") if self._active_case_path is not None: - self.case_status.setText( - f"Guided case finalized at {self._active_case_path}. Create a new case before another collection." - ) - self._active_case_path = None + if self._collection_case_finished: + self.case_status.setText( + f"Guided case finalized at {self._active_case_path}. Create a new case before another collection." + ) + self._active_case_path = None + else: + self.case_status.setText( + f"Finalization was not confirmed for {self._active_case_path}. Review the directory; the guided case remains active for retry." + ) self.start_collection_button.setEnabled(self.selected_device() is not None) - self.create_case_button.setEnabled(self.selected_device() is not None) + self.create_case_button.setEnabled( + self.selected_device() is not None and self._active_case_path is None + ) self.stop_collection_button.setEnabled(False) - - def _collection_error(self, process_error: QProcess.ProcessError) -> None: - del process_error - if self._collection_process is not None: - self.collection_output.appendPlainText(f"\nProcess error: {self._collection_process.errorString()}") + if self._close_after_collection: + QTimer.singleShot(0, self.close) def stop_collection(self) -> None: - if self._collection_process is not None: + if self._collection_controller.is_running(): self.collection_output.appendPlainText("\nRequesting a clean stop and evidence finalization…") - self._collection_process.terminate() + self.stop_collection_button.setEnabled(False) + self.case_status.setText( + "Stop requested. Waiting for the collector to finalize its manifest and hashes." + ) + self._collection_controller.cancel() def open_last_case(self) -> None: if self._last_case_path is None or not self._last_case_path.is_dir(): @@ -5547,23 +5580,25 @@ def closeEvent(self, event: QCloseEvent) -> None: ipa_inspection_running = self._ipa_inspection_controller.is_running() sideload_running = self._sideload_controller.is_running() backup_running = self._backup_controller.is_running() + collection_running = self._collection_controller.is_running() critical_processes = tuple( process for process in ( - self._collection_process, self._console_process, self._location_process, ) if process is not None and process.state() != QProcess.ProcessState.NotRunning ) - if ( + active_operations = ( action_running or apps_running or ipa_inspection_running or sideload_running or backup_running + or collection_running or critical_processes - ): + ) + if active_operations and not self._close_after_collection: should_close = self._confirm( "Stop Active Operations?", "A DDI, evidence, app, backup, Location Lab, or Command Center operation is still running. " @@ -5572,6 +5607,15 @@ def closeEvent(self, event: QCloseEvent) -> None: if not should_close: event.ignore() return + if collection_running: + if not self._close_after_collection: + self._close_after_collection = True + self.case_status.setText( + "Closing is waiting for evidence finalization. The collector has up to two minutes to write its manifest and hashes." + ) + self.stop_collection() + event.ignore() + return self._scanner.stop() self._reconnect_timeout_timer.stop() self._manpage_controller.shutdown(3000, 1000) @@ -5581,6 +5625,7 @@ def closeEvent(self, event: QCloseEvent) -> None: self._ipa_inspection_controller.shutdown(10000, 3000) self._sideload_controller.shutdown(10000, 3000) self._backup_controller.shutdown(10000, 3000) + self._collection_controller.shutdown(10000, 3000) capability_process = self._capability_process if capability_process is not None and capability_process.state() != QProcess.ProcessState.NotRunning: self._terminate_capability_children(capability_process) diff --git a/ios_developer_toolkit/backup_process.py b/ios_developer_toolkit/backup_process.py index b2c9218..650c51f 100644 --- a/ios_developer_toolkit/backup_process.py +++ b/ios_developer_toolkit/backup_process.py @@ -90,7 +90,8 @@ def start( process.start() def cancel(self) -> None: - if not self.is_running(): + process = self._process + if process is None or process.state() == QProcess.ProcessState.NotRunning: return self._stop_outcome = "cancelled" self._terminate() diff --git a/ios_developer_toolkit/collection_process.py b/ios_developer_toolkit/collection_process.py new file mode 100644 index 0000000..4842f12 --- /dev/null +++ b/ios_developer_toolkit/collection_process.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone +from typing import Literal, Mapping, Sequence + +from PySide6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, Signal + +from ios_developer_toolkit.collection_protocol import ( + CollectionEvent, + CollectionProtocolError, + parse_collection_event, +) +from ios_developer_toolkit.qt_process import OperationResult, ProcessOutcome +from ios_developer_toolkit.runtime import ExecutableCommand, command_arguments, command_argv + + +class CollectionProcessController(QObject): + """Own one evidence collector and preserve its graceful finalization window.""" + + stdout_received = Signal(bytes) + stderr_received = Signal(bytes) + event_received = Signal(object) + completed = Signal(object) + + def __init__(self, parent: QObject) -> None: + super().__init__(parent) + self._process: QProcess | None = None + self._command: ExecutableCommand | None = None + self._arguments: tuple[str, ...] = () + self._stdout = bytearray() + self._stdout_line = bytearray() + self._stderr = bytearray() + self._started_at = "" + self._error_message: str | None = None + self._stop_outcome: Literal["cancelled", "timed-out"] | None = None + self._protocol_failed = False + self._completed = False + self._finalization_timeout_milliseconds = 0 + self._finalization_timer = QTimer(self) + self._finalization_timer.setSingleShot(True) + self._finalization_timer.timeout.connect(self._force_stop_after_finalization_timeout) + + def is_running(self) -> bool: + return self._process is not None + + def start( + self, + command: ExecutableCommand, + arguments: Sequence[str], + environment: Mapping[str, str], + finalization_timeout_milliseconds: int, + ) -> None: + if self.is_running(): + raise RuntimeError("Cannot start an evidence collection while another collection is running") + if finalization_timeout_milliseconds <= 0: + raise ValueError( + f"Collection finalization timeout must be positive: {finalization_timeout_milliseconds}" + ) + self._command = command + self._arguments = tuple(arguments) + self._stdout.clear() + self._stdout_line.clear() + self._stderr.clear() + self._started_at = datetime.now(timezone.utc).isoformat() + self._error_message = None + self._stop_outcome = None + self._protocol_failed = False + self._completed = False + self._finalization_timeout_milliseconds = finalization_timeout_milliseconds + + process = QProcess(self) + process.setProgram(str(command.program)) + process.setArguments(list(command_arguments(command, self._arguments))) + process_environment = QProcessEnvironment.systemEnvironment() + for key, value in sorted(environment.items()): + process_environment.insert(key, value) + process.setProcessEnvironment(process_environment) + process.readyReadStandardOutput.connect(self._drain_output) + process.readyReadStandardError.connect(self._drain_output) + process.errorOccurred.connect(self._process_error) + process.finished.connect(self._finished) + self._process = process + process.start() + + def cancel(self) -> None: + process = self._process + if process is None or process.state() == QProcess.ProcessState.NotRunning: + return + if self._stop_outcome == "cancelled": + return + self._stop_outcome = "cancelled" + self._request_graceful_stop() + + def shutdown(self, terminate_timeout_milliseconds: int, kill_timeout_milliseconds: int) -> None: + if terminate_timeout_milliseconds <= 0: + raise ValueError(f"Shutdown termination timeout must be positive: {terminate_timeout_milliseconds}") + if kill_timeout_milliseconds <= 0: + raise ValueError(f"Shutdown kill timeout must be positive: {kill_timeout_milliseconds}") + process = self._process + if process is None: + return + self._stop_outcome = "cancelled" + self._finalization_timer.stop() + if process.state() != QProcess.ProcessState.NotRunning: + process.terminate() + if not process.waitForFinished(terminate_timeout_milliseconds): + process.kill() + if not process.waitForFinished(kill_timeout_milliseconds): + raise RuntimeError(f"Collector did not stop after terminate and kill: {process.program()}") + else: + self._finish_once("cancelled", process.exitCode()) + + def _drain_output(self) -> None: + process = self._process + if process is None: + return + stdout = bytes(process.readAllStandardOutput()) + stderr = bytes(process.readAllStandardError()) + if stdout: + self._stdout.extend(stdout) + self._stdout_line.extend(stdout) + self.stdout_received.emit(stdout) + self._consume_complete_lines() + if stderr: + self._stderr.extend(stderr) + self.stderr_received.emit(stderr) + + def _consume_complete_lines(self) -> None: + while b"\n" in self._stdout_line: + line, _, remainder = self._stdout_line.partition(b"\n") + self._stdout_line = bytearray(remainder) + if line.strip(): + self._consume_event_line(line) + + def _consume_event_line(self, line: bytes) -> None: + try: + event = parse_collection_event(line.decode("utf-8")) + except (CollectionProtocolError, json.JSONDecodeError, UnicodeDecodeError) as error: + self._protocol_failure(f"Invalid collector event: {error}") + return + self.event_received.emit(event) + + def _protocol_failure(self, message: str) -> None: + if self._protocol_failed: + return + self._protocol_failed = True + self._error_message = message + process = self._process + if process is not None and process.state() != QProcess.ProcessState.NotRunning: + self._request_graceful_stop() + + def _request_graceful_stop(self) -> None: + process = self._process + if process is None: + raise RuntimeError("Cannot stop evidence collection without an active process") + if process.state() == QProcess.ProcessState.NotRunning: + return + process.terminate() + self._finalization_timer.start(self._finalization_timeout_milliseconds) + + def _force_stop_after_finalization_timeout(self) -> None: + process = self._process + if process is None or process.state() == QProcess.ProcessState.NotRunning: + return + if not self._protocol_failed: + self._stop_outcome = "timed-out" + self._error_message = "Collector did not finish evidence finalization before the safety deadline" + process.kill() + + def _process_error(self, process_error: QProcess.ProcessError) -> None: + process = self._process + if process is None: + raise RuntimeError("Collector reported an error without an active process") + if self._error_message is None: + self._error_message = process.errorString() + if process_error == QProcess.ProcessError.FailedToStart: + self._finish_once("launch-failed", None) + + def _finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: + self._drain_output() + if self._stdout_line.strip(): + line = bytes(self._stdout_line) + self._stdout_line.clear() + self._consume_event_line(line) + if self._protocol_failed: + outcome: ProcessOutcome = "failed" + elif self._stop_outcome is not None: + outcome = self._stop_outcome + elif exit_status == QProcess.ExitStatus.CrashExit: + outcome = "crashed" + elif exit_code == 0: + outcome = "succeeded" + else: + outcome = "failed" + self._finish_once(outcome, exit_code) + + def _finish_once(self, outcome: ProcessOutcome, exit_code: int | None) -> None: + if self._completed: + return + command = self._command + if command is None: + raise RuntimeError("Collector completed without a command") + self._drain_output() + self._completed = True + self._finalization_timer.stop() + result = OperationResult( + command_argv(command, self._arguments), + outcome, + self._started_at, + datetime.now(timezone.utc).isoformat(), + exit_code, + self._error_message, + bytes(self._stdout), + bytes(self._stderr), + ) + process = self._process + self._process = None + if process is not None: + process.deleteLater() + self.completed.emit(result) diff --git a/ios_developer_toolkit/collection_protocol.py b/ios_developer_toolkit/collection_protocol.py new file mode 100644 index 0000000..6bb89e7 --- /dev/null +++ b/ios_developer_toolkit/collection_protocol.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Mapping + + +class CollectionProtocolError(ValueError): + """Raised when a collector event does not match the documented JSON-line schema.""" + + +@dataclass(frozen=True) +class CollectionEvent: + event: str + message: str + timestamp: str + path: Path | None + status: str | None + failures: int | None + + +def required_string(record: Mapping[str, object], field_name: str) -> str: + value = record.get(field_name) + if not isinstance(value, str) or not value.strip(): + raise CollectionProtocolError(f"{field_name} must be a non-empty string") + return value.strip() + + +def optional_string(record: Mapping[str, object], field_name: str) -> str | None: + value = record.get(field_name) + if value is None: + return None + if not isinstance(value, str) or not value.strip(): + raise CollectionProtocolError(f"{field_name} must be a non-empty string when present") + return value.strip() + + +def optional_nonnegative_integer(record: Mapping[str, object], field_name: str) -> int | None: + value = record.get(field_name) + if value is None: + return None + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise CollectionProtocolError(f"{field_name} must be a non-negative integer when present") + return value + + +def parse_collection_event(payload: str) -> CollectionEvent: + raw: object = json.loads(payload) + if not isinstance(raw, dict) or not all(isinstance(key, str) for key in raw): + raise CollectionProtocolError("collector event must be a string-keyed JSON object") + record: Mapping[str, object] = raw + path_text = optional_string(record, "path") + path = Path(path_text) if path_text is not None else None + if path is not None and not path.is_absolute(): + raise CollectionProtocolError("collector event path must be absolute") + return CollectionEvent( + event=required_string(record, "event"), + message=required_string(record, "message"), + timestamp=required_string(record, "timestamp"), + path=path, + status=optional_string(record, "status"), + failures=optional_nonnegative_integer(record, "failures"), + ) diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index da5afe3..5d39b3f 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -326,6 +326,30 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError(f"GUI backup controller did not apply its typed event: {window.backup_output.toPlainText()}") if "Encryption status check completed." not in window.backup_output.toPlainText(): raise RuntimeError(f"GUI backup controller reported the wrong completion: {window.backup_output.toPlainText()}") + synthetic_collection_event = ( + '{"event":"case-finished","message":"Synthetic evidence finalization.",' + '"timestamp":"2026-09-22T00:00:00+00:00","path":"/tmp/toolkit-smoke-case",' + '"status":"completed","failures":0}' + ) + window._collection_case_finished = False + window._collection_controller.start( + ExecutableCommand(Path("/usr/bin/printf"), ()), + (synthetic_collection_event,), + {}, + 5_000, + ) + collection_deadline = time.monotonic() + 10 + while window._collection_controller.is_running() and time.monotonic() < collection_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._collection_controller.is_running(): + window._collection_controller.cancel() + raise RuntimeError("GUI evidence controller did not complete within its bounded smoke-test window") + if not window._collection_case_finished or window._last_case_path != Path("/tmp/toolkit-smoke-case"): + raise RuntimeError(f"GUI evidence controller did not apply finalization: {window.collection_output.toPlainText()}") + if "Collection process finished: succeeded; exit 0." not in window.collection_output.toPlainText(): + raise RuntimeError(f"GUI evidence controller reported the wrong completion: {window.collection_output.toPlainText()}") window.close() application.processEvents() print(f"GUI smoke test passed with {len(buttons)} action buttons", flush=True) diff --git a/tests/test_collection_process.py b/tests/test_collection_process.py new file mode 100644 index 0000000..c860328 --- /dev/null +++ b/tests/test_collection_process.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import sys +import time +import unittest +from collections.abc import Callable +from pathlib import Path + +from PySide6.QtCore import QCoreApplication + +from ios_developer_toolkit.collection_process import CollectionProcessController +from ios_developer_toolkit.collection_protocol import CollectionEvent +from ios_developer_toolkit.qt_process import OperationResult +from ios_developer_toolkit.runtime import ExecutableCommand + + +class CollectionProcessControllerTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.application = QCoreApplication.instance() or QCoreApplication(["collection-process-tests"]) + + def test_reassembles_fragmented_json_event_and_drains_terminal_output(self) -> None: + controller = CollectionProcessController(self.application) + events: list[CollectionEvent] = [] + results: list[OperationResult] = [] + controller.event_received.connect(events.append) + controller.completed.connect(results.append) + first = '{"event":"case-created","message":"created",' + second = '"timestamp":"2026-09-22T00:00:00+00:00","path":"/tmp/toolkit-case"}' + script = ( + f"import sys,time; sys.stdout.write({first!r}); sys.stdout.flush(); time.sleep(0.05); " + f"sys.stdout.write({second!r})" + ) + + controller.start( + ExecutableCommand(Path(sys.executable), ("-c", script)), + (), + {}, + 3_000, + ) + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(results[0].outcome, "succeeded") + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event, "case-created") + self.assertEqual(events[0].path, Path("/tmp/toolkit-case")) + self.assertEqual(results[0].stdout, (first + second).encode("utf-8")) + + def test_cancel_waits_for_case_finalization_event(self) -> None: + controller = CollectionProcessController(self.application) + events: list[CollectionEvent] = [] + results: list[OperationResult] = [] + controller.event_received.connect(events.append) + controller.completed.connect(results.append) + script = """ +import json +import signal +import time + +stop = False + +def request_stop(signum, frame): + global stop + del signum, frame + stop = True + +signal.signal(signal.SIGTERM, request_stop) +print(json.dumps({"event":"case-created","message":"created","timestamp":"2026-09-22T00:00:00+00:00","path":"/tmp/toolkit-case"}), flush=True) +while not stop: + time.sleep(0.01) +print(json.dumps({"event":"case-finished","message":"finalized","timestamp":"2026-09-22T00:00:01+00:00","path":"/tmp/toolkit-case","status":"cancelled","failures":0}), flush=True) +""" + + controller.start( + ExecutableCommand(Path(sys.executable), ("-c", script)), + (), + {}, + 3_000, + ) + self._wait_for(lambda: len(events) == 1, 3) + controller.cancel() + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(results[0].outcome, "cancelled") + self.assertEqual([event.event for event in events], ["case-created", "case-finished"]) + self.assertEqual(events[-1].status, "cancelled") + self.assertFalse(controller.is_running()) + + def test_protocol_failure_requests_finalization_and_preserves_root_cause(self) -> None: + controller = CollectionProcessController(self.application) + events: list[CollectionEvent] = [] + results: list[OperationResult] = [] + controller.event_received.connect(events.append) + controller.completed.connect(results.append) + script = """ +import json +import signal +import time + +stop = False + +def request_stop(signum, frame): + global stop + del signum, frame + stop = True + +signal.signal(signal.SIGTERM, request_stop) +print("not-json", flush=True) +while not stop: + time.sleep(0.01) +print(json.dumps({"event":"case-finished","message":"finalized","timestamp":"2026-09-22T00:00:01+00:00","path":"/tmp/toolkit-case","status":"cancelled","failures":1}), flush=True) +""" + + controller.start( + ExecutableCommand(Path(sys.executable), ("-c", script)), + (), + {}, + 3_000, + ) + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(results[0].outcome, "failed") + self.assertIn("Invalid collector event", results[0].error_message or "") + self.assertEqual([event.event for event in events], ["case-finished"]) + self.assertFalse(controller.is_running()) + + def test_forces_stop_when_finalization_deadline_expires(self) -> None: + controller = CollectionProcessController(self.application) + events: list[CollectionEvent] = [] + results: list[OperationResult] = [] + controller.event_received.connect(events.append) + controller.completed.connect(results.append) + script = """ +import json +import signal +import time + +signal.signal(signal.SIGTERM, signal.SIG_IGN) +print(json.dumps({"event":"case-created","message":"created","timestamp":"2026-09-22T00:00:00+00:00","path":"/tmp/toolkit-case"}), flush=True) +time.sleep(10) +""" + + controller.start( + ExecutableCommand(Path(sys.executable), ("-c", script)), + (), + {}, + 100, + ) + self._wait_for(lambda: len(events) == 1, 3) + controller.cancel() + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(results[0].outcome, "timed-out") + self.assertIn("finalization", results[0].error_message or "") + self.assertFalse(controller.is_running()) + + def _wait_for(self, predicate: Callable[[], bool], timeout_seconds: int) -> None: + deadline = time.monotonic() + timeout_seconds + while not predicate() and time.monotonic() < deadline: + self.application.processEvents() + time.sleep(0.01) + self.application.processEvents() + + +if __name__ == "__main__": + unittest.main() From f7e3a78f7402d567fe846c5d9a637a21fdea0506 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 03:28:35 -0700 Subject: [PATCH 07/16] Add native Xcode handoffs --- README.md | 22 ++++- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- docs/screenshots/xcode-handoff.png | Bin 0 -> 148013 bytes ios_developer_toolkit/app.py | 119 +++++++++++++++++++++++-- ios_developer_toolkit/entrypoint.py | 17 +++- ios_developer_toolkit/xcode_handoff.py | 73 +++++++++++++++ tests/test_xcode_handoff.py | 83 +++++++++++++++++ 7 files changed, 305 insertions(+), 12 deletions(-) create mode 100644 docs/screenshots/xcode-handoff.png create mode 100644 ios_developer_toolkit/xcode_handoff.py create mode 100644 tests/test_xcode_handoff.py diff --git a/README.md b/README.md index bba677c..29b0c1d 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,10 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one selected-device workflow; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 102 tests, embedded CLI checks, a 90-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 106 tests, embedded CLI checks, a 94-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. -The README contains 17 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. +The README contains 18 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. | Prepare the device and DDI | Observe live services | Run guided commands | |---|---|---| @@ -123,7 +123,7 @@ The README contains 17 sanitized screenshots. The six views below provide a quic | Workspace | Primary purpose | DDI needed? | Important result | |---|---|---:|---| | **Home** | Understand the workflow and jump to a task | No | Service-layer overview and guided entry points | -| **Device & DDI** | Check Developer Mode; mount, list, or remove a developer image | For mounting | Explicit device target and image source | +| **Device & DDI** | Check Developer Mode; manage a developer image; hand off to CoreDevice, RVI, Xcode, or Instruments | For mounting and some CoreDevice details | Explicit device target, image source, and native-tool output | | **Capability Matrix** | Test host, connection, trust, DDI, tunnel, and developer-service readiness | Only for the developer-service rows | Bounded per-capability state, evidence, remediation, and real-device comparison | | **Location Lab** | Set a coordinate or replay a validated GPX track | Usually | Structured location-event evidence and explicit Clear | | **Live Logs** | Open independent Unified Logs, classic syslog, and DVT OSLog windows | Only DVT OSLog | Complete raw spool plus filtered working view | @@ -441,6 +441,19 @@ Both modern paths normally require Apple TSS access. A cached DDI payload does n Developer Mode queries and DDI mount, list, unmount, install, and uninstall actions use the shared bounded operation controller. It drains both output channels at completion, reports launch failures and crashes distinctly, prevents periodic device refreshes from re-enabling conflicting controls, and stops an action that exceeds the 15-minute safety limit. +#### Apple developer-tool handoff + +![Apple developer-tool handoff controls](docs/screenshots/xcode-handoff.png) + +The final group in Device & DDI deliberately hands native work back to Apple tools: + +- **CoreDevice Details** runs `xcrun devicectl device info details --device --timeout 30` and displays Apple's human-readable output without treating it as a stable machine schema; +- **List RVI Interfaces** runs the installed `rvictl -l` so an existing Remote Virtual Interface can be cross-checked before packet capture; +- **Open Xcode Project…** validates an `.xcodeproj`, `.xcworkspace`, or `Package.swift` and hands it to Apple's `xed` launcher; +- **Open Result / Trace…** validates and opens an `.xcresult` or `.trace` bundle in Xcode or Instruments. + +These are read-only host handoffs. They do not create a project, run tests, start a trace, create or remove an RVI, or parse proprietary Xcode result formats. CoreDevice, RVI, and `xed` handoffs have a 60-second GUI safety limit and retain the exact command and terminal result in the shared output panel. + ### Device Capability Matrix ![Device Capability Matrix workspace](docs/screenshots/capability-matrix.png) @@ -1021,6 +1034,7 @@ Install or update Xcode if the candidate is absent. The toolkit requires the exp │ ├── qt_process.py # typed, bounded finite-process lifecycle controller │ ├── runtime.py # source/frozen commands and device environment │ ├── ufade_connector.py # isolated external UFADE validation and launch +│ ├── xcode_handoff.py # validated CoreDevice, RVI, project, and artifact handoffs │ └── assets/ ├── .github/workflows/ # native Intel and Apple Silicon release builds ├── docs/screenshots/ # sanitized current-interface captures @@ -1050,7 +1064,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 102 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 90-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 106 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 94-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 7954ada..973b5dc 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -117,7 +117,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack * Make a contextual readiness pane for the selected action, with one-click scoped rechecks and copyable remediation. * Maintain the opt-in physical-device compatibility protocol and its explicit USB, usbmux, CoreDevice, developer-service, privacy, and state-changing test boundaries. A pre-release dual-architecture frozen-artifact smoke workflow is now present. The release builder rejects any bundled Mach-O whose minimum macOS version is newer than the advertised 13.0 floor or lacks the native release architecture. * Generate concise changelog/release notes from tested behavior. Source, bundle, citation, packaging, and third-party-source metadata drift is now covered by automated tests. -* Add Xcode project/device handoffs: selected `devicectl` discovery, RVI status, and `.xcresult`/`xctrace` opening without reimplementing those formats. +* Maintain bounded Xcode project/device handoffs: selected-device `devicectl` details, RVI status, and native `.xcresult`/Instruments trace opening without parsing or reimplementing Apple's formats. ### P2 — deepen expert workflows without scope creep @@ -174,6 +174,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Migrated local IPA inspection and device installation to the shared finite-operation controller with explicit five- and 15-minute limits. | The 95-test suite and 90-action GUI smoke passed; the smoke test now validates typed inspection metadata, streamed installation output, and structured completion. | Backup and evidence capture retain specialized worker lifecycles pending deliberate migration. | | 2026-09-22 | Replaced the Backup workspace's blocking, hand-buffered process path with a password-safe typed controller. | 98 tests and the 90-action GUI smoke passed; real child-process tests cover stdin-only credentials, validated streamed events, malformed-protocol termination, and one-result cancellation. | Evidence capture still needs a lifecycle designed around partial-artifact finalization rather than a generic finite command. | | 2026-09-22 | Added a typed Evidence Capture controller and close-safe graceful finalization. | 102 tests and the 90-action GUI smoke passed; real child-process tests cover fragmented JSON events, final-drain parsing, cancellation through `case-finished`, malformed-protocol finalization, and forced stop after the finalization deadline. | Physical-device collection remains opt-in; review each case manifest and hash inventory before relying on it. | +| 2026-09-22 | Added native Apple developer-tool handoffs for selected-device CoreDevice details, RVI status, Xcode projects, test results, and Instruments traces. | 106 tests and a 94-action GUI smoke passed; tests validate the exact selected-device and RVI commands and reject missing or unrelated local targets. | The toolkit displays native output and opens native formats; it does not claim a stable schema for human `devicectl` output or reimplement Xcode. | ## Research sources diff --git a/docs/screenshots/xcode-handoff.png b/docs/screenshots/xcode-handoff.png new file mode 100644 index 0000000000000000000000000000000000000000..2942e8679fdc0a8d27afc2e0980b6f796eb56814 GIT binary patch literal 148013 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

VgZ5f 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 E4bI@*=>Px# literal 0 HcmV?d00001 diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index e8db25a..caa4367 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -204,6 +204,13 @@ macos_setup_commands, ) from ios_developer_toolkit.validation import output_indicates_failure +from ios_developer_toolkit.xcode_handoff import ( + XcodeHandoffError, + coredevice_details_handoff, + rvi_list_handoff, + validated_xcode_artifact, + xcode_project_handoff, +) XCODE_CANDIDATE_DDI = Path("/Library/Developer/CoreDevice/CandidateDDIs/iOS_DDI.dmg") @@ -218,6 +225,7 @@ IPA_INSPECTION_TIMEOUT_MS = 5 * 60_000 IPA_INSTALL_TIMEOUT_MS = 15 * 60_000 COLLECTION_FINALIZATION_TIMEOUT_MS = 2 * 60_000 +XCODE_HANDOFF_TIMEOUT_MS = 60_000 PROCESS_TERMINATE_GRACE_MS = 1_500 @@ -641,7 +649,7 @@ def _build_ui(self) -> None: title = QLabel("iOS Developer Toolkit") title.setObjectName("appTitle") title.setFont(QFont(title.font().family(), 24, QFont.Weight.Bold)) - subtitle = QLabel("pymobiledevice3 Swiss-army GUI • Developer images • diagnostics • evidence") + subtitle = QLabel("iOS developer workbench • pymobiledevice3 • diagnostics • evidence") subtitle.setObjectName("appSubtitle") title_block.addWidget(title) title_block.addWidget(subtitle) @@ -1139,11 +1147,41 @@ def _build_overview_tab(self) -> QWidget: ddi_layout.addLayout(button_layout) layout.addWidget(ddi_group) + xcode_group = QGroupBox("3. Apple developer-tool handoff") + xcode_layout = QVBoxLayout(xcode_group) + xcode_explanation = QLabel( + "Use Apple's installed tools for CoreDevice visibility, RVI status, projects, test results, and " + "Instruments traces. The toolkit shows exact command output but does not reinterpret proprietary " + "Xcode formats." + ) + xcode_explanation.setWordWrap(True) + xcode_layout.addWidget(xcode_explanation) + xcode_buttons = QHBoxLayout() + self.coredevice_details_button = QPushButton("CoreDevice Details") + self.coredevice_details_button.setObjectName("coreDeviceDetailsButton") + self.coredevice_details_button.clicked.connect(self.show_coredevice_details) + xcode_buttons.addWidget(self.coredevice_details_button) + self.rvi_status_button = QPushButton("List RVI Interfaces") + self.rvi_status_button.setObjectName("listRVIInterfacesButton") + self.rvi_status_button.clicked.connect(self.list_rvi_interfaces) + xcode_buttons.addWidget(self.rvi_status_button) + self.open_xcode_project_button = QPushButton("Open Xcode Project…") + self.open_xcode_project_button.setObjectName("openXcodeProjectButton") + self.open_xcode_project_button.clicked.connect(self.open_xcode_project) + xcode_buttons.addWidget(self.open_xcode_project_button) + open_artifact_button = QPushButton("Open Result / Trace…") + open_artifact_button.setObjectName("openXcodeArtifactButton") + open_artifact_button.clicked.connect(self.open_xcode_artifact) + xcode_buttons.addWidget(open_artifact_button) + xcode_buttons.addStretch() + xcode_layout.addLayout(xcode_buttons) + layout.addWidget(xcode_group) + self.action_output = QPlainTextEdit() self.action_output.setObjectName("ddiActionOutput") self.action_output.setReadOnly(True) self.action_output.setMaximumBlockCount(3000) - self.action_output.setPlaceholderText("DDI and Developer Mode command output appears here.") + self.action_output.setPlaceholderText("DDI, Developer Mode, CoreDevice, and RVI command output appears here.") layout.addWidget(self.action_output, 1) self._ddi_source_changed() return tab @@ -2550,6 +2588,11 @@ def _update_device_fields(self, device: IOSDevice | None) -> None: action_available = enabled and not self._action_controller.is_running() self.mount_button.setEnabled(action_available) self.remove_button.setEnabled(action_available) + self.coredevice_details_button.setEnabled(action_available) + self.rvi_status_button.setEnabled(not self._demo_mode and not self._action_controller.is_running()) + self.open_xcode_project_button.setEnabled( + not self._demo_mode and not self._action_controller.is_running() + ) self.start_collection_button.setEnabled(enabled and not self._collection_controller.is_running()) self.create_case_button.setEnabled( enabled and not self._collection_controller.is_running() and self._active_case_path is None @@ -3047,6 +3090,7 @@ def mount_selected_ddi(self) -> None: ("--candidate", str(XCODE_CANDIDATE_DDI), "--udid", device.identifier), base_environment(), "mount-local-cryptex", + DDI_ACTION_TIMEOUT_MS, ) def remove_selected_ddi(self) -> None: @@ -3078,12 +3122,71 @@ def list_mounted_images(self) -> None: arguments = ("mounter", "list") if self.personalized_radio.isChecked() else ("cryptex", "list") self._run_pmd3_action(arguments, "list-images") + def show_coredevice_details(self) -> None: + device = self.selected_device() + if device is None: + self._show_no_device() + return + try: + command, arguments = coredevice_details_handoff(device.identifier) + except XcodeHandoffError as error: + QMessageBox.critical(self, "CoreDevice Tool Unavailable", str(error)) + return + self._start_action(command, arguments, base_environment(), "coredevice-details", XCODE_HANDOFF_TIMEOUT_MS) + + def list_rvi_interfaces(self) -> None: + try: + command, arguments = rvi_list_handoff() + except XcodeHandoffError as error: + QMessageBox.critical(self, "RVI Tool Unavailable", str(error)) + return + self._start_action(command, arguments, base_environment(), "rvi-status", XCODE_HANDOFF_TIMEOUT_MS) + + def open_xcode_project(self) -> None: + selected, _ = QFileDialog.getOpenFileName( + self, + "Open Xcode project, workspace, or Swift package", + str(Path.home()), + "Xcode projects (*.xcodeproj *.xcworkspace);;Swift package (Package.swift)", + ) + if not selected: + return + try: + command, arguments = xcode_project_handoff(Path(selected)) + except XcodeHandoffError as error: + QMessageBox.critical(self, "Invalid Xcode Project", str(error)) + return + self._start_action(command, arguments, base_environment(), "open-xcode-project", XCODE_HANDOFF_TIMEOUT_MS) + + def open_xcode_artifact(self) -> None: + selected, _ = QFileDialog.getOpenFileName( + self, + "Open Xcode result or Instruments trace", + str(Path.home()), + "Xcode and Instruments artifacts (*.xcresult *.trace)", + ) + if not selected: + return + try: + target = validated_xcode_artifact(Path(selected)) + except XcodeHandoffError as error: + QMessageBox.critical(self, "Invalid Xcode Artifact", str(error)) + return + if not QDesktopServices.openUrl(QUrl.fromLocalFile(str(target))): + QMessageBox.critical(self, "Could Not Open Artifact", f"macOS could not open the selected target:\n{target}") + def _run_pmd3_action(self, arguments: tuple[str, ...], context: str) -> None: device = self.selected_device() if device is None: self._show_no_device() return - self._start_action(self._pmd3, arguments, device_environment(device.identifier), context) + self._start_action( + self._pmd3, + arguments, + device_environment(device.identifier), + context, + DDI_ACTION_TIMEOUT_MS, + ) def _start_action( self, @@ -3091,20 +3194,24 @@ def _start_action( arguments: tuple[str, ...], environment: Mapping[str, str], context: str, + timeout_milliseconds: int, ) -> None: if self._action_controller.is_running(): - QMessageBox.warning(self, "Action Running", "Wait for the current DDI action to finish.") + QMessageBox.warning(self, "Action Running", "Wait for the current Device & DDI action to finish.") return self.action_output.appendPlainText(f"$ {command_text(program, arguments)}") self._action_context = context self.mount_button.setEnabled(False) self.remove_button.setEnabled(False) + self.coredevice_details_button.setEnabled(False) + self.rvi_status_button.setEnabled(False) + self.open_xcode_project_button.setEnabled(False) self._action_controller.start( finite_process_request( program, arguments, environment, - DDI_ACTION_TIMEOUT_MS, + timeout_milliseconds, PROCESS_TERMINATE_GRACE_MS, ) ) @@ -3128,7 +3235,7 @@ def _action_completed(self, result_object: object) -> None: self.action_output.appendPlainText(f"Process error: {result_object.error_message}") if result_object.outcome == "timed-out": self.action_output.appendPlainText( - "The DDI action exceeded the 15-minute safety limit and was stopped." + "The action exceeded its safety limit and was stopped." ) if context == "developer-mode-status": if succeeded and b"true" in combined_output.lower(): diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 5d39b3f..9f1a13a 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -99,6 +99,21 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError(f"GUI buttons are missing stable identifiers: {missing_identifiers}") if disconnected: raise RuntimeError(f"GUI buttons are missing click handlers: {disconnected}") + for button_name in ( + "coreDeviceDetailsButton", + "listRVIInterfacesButton", + "openXcodeProjectButton", + "openXcodeArtifactButton", + ): + button = window.findChild(QPushButton, button_name) + if button is None: + raise RuntimeError(f"GUI Xcode handoff action is missing: {button_name}") + coredevice_button = window.findChild(QPushButton, "coreDeviceDetailsButton") + rvi_button = window.findChild(QPushButton, "listRVIInterfacesButton") + if coredevice_button is None or coredevice_button.isEnabled(): + raise RuntimeError("GUI CoreDevice handoff must require a selected device") + if rvi_button is None or not rvi_button.isEnabled(): + raise RuntimeError("GUI RVI status handoff should be available without a selected device") navigation_actions = { "homeOpenDevice&DDIButton": "Device & DDI", "homeOpenCapabilityMatrixButton": "Capability Matrix", @@ -206,7 +221,7 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError("Demo mode must disable live-device log collection") demo_mode_button.click() application.processEvents() - window._start_action(pymobiledevice3_command(), ("version",), {}, "smoke") + window._start_action(pymobiledevice3_command(), ("version",), {}, "smoke", 20_000) action_deadline = time.monotonic() + 20 while window._action_controller.is_running() and time.monotonic() < action_deadline: application.processEvents() diff --git a/ios_developer_toolkit/xcode_handoff.py b/ios_developer_toolkit/xcode_handoff.py new file mode 100644 index 0000000..ea21b20 --- /dev/null +++ b/ios_developer_toolkit/xcode_handoff.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import os +import shutil +from pathlib import Path + +from ios_developer_toolkit.runtime import ExecutableCommand + + +class XcodeHandoffError(ValueError): + """Raised when an Apple developer-tool handoff cannot be built safely.""" + + +def executable_command(name: str, fixed_candidates: tuple[Path, ...]) -> ExecutableCommand: + if not name or Path(name).name != name: + raise XcodeHandoffError(f"Developer tool name must be a basename: {name!r}") + candidates = (*fixed_candidates, *(Path(path) for path in (shutil.which(name),) if path is not None)) + executable = next((path for path in candidates if path.is_file() and os.access(path, os.X_OK)), None) + if executable is None: + checked = ", ".join(str(path) for path in candidates) or "no candidate paths" + raise XcodeHandoffError(f"Could not find executable developer tool {name}; checked {checked}") + return ExecutableCommand(executable.resolve(), ()) + + +def coredevice_details_handoff(udid: str) -> tuple[ExecutableCommand, tuple[str, ...]]: + normalized_udid = udid.strip() + if not normalized_udid: + raise XcodeHandoffError("CoreDevice details require a selected device identifier") + command = executable_command("xcrun", (Path("/usr/bin/xcrun"),)) + return command, ( + "devicectl", + "device", + "info", + "details", + "--device", + normalized_udid, + "--timeout", + "30", + ) + + +def rvi_list_handoff() -> tuple[ExecutableCommand, tuple[str, ...]]: + command = executable_command( + "rvictl", + (Path("/Library/Apple/usr/bin/rvictl"), Path("/usr/bin/rvictl")), + ) + return command, ("-l",) + + +def validated_xcode_target(path: Path, allowed_suffixes: tuple[str, ...], allowed_names: tuple[str, ...]) -> Path: + resolved = path.expanduser().resolve() + normalized_suffixes = tuple(suffix.casefold() for suffix in allowed_suffixes) + normalized_names = tuple(name.casefold() for name in allowed_names) + if resolved.suffix.casefold() not in normalized_suffixes and resolved.name.casefold() not in normalized_names: + expected = ", ".join((*allowed_suffixes, *allowed_names)) + raise XcodeHandoffError(f"Unsupported Xcode handoff target {resolved}; expected one of: {expected}") + if not resolved.exists(): + raise XcodeHandoffError(f"Xcode handoff target does not exist: {resolved}") + return resolved + + +def validated_xcode_project(path: Path) -> Path: + return validated_xcode_target(path, (".xcodeproj", ".xcworkspace"), ("Package.swift",)) + + +def xcode_project_handoff(path: Path) -> tuple[ExecutableCommand, tuple[str, ...]]: + target = validated_xcode_project(path) + command = executable_command("xcrun", (Path("/usr/bin/xcrun"),)) + return command, ("xed", str(target)) + + +def validated_xcode_artifact(path: Path) -> Path: + return validated_xcode_target(path, (".xcresult", ".trace"), ()) diff --git a/tests/test_xcode_handoff.py b/tests/test_xcode_handoff.py new file mode 100644 index 0000000..0cd2eee --- /dev/null +++ b/tests/test_xcode_handoff.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import os +import shutil +import tempfile +import unittest +from pathlib import Path + +from ios_developer_toolkit.xcode_handoff import ( + XcodeHandoffError, + coredevice_details_handoff, + rvi_list_handoff, + validated_xcode_artifact, + validated_xcode_project, + xcode_project_handoff, +) + + +class XcodeHandoffTests(unittest.TestCase): + @unittest.skipIf(shutil.which("xcrun") is None, "xcrun is unavailable") + def test_builds_selected_device_coredevice_details_command(self) -> None: + command, arguments = coredevice_details_handoff(" 00008110-001122334455001E ") + + self.assertTrue(command.program.is_file()) + self.assertTrue(os.access(command.program, os.X_OK)) + self.assertEqual( + arguments, + ( + "devicectl", + "device", + "info", + "details", + "--device", + "00008110-001122334455001E", + "--timeout", + "30", + ), + ) + + @unittest.skipUnless( + Path("/Library/Apple/usr/bin/rvictl").is_file() or shutil.which("rvictl") is not None, + "rvictl is unavailable", + ) + def test_resolves_read_only_rvi_inventory_command(self) -> None: + command, arguments = rvi_list_handoff() + + self.assertTrue(command.program.is_file()) + self.assertTrue(os.access(command.program, os.X_OK)) + self.assertEqual(arguments, ("-l",)) + + def test_validates_native_xcode_projects_results_and_traces(self) -> None: + temporary_directory = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, temporary_directory) + project = temporary_directory / "Toolkit.xcodeproj" + result = temporary_directory / "Toolkit.xcresult" + trace = temporary_directory / "Toolkit.trace" + package = temporary_directory / "Package.swift" + for directory in (project, result, trace): + directory.mkdir() + package.write_text("// swift-tools-version: 6.0\n", encoding="utf-8") + + self.assertEqual(validated_xcode_project(project), project.resolve()) + self.assertEqual(validated_xcode_project(package), package.resolve()) + self.assertEqual(validated_xcode_artifact(result), result.resolve()) + self.assertEqual(validated_xcode_artifact(trace), trace.resolve()) + command, arguments = xcode_project_handoff(project) + self.assertTrue(command.program.is_file()) + self.assertEqual(arguments, ("xed", str(project.resolve()))) + + def test_rejects_missing_or_unrelated_handoff_target(self) -> None: + temporary_directory = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, temporary_directory) + unrelated = temporary_directory / "notes.txt" + unrelated.write_text("not an Xcode artifact\n", encoding="utf-8") + + with self.assertRaises(XcodeHandoffError): + validated_xcode_artifact(unrelated) + with self.assertRaises(XcodeHandoffError): + validated_xcode_project(temporary_directory / "Missing.xcodeproj") + + +if __name__ == "__main__": + unittest.main() From 7934e21515c83d0b9c3379e549207b2161c1a8c8 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 03:39:42 -0700 Subject: [PATCH 08/16] Harden Command Center process handling --- README.md | 7 +- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- ios_developer_toolkit/app.py | 67 ++++---- ios_developer_toolkit/catalog.py | 3 + ios_developer_toolkit/entrypoint.py | 13 ++ ios_developer_toolkit/interactive_process.py | 171 +++++++++++++++++++ tests/test_core.py | 3 + tests/test_interactive_process.py | 83 +++++++++ 8 files changed, 309 insertions(+), 41 deletions(-) create mode 100644 ios_developer_toolkit/interactive_process.py create mode 100644 tests/test_interactive_process.py diff --git a/README.md b/README.md index 29b0c1d..615fd14 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one selected-device workflow; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 106 tests, embedded CLI checks, a 94-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 109 tests, embedded CLI checks, a 94-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. The README contains 18 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. @@ -547,7 +547,7 @@ For retained system log archives, use the applicable `syslog collect` command th ![Command Center](docs/screenshots/pymobiledevice3-console.png) -Command Center is the low-typing interface to the pinned `pymobiledevice3` runtime. Search or filter a preset, review its description and prerequisites, fill only the required parameters, inspect the exact command, and run it directly. The **Selected command readiness** pane evaluates only the capabilities that preset needs. **Run Device Readiness Check** opens the existing bounded, read-only Capability Matrix; it does not execute the selected command or repair the device automatically. **Check Guided Command Drift** is a separate host-only preflight that calls the installed CLI's `--help` for every guided route and verifies any preset option flags such as `--out`; it does not run a preset or contact a device. Each route uses the shared finite-operation controller for complete output draining, a five-second timeout, cancellation, launch diagnostics, and clean sequential relaunch. The report distinguishes unavailable routes, changed option syntax, failed checks, and routes not completed before cancellation. +Command Center is the low-typing interface to the pinned `pymobiledevice3` runtime. Search or filter a preset, review its description and prerequisites, fill only the required parameters, inspect the exact command, and run it directly. The **Selected command readiness** pane evaluates only the capabilities that preset needs. **Run Device Readiness Check** opens the existing bounded, read-only Capability Matrix; it does not execute the selected command or repair the device automatically. Guided and advanced commands use a typed interactive-process controller that drains both output channels at completion, distinguishes launch failure, crash, failure, success, and cancellation, and preserves the explicit **Stop** control required by streaming presets without imposing an arbitrary runtime limit. **Check Guided Command Drift** is a separate host-only preflight that calls the installed CLI's `--help` for every guided route and verifies any preset option flags such as `--out`; it does not run a preset or contact a device. Each drift route uses the shared finite-operation controller for complete output draining, a five-second timeout, cancellation, launch diagnostics, and clean sequential relaunch. The report distinguishes unavailable routes, changed option syntax, failed checks, and routes not completed before cancellation. Every preset has a visible risk class: @@ -1025,6 +1025,7 @@ Install or update Xcode if the candidate is absent. The toolkit requires the exp │ ├── device_compatibility.py # redacted local real-device readiness history │ ├── gui_pages.py # stateless Home, Live Logs, Safety pages and styling │ ├── installed_apps.py # app inventory validation and formatting +│ ├── interactive_process.py # typed user-stoppable process lifecycle controller │ ├── ipa_inspector.py # safe IPA extraction, provisioning, signature checks │ ├── live_logs.py # independent raw-spooling log windows │ ├── local_ddi.py # local Xcode candidate/Cryptex workflow @@ -1064,7 +1065,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 106 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 94-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 109 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 94-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 973b5dc..e1f72a1 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -113,7 +113,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P1 — turn diagnostics into a coherent workbench -* Maintain typed subprocess lifecycles and `OperationResult` across the workbench. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, Installed Apps inventory/uninstall, and IPA inspection/install use the finite-operation controller. Backup preserves private stdin requests and validated progress events without an arbitrary completion timeout. Evidence Capture reassembles validated JSON-line events and reserves a graceful finalization window for partial artifacts, coverage, manifests, and hashes. Long-running interactive streams retain their purpose-built lifecycle and explicit Stop controls. +* Maintain typed subprocess lifecycles and `OperationResult` across the workbench. Device discovery, Man Pages, sequential command drift, DDI/developer-image actions, Installed Apps inventory/uninstall, and IPA inspection/install use the finite-operation controller. Command Center uses a typed interactive controller that retains explicit Stop controls without an arbitrary runtime limit. Backup preserves private stdin requests and validated progress events without an arbitrary completion timeout. Evidence Capture reassembles validated JSON-line events and reserves a graceful finalization window for partial artifacts, coverage, manifests, and hashes. Long-running live-log streams retain their purpose-built lifecycle and explicit Stop controls. * Make a contextual readiness pane for the selected action, with one-click scoped rechecks and copyable remediation. * Maintain the opt-in physical-device compatibility protocol and its explicit USB, usbmux, CoreDevice, developer-service, privacy, and state-changing test boundaries. A pre-release dual-architecture frozen-artifact smoke workflow is now present. The release builder rejects any bundled Mach-O whose minimum macOS version is newer than the advertised 13.0 floor or lacks the native release architecture. * Generate concise changelog/release notes from tested behavior. Source, bundle, citation, packaging, and third-party-source metadata drift is now covered by automated tests. @@ -175,6 +175,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Replaced the Backup workspace's blocking, hand-buffered process path with a password-safe typed controller. | 98 tests and the 90-action GUI smoke passed; real child-process tests cover stdin-only credentials, validated streamed events, malformed-protocol termination, and one-result cancellation. | Evidence capture still needs a lifecycle designed around partial-artifact finalization rather than a generic finite command. | | 2026-09-22 | Added a typed Evidence Capture controller and close-safe graceful finalization. | 102 tests and the 90-action GUI smoke passed; real child-process tests cover fragmented JSON events, final-drain parsing, cancellation through `case-finished`, malformed-protocol finalization, and forced stop after the finalization deadline. | Physical-device collection remains opt-in; review each case manifest and hash inventory before relying on it. | | 2026-09-22 | Added native Apple developer-tool handoffs for selected-device CoreDevice details, RVI status, Xcode projects, test results, and Instruments traces. | 106 tests and a 94-action GUI smoke passed; tests validate the exact selected-device and RVI commands and reject missing or unrelated local targets. | The toolkit displays native output and opens native formats; it does not claim a stable schema for human `devicectl` output or reimplement Xcode. | +| 2026-09-22 | Migrated Command Center guided, advanced, finite, and streaming commands to a typed interactive-process lifecycle. | 109 tests and the 94-action GUI smoke passed; real child-process tests cover final stdout/stderr draining, launch failure, idempotent cancellation, and no arbitrary runtime limit. | Command output remains session-local unless the user explicitly preserves it through a task-specific evidence workflow. | ## Research sources diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index caa4367..a0a628b 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -137,6 +137,7 @@ format_byte_count, parse_installed_apps_json, ) +from ios_developer_toolkit.interactive_process import InteractiveProcessController from ios_developer_toolkit.ipa_inspector import ( IPAInspection, IPAInspectionError, @@ -593,7 +594,10 @@ def __init__(self) -> None: self._saved_locations = load_saved_locations(self._saved_locations_path) except LocationLabError as error: self._saved_locations_error = str(error) - self._console_process: QProcess | None = None + self._console_controller = InteractiveProcessController(self) + self._console_controller.stdout_received.connect(self._append_console_output) + self._console_controller.stderr_received.connect(self._append_console_output) + self._console_controller.completed.connect(self._console_completed) self._presets = command_presets() self._current_preset: CommandPreset | None = None self._preset_parameter_fields: dict[str, QLineEdit] = {} @@ -4972,7 +4976,7 @@ def _update_command_preview(self) -> None: self._update_command_controls() def _update_command_controls(self) -> None: - running = self._console_process is not None + running = self._console_controller.is_running() preset = self._current_preset preset_valid = False if preset is not None: @@ -5091,7 +5095,7 @@ def open_selected_preset_help(self) -> None: self.select_manpage_path(preset.manpage_path) def run_console_command(self) -> None: - if self._console_process is not None: + if self._console_controller.is_running(): QMessageBox.warning(self, "Command Running", "Stop the active console command first.") return try: @@ -5127,7 +5131,7 @@ def _run_console_arguments( requires_device: bool, profile: ActionSafetyProfile, ) -> None: - if self._console_process is not None: + if self._console_controller.is_running(): QMessageBox.warning(self, "Command Running", "Stop the active console command first.") return device = self.selected_device() @@ -5136,19 +5140,15 @@ def _run_console_arguments( return approval = "" if profile.level == "read-only" else f"\n[safety approval: {profile.level}; acknowledgement accepted]" self.console_output.appendPlainText(f"\n[{title}]{approval}\n$ pymobiledevice3 {shlex.join(arguments)}\n") - process = QProcess(self) - process.setProgram(str(self._pmd3.program)) - process.setArguments(list(command_arguments(self._pmd3, arguments))) - process.setWorkingDirectory(str(Path.home())) environment = base_environment() if device is None else device_environment(device.identifier) - process.setProcessEnvironment(qprocess_environment(environment)) - process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) - process.readyReadStandardOutput.connect(self._read_console_output) - process.finished.connect(self._console_finished) - process.errorOccurred.connect(self._console_error) - self._console_process = process + self._console_controller.start( + self._pmd3, + arguments, + environment, + Path.home(), + PROCESS_TERMINATE_GRACE_MS, + ) self._update_command_controls() - process.start() def _advanced_command_can_run_without_device(self, arguments: tuple[str, ...]) -> bool: prefixes = ( @@ -5159,30 +5159,24 @@ def _advanced_command_can_run_without_device(self, arguments: tuple[str, ...]) - ) return any(arguments[: len(prefix)] == prefix for prefix in prefixes) - def _read_console_output(self) -> None: - if self._console_process is not None: - text = bytes(self._console_process.readAllStandardOutput()).decode("utf-8", errors="replace") - self.console_output.moveCursor(QTextCursor.MoveOperation.End) - self.console_output.insertPlainText(text) + def _append_console_output(self, output: bytes) -> None: + self.console_output.moveCursor(QTextCursor.MoveOperation.End) + self.console_output.insertPlainText(output.decode("utf-8", errors="replace")) - def _console_finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: - del exit_status - self.console_output.appendPlainText(f"\n[finished: exit {exit_code}]") - self._console_process = None + def _console_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) + self.console_output.appendPlainText(f"\n[finished: {result_object.outcome}; exit {exit_label}]") + if result_object.error_message: + self.console_output.appendPlainText(f"Process error: {result_object.error_message}") self._update_command_controls() - def _console_error(self, process_error: QProcess.ProcessError) -> None: - if self._console_process is not None: - self.console_output.appendPlainText(f"\nProcess error: {self._console_process.errorString()}") - if process_error == QProcess.ProcessError.FailedToStart: - self._console_process = None - self._update_command_controls() - def stop_console_command(self) -> None: - if self._console_process is not None: + if self._console_controller.is_running(): self.console_output.appendPlainText("\nRequesting command stop…") - self._console_process.terminate() + self._console_controller.cancel() def start_command_drift_check(self) -> None: if self._command_drift_session_active: @@ -5690,10 +5684,7 @@ def closeEvent(self, event: QCloseEvent) -> None: collection_running = self._collection_controller.is_running() critical_processes = tuple( process - for process in ( - self._console_process, - self._location_process, - ) + for process in (self._location_process,) if process is not None and process.state() != QProcess.ProcessState.NotRunning ) active_operations = ( @@ -5703,6 +5694,7 @@ def closeEvent(self, event: QCloseEvent) -> None: or sideload_running or backup_running or collection_running + or self._console_controller.is_running() or critical_processes ) if active_operations and not self._close_after_collection: @@ -5727,6 +5719,7 @@ def closeEvent(self, event: QCloseEvent) -> None: self._reconnect_timeout_timer.stop() self._manpage_controller.shutdown(3000, 1000) self._command_drift_controller.shutdown(3000, 1000) + self._console_controller.shutdown(10000, 3000) self._action_controller.shutdown(10000, 3000) self._apps_controller.shutdown(10000, 3000) self._ipa_inspection_controller.shutdown(10000, 3000) diff --git a/ios_developer_toolkit/catalog.py b/ios_developer_toolkit/catalog.py index 40ded2f..075913b 100644 --- a/ios_developer_toolkit/catalog.py +++ b/ios_developer_toolkit/catalog.py @@ -54,7 +54,10 @@ def is_potentially_mutating(arguments: tuple[str, ...]) -> bool: if not arguments: return False safe_prefixes: tuple[tuple[str, ...], ...] = ( + ("version",), ("usbmux", "list"), + ("bonjour",), + ("remote", "browse"), ("lockdown", "info"), ("mounter", "list"), ("mounter", "lookup"), diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 9f1a13a..3cafaeb 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -233,6 +233,19 @@ def run_smoke_test(arguments: Sequence[str]) -> int: action_output = window.action_output.toPlainText() if "[finished: succeeded; exit 0]" not in action_output: raise RuntimeError(f"GUI DDI action controller failed its host-only smoke command: {action_output}") + window.console_input.setText("version") + window.console_run_button.click() + console_deadline = time.monotonic() + 20 + while window._console_controller.is_running() and time.monotonic() < console_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._console_controller.is_running(): + window._console_controller.cancel() + raise RuntimeError("GUI Command Center controller did not complete within its smoke-test window") + console_output = window.console_output.toPlainText() + if "[finished: succeeded; exit 0]" not in console_output: + raise RuntimeError(f"GUI Command Center controller failed its host-only smoke command: {console_output}") synthetic_inventory = ( '{"com.example.toolkit-smoke": {' '"CFBundleIdentifier": "com.example.toolkit-smoke", ' diff --git a/ios_developer_toolkit/interactive_process.py b/ios_developer_toolkit/interactive_process.py new file mode 100644 index 0000000..60a6c73 --- /dev/null +++ b/ios_developer_toolkit/interactive_process.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from pathlib import Path +from typing import Literal, Mapping, Sequence + +from PySide6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, Signal + +from ios_developer_toolkit.qt_process import OperationResult, ProcessOutcome +from ios_developer_toolkit.runtime import ExecutableCommand, command_arguments, command_argv + + +class InteractiveProcessController(QObject): + """Own one user-stoppable process without imposing an arbitrary runtime limit.""" + + stdout_received = Signal(bytes) + stderr_received = Signal(bytes) + completed = Signal(object) + + def __init__(self, parent: QObject) -> None: + super().__init__(parent) + self._process: QProcess | None = None + self._command: ExecutableCommand | None = None + self._arguments: tuple[str, ...] = () + self._terminate_grace_milliseconds = 0 + self._stdout = bytearray() + self._stderr = bytearray() + self._started_at = "" + self._error_message: str | None = None + self._stop_outcome: Literal["cancelled"] | None = None + self._completed = False + self._kill_timer = QTimer(self) + self._kill_timer.setSingleShot(True) + self._kill_timer.timeout.connect(self._kill) + + def is_running(self) -> bool: + return self._process is not None + + def start( + self, + command: ExecutableCommand, + arguments: Sequence[str], + environment: Mapping[str, str], + working_directory: Path, + terminate_grace_milliseconds: int, + ) -> None: + if self.is_running(): + raise RuntimeError("Cannot start an interactive process while another process is running") + if terminate_grace_milliseconds <= 0: + raise ValueError( + f"Interactive process termination grace period must be positive: {terminate_grace_milliseconds}" + ) + resolved_working_directory = working_directory.expanduser().resolve() + if not resolved_working_directory.is_dir(): + raise ValueError(f"Interactive process working directory does not exist: {resolved_working_directory}") + self._command = command + self._arguments = tuple(arguments) + self._terminate_grace_milliseconds = terminate_grace_milliseconds + self._stdout.clear() + self._stderr.clear() + self._started_at = datetime.now(timezone.utc).isoformat() + self._error_message = None + self._stop_outcome = None + self._completed = False + + process = QProcess(self) + process.setProgram(str(command.program)) + process.setArguments(list(command_arguments(command, self._arguments))) + process.setWorkingDirectory(str(resolved_working_directory)) + process_environment = QProcessEnvironment.systemEnvironment() + for key, value in sorted(environment.items()): + process_environment.insert(key, value) + process.setProcessEnvironment(process_environment) + process.readyReadStandardOutput.connect(self._drain_output) + process.readyReadStandardError.connect(self._drain_output) + process.errorOccurred.connect(self._process_error) + process.finished.connect(self._finished) + self._process = process + process.start() + + def cancel(self) -> None: + process = self._process + if process is None or process.state() == QProcess.ProcessState.NotRunning: + return + if self._stop_outcome == "cancelled": + return + self._stop_outcome = "cancelled" + process.terminate() + self._kill_timer.start(self._terminate_grace_milliseconds) + + def shutdown(self, terminate_timeout_milliseconds: int, kill_timeout_milliseconds: int) -> None: + if terminate_timeout_milliseconds <= 0: + raise ValueError(f"Shutdown termination timeout must be positive: {terminate_timeout_milliseconds}") + if kill_timeout_milliseconds <= 0: + raise ValueError(f"Shutdown kill timeout must be positive: {kill_timeout_milliseconds}") + process = self._process + if process is None: + return + self._stop_outcome = "cancelled" + self._kill_timer.stop() + if process.state() != QProcess.ProcessState.NotRunning: + process.terminate() + if not process.waitForFinished(terminate_timeout_milliseconds): + process.kill() + if not process.waitForFinished(kill_timeout_milliseconds): + raise RuntimeError(f"Interactive process did not stop after terminate and kill: {process.program()}") + else: + self._finish_once("cancelled", process.exitCode()) + + def _drain_output(self) -> None: + process = self._process + if process is None: + return + stdout = bytes(process.readAllStandardOutput()) + stderr = bytes(process.readAllStandardError()) + if stdout: + self._stdout.extend(stdout) + self.stdout_received.emit(stdout) + if stderr: + self._stderr.extend(stderr) + self.stderr_received.emit(stderr) + + def _process_error(self, process_error: QProcess.ProcessError) -> None: + process = self._process + if process is None: + raise RuntimeError("Interactive process reported an error without an active process") + self._error_message = process.errorString() + if process_error == QProcess.ProcessError.FailedToStart: + self._finish_once("launch-failed", None) + + def _finished(self, exit_code: int, exit_status: QProcess.ExitStatus) -> None: + self._drain_output() + if self._stop_outcome is not None: + outcome: ProcessOutcome = self._stop_outcome + elif exit_status == QProcess.ExitStatus.CrashExit: + outcome = "crashed" + elif exit_code == 0: + outcome = "succeeded" + else: + outcome = "failed" + self._finish_once(outcome, exit_code) + + def _kill(self) -> None: + process = self._process + if process is not None and process.state() != QProcess.ProcessState.NotRunning: + process.kill() + + def _finish_once(self, outcome: ProcessOutcome, exit_code: int | None) -> None: + if self._completed: + return + command = self._command + if command is None: + raise RuntimeError("Interactive process completed without a command") + self._drain_output() + self._completed = True + self._kill_timer.stop() + result = OperationResult( + command_argv(command, self._arguments), + outcome, + self._started_at, + datetime.now(timezone.utc).isoformat(), + exit_code, + self._error_message, + bytes(self._stdout), + bytes(self._stderr), + ) + process = self._process + self._process = None + if process is not None: + process.deleteLater() + self.completed.emit(result) diff --git a/tests/test_core.py b/tests/test_core.py index c169e58..7282f70 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -129,6 +129,9 @@ def test_simulated_device_is_visibly_labeled_and_never_looks_like_usbmux_data(se class CommandPolicyTests(unittest.TestCase): def test_read_commands_do_not_require_mutation_confirmation(self) -> None: + self.assertFalse(is_potentially_mutating(("version",))) + self.assertFalse(is_potentially_mutating(("bonjour", "rsd"))) + self.assertFalse(is_potentially_mutating(("remote", "browse"))) self.assertFalse(is_potentially_mutating(("developer", "dvt", "ls", "/"))) self.assertFalse(is_potentially_mutating(("pcap", "--out", "capture.pcap"))) diff --git a/tests/test_interactive_process.py b/tests/test_interactive_process.py new file mode 100644 index 0000000..2d50943 --- /dev/null +++ b/tests/test_interactive_process.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import sys +import time +import unittest +from collections.abc import Callable +from pathlib import Path + +from PySide6.QtCore import QCoreApplication + +from ios_developer_toolkit.interactive_process import InteractiveProcessController +from ios_developer_toolkit.qt_process import OperationResult +from ios_developer_toolkit.runtime import ExecutableCommand + + +class InteractiveProcessControllerTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.application = QCoreApplication.instance() or QCoreApplication(["interactive-process-tests"]) + + def test_returns_terminal_output_and_status(self) -> None: + controller = InteractiveProcessController(self.application) + results: list[OperationResult] = [] + controller.completed.connect(results.append) + + controller.start( + ExecutableCommand(Path(sys.executable), ()), + ("-c", "import sys; sys.stdout.write('ready'); sys.stderr.write('notice')"), + {}, + Path.cwd(), + 500, + ) + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].outcome, "succeeded") + self.assertEqual(results[0].stdout, b"ready") + self.assertEqual(results[0].stderr, b"notice") + self.assertFalse(controller.is_running()) + + def test_cancels_streaming_command_once(self) -> None: + controller = InteractiveProcessController(self.application) + results: list[OperationResult] = [] + controller.completed.connect(results.append) + + controller.start( + ExecutableCommand(Path(sys.executable), ()), + ("-c", "import time; time.sleep(10)"), + {}, + Path.cwd(), + 500, + ) + controller.cancel() + controller.cancel() + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].outcome, "cancelled") + self.assertFalse(controller.is_running()) + + def test_reports_launch_failure(self) -> None: + controller = InteractiveProcessController(self.application) + results: list[OperationResult] = [] + controller.completed.connect(results.append) + + controller.start(ExecutableCommand(Path("/missing/interactive-tool"), ()), (), {}, Path.cwd(), 500) + self._wait_for(lambda: bool(results), 3) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].outcome, "launch-failed") + self.assertTrue(results[0].error_message) + self.assertFalse(controller.is_running()) + + def _wait_for(self, predicate: Callable[[], bool], timeout_seconds: int) -> None: + deadline = time.monotonic() + timeout_seconds + while not predicate() and time.monotonic() < deadline: + self.application.processEvents() + time.sleep(0.01) + self.application.processEvents() + + +if __name__ == "__main__": + unittest.main() From 4ff040ca1f327a822c2f2ee4e44baf68cb7d4689 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 03:55:11 -0700 Subject: [PATCH 09/16] Add session operation manifests --- README.md | 20 +- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- docs/screenshots/session-activity.png | Bin 0 -> 87913 bytes ios_developer_toolkit/app.py | 248 ++++++++++++++- ios_developer_toolkit/entrypoint.py | 50 ++- ios_developer_toolkit/operation_history.py | 347 +++++++++++++++++++++ tests/test_operation_history.py | 91 ++++++ 7 files changed, 750 insertions(+), 9 deletions(-) create mode 100644 docs/screenshots/session-activity.png create mode 100644 ios_developer_toolkit/operation_history.py create mode 100644 tests/test_operation_history.py diff --git a/README.md b/README.md index 615fd14..7309285 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ The screenshots use an illustrative device name, model, version, build, and UDID - [Evidence Capture](#evidence-capture) - [Man Pages](#man-pages) - [Scope and Safety](#scope-and-safety) + - [Session Activity and operation manifests](#session-activity-and-operation-manifests) - [Developer Disk Images explained](#developer-disk-images-explained) - [Guided command catalog](#guided-command-catalog) - [Evidence case contents](#evidence-case-contents) @@ -84,6 +85,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - **Retry Scan** performs an immediate usbmux device check, while **Reconnect & Retry…** opens a guided detection window without attempting to restart SIP-protected Apple services; - **Connection diagnostic** records whether usbmux did not launch, failed, returned malformed output, found no devices, or returned selectable devices; its privacy-safe summary is visible in Device & DDI and included in a sanitized support bundle; - **Selected command readiness** maps each guided Command Center action to the exact connection, trust, Developer Mode, DDI, tunnel, CoreDevice, DVT, or Web Inspector checks it needs, with a one-click route to the bounded read-only matrix; +- **Session Activity** correlates completed typed operations with workspace, target, transport, exact argument vector, timing, terminal status, prerequisite snapshot, output paths, and output hashes without automatically persisting raw command output; - the desktop UI starts independently of the MobileBackup2 transport, and device discovery consumes output both while the child process runs and after it exits, so a fast successful `usbmux list` result is not lost before the picker is updated; - **Demo Mode** shows a prominently labeled simulated iPhone for walkthroughs and screenshots, while deliberately withholding a selected physical-device target and disabling device operations; - the manual **Capability Matrix** reports host, trust, Developer Mode, DDI, tunnel, DVT, CoreDevice, and related readiness as separate bounded results, then compares completed local probes across real devices without retaining raw UDIDs; @@ -91,10 +93,10 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one selected-device workflow; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 109 tests, embedded CLI checks, a 94-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 113 tests, embedded CLI checks, a 95-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. -The README contains 18 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. +The README contains 19 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. | Prepare the device and DDI | Observe live services | Run guided commands | |---|---|---| @@ -117,6 +119,7 @@ The README contains 18 sanitized screenshots. The six views below provide a quic | Keyboard-first access | Adds named controls, standard navigation, and application-wide workspace shortcuts. | Shortcuts never bypass action confirmation. | | Sanitized Support Bundle | Creates a reviewable local ZIP with environment/readiness summaries and a SHA-256 manifest. | Excludes device identity, captures, backups, command output, credentials, and common host/network identifiers. | | Demo Mode | Shows a local simulated iPhone for an honest product walkthrough or screenshot. | The banner identifies the simulation and no device service, command, mount, capture, backup, or location operation can run. | +| Session Activity | Correlates completed typed operations and previews an exportable structured JSON manifest. | Session-only by default; raw output is omitted, and explicit exports can still contain identifiers and local paths. | ## What the workbench covers @@ -141,6 +144,16 @@ The interface gives named controls and descriptions to the primary device picker Use **Keyboard Shortcuts** in the window header, or press `⌘ /`, for the complete reference. The most useful shortcuts are `⌘ L` to focus workspace navigation, `⌘ F` to focus contextual search, `⌘ R` to retry discovery, `⌘ 1` through `⌘ 0` to open the first ten workspaces, `⌘ ⇧ M` for Man Pages, and `⌘ ⇧ S` for Scope & Safety. `⌘ ⌥ ←` and `⌘ ⌥ →` move between workspaces. Tab, Shift-Tab, Space, Return, and Arrow keys retain their standard Qt behavior. Shortcuts never skip device-action confirmation or typed acknowledgements. +### Session Activity and operation manifests + +![Session Activity and structured operation manifest](docs/screenshots/session-activity.png) + +**Session Activity** in the sidebar shows completed operations from typed controllers in the current app session. The journal currently covers Device & DDI and Apple handoffs, Command Center, Installed Apps, IPA inspection and installation, MobileBackup2, Evidence Capture, and live Man Pages. Periodic discovery, Command Drift's internal per-route probes, and raw live-log streams are intentionally excluded; those have their own aggregate reports or evidence sidecars. + +Each record distinguishes the workspace and target from the transport, exact argument vector, start and finish timestamps, duration, terminal process outcome, exit code, process error, prerequisite-state snapshot, declared output paths, and SHA-256 plus byte count for each captured output channel. The manifest does **not** embed stdout or stderr. The UI keeps at most 250 records in memory and writes nothing automatically. + +Use **Copy Selected Manifest** or **Save Selected Manifest…** only when you intend to preserve a record. A saved JSON file is created with owner-only permissions and is never overwritten. Because exact arguments and targets can include a UDID, device details, IPA or GPX paths, case locations, and other sensitive values, review the manifest before sharing it. Session Activity is separate from the sanitized support bundle, which continues to exclude command arguments and device identity. + ### Sanitized support bundle **Create Support Bundle…** in the window header creates a local ZIP for a bug report or support request. It is opt-in and never uploads anything. The ZIP contains toolkit and dependency versions, macOS/Python metadata, selected workspace, device count without identity, aggregate Capability Matrix states, sanitized status summaries, the sanitized Command Drift report, and a SHA-256 manifest. @@ -1031,6 +1044,7 @@ Install or update Xcode if the candidate is absent. The toolkit requires the exp │ ├── local_ddi.py # local Xcode candidate/Cryptex workflow │ ├── location_lab.py # coordinates, GPX, routes, saved places, evidence │ ├── models.py # typed device and collection models +│ ├── operation_history.py # session journal, output digests, and explicit JSON export │ ├── entrypoint.py # packaged internal CLI and worker dispatch │ ├── qt_process.py # typed, bounded finite-process lifecycle controller │ ├── runtime.py # source/frozen commands and device environment @@ -1065,7 +1079,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 109 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 94-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 113 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 95-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index e1f72a1..cc5b4dc 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -121,7 +121,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P2 — deepen expert workflows without scope creep -* Add per-operation history, structured output manifests, and a universal command/action palette that only exposes eligible operations. +* Maintain the session-local typed-operation journal and explicit structured JSON manifests. Add a universal command/action palette that only exposes eligible operations. * Implement a guided MVT backup-analysis handoff with explicit consent, no password persistence, output isolation, and no “clean device” conclusion. * Add optional user-configured adapters for `go-ios`, `idb`, and `ipsw`, each with executable provenance and version display. * Publish a small documentation site split into quick start, architecture, safety, troubleshooting, release verification, and contributor paths. @@ -176,6 +176,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Added a typed Evidence Capture controller and close-safe graceful finalization. | 102 tests and the 90-action GUI smoke passed; real child-process tests cover fragmented JSON events, final-drain parsing, cancellation through `case-finished`, malformed-protocol finalization, and forced stop after the finalization deadline. | Physical-device collection remains opt-in; review each case manifest and hash inventory before relying on it. | | 2026-09-22 | Added native Apple developer-tool handoffs for selected-device CoreDevice details, RVI status, Xcode projects, test results, and Instruments traces. | 106 tests and a 94-action GUI smoke passed; tests validate the exact selected-device and RVI commands and reject missing or unrelated local targets. | The toolkit displays native output and opens native formats; it does not claim a stable schema for human `devicectl` output or reimplement Xcode. | | 2026-09-22 | Migrated Command Center guided, advanced, finite, and streaming commands to a typed interactive-process lifecycle. | 109 tests and the 94-action GUI smoke passed; real child-process tests cover final stdout/stderr draining, launch failure, idempotent cancellation, and no arbitrary runtime limit. | Command output remains session-local unless the user explicitly preserves it through a task-specific evidence workflow. | +| 2026-09-22 | Added a session-local operation journal and explicit per-operation JSON manifests across the primary typed workflows. | 113 tests and a 95-action GUI smoke passed; tests cover immutable bounded history, exact argument retention, output hashing without raw-output embedding, owner-only export, and overwrite refusal. | Capability Matrix, Location Lab, Live Logs, and Command Drift retain their stronger workflow-specific records rather than duplicating raw or high-volume events into this journal. | ## Research sources diff --git a/docs/screenshots/session-activity.png b/docs/screenshots/session-activity.png new file mode 100644 index 0000000000000000000000000000000000000000..70658d1cba880107929ea4937587ba488ee5b606 GIT binary patch literal 87913 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 D;#l-9 literal 0 HcmV?d00001 diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index a0a628b..177c030 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -173,6 +173,15 @@ ) from ios_developer_toolkit.live_logs import LiveLogError, LiveLogWindow, log_stream_specs, stream_spec from ios_developer_toolkit.models import DeviceDataError, IOSDevice, parse_devices_json +from ios_developer_toolkit.operation_history import ( + OperationContext, + OperationHistoryDialog, + OperationRecord, + append_operation_record, + operation_context, + operation_record, + with_output_paths, +) from ios_developer_toolkit.qt_process import ( FiniteProcessController, OperationResult, @@ -228,6 +237,7 @@ COLLECTION_FINALIZATION_TIMEOUT_MS = 2 * 60_000 XCODE_HANDOFF_TIMEOUT_MS = 60_000 PROCESS_TERMINATE_GRACE_MS = 1_500 +MAX_SESSION_OPERATION_RECORDS = 250 def application_icon_path() -> Path: @@ -616,6 +626,8 @@ def __init__(self) -> None: self._command_drift_probes: dict[tuple[str, ...], HelpRouteProbe] = {} self._last_case_path: Path | None = None self._active_case_path: Path | None = None + self._operation_records: tuple[OperationRecord, ...] = () + self._pending_operation_contexts: dict[str, OperationContext] = {} self._keyboard_shortcuts: list[QShortcut] = [] self._build_ui() self._configure_accessibility() @@ -712,6 +724,13 @@ def _build_ui(self) -> None: self.navigation_list.setObjectName("workspaceNavigation") self.navigation_list.setSpacing(2) sidebar_layout.addWidget(self.navigation_list, 1) + self.session_activity_button = QPushButton("Session Activity (0)") + self.session_activity_button.setObjectName("sessionActivityButton") + self.session_activity_button.setToolTip( + "Review completed typed operations from this session and explicitly export a structured manifest" + ) + self.session_activity_button.clicked.connect(self.show_session_activity) + sidebar_layout.addWidget(self.session_activity_button) version_note = QLabel(f"Toolkit {APP_VERSION}\npymobiledevice3 11.15.1") version_note.setObjectName("sidebarVersion") version_note.setWordWrap(True) @@ -768,6 +787,10 @@ def _configure_accessibility(self) -> None: self.support_bundle_button.setAccessibleDescription( "Create a local ZIP that excludes device content and sensitive artifacts. The application never uploads it." ) + self.session_activity_button.setAccessibleName("Session activity") + self.session_activity_button.setAccessibleDescription( + "Review completed typed operations and explicitly export a selected structured manifest." + ) self.connection_banner.setAccessibleName("Device connection status") self.connection_banner.setAccessibleDescription( "Reports whether a trusted iPhone or iPad is currently available to the toolkit." @@ -816,6 +839,7 @@ def _configure_accessibility(self) -> None: QWidget.setTabOrder(self.reconnect_device_button, self.keyboard_shortcuts_button) QWidget.setTabOrder(self.keyboard_shortcuts_button, self.support_bundle_button) QWidget.setTabOrder(self.support_bundle_button, self.navigation_list) + QWidget.setTabOrder(self.navigation_list, self.session_activity_button) def _configure_keyboard_shortcuts(self) -> None: self._add_application_shortcut("Meta+R", self._scanner_scan, "shortcutRetryDeviceScan") @@ -955,6 +979,81 @@ def show_keyboard_shortcuts(self) -> None: layout.addWidget(buttons) dialog.exec() + def show_session_activity(self) -> None: + dialog = OperationHistoryDialog(self._operation_records, self) + dialog.exec() + + def _host_operation_context( + self, + title: str, + workspace: str, + transport: str, + output_paths: tuple[str, ...], + ) -> OperationContext: + return operation_context( + title, + workspace, + "Local Mac", + transport, + ("device:not-required",), + output_paths, + ) + + def _device_operation_context( + self, + title: str, + workspace: str, + transport: str, + device: IOSDevice, + output_paths: tuple[str, ...], + ) -> OperationContext: + identifier = "".join(character for character in device.identifier.upper() if character.isalnum()) + suffix = identifier[-6:] if len(identifier) >= 6 else "unknown" + target = ( + f"{device.product_type} • iOS {device.product_version} • {device.connection_type} • " + f"identifier ending {suffix}" + ) + observed_capabilities = tuple( + f"{capability_identifier}:{result.state}" + for capability_identifier, result in sorted(self._capability_results.items()) + if result.state != "not-tested" + ) + capability_snapshot = observed_capabilities or ("capabilities:not-tested",) + return operation_context( + title, + workspace, + target, + transport, + ("device:selected", *capability_snapshot), + output_paths, + ) + + def _begin_operation(self, slot: str, context: OperationContext) -> None: + normalized_slot = slot.strip() + if not normalized_slot: + raise ValueError("Operation slot cannot be empty") + if normalized_slot in self._pending_operation_contexts: + raise RuntimeError(f"Operation slot is already active: {normalized_slot}") + self._pending_operation_contexts[normalized_slot] = context + + def _update_operation_output_paths(self, slot: str, output_paths: tuple[str, ...]) -> None: + context = self._pending_operation_contexts.get(slot) + if context is None: + raise RuntimeError(f"Cannot update output paths for inactive operation slot: {slot}") + self._pending_operation_contexts[slot] = with_output_paths(context, output_paths) + + def _complete_operation(self, slot: str, result: OperationResult) -> None: + context = self._pending_operation_contexts.pop(slot, None) + if context is None: + raise RuntimeError(f"Cannot complete inactive operation slot: {slot}") + record = operation_record(context, result) + self._operation_records = append_operation_record( + self._operation_records, + record, + MAX_SESSION_OPERATION_RECORDS, + ) + self.session_activity_button.setText(f"Session Activity ({len(self._operation_records)})") + def create_support_bundle(self) -> None: message = ( "Create a local sanitized support ZIP?\n\n" @@ -3095,6 +3194,13 @@ def mount_selected_ddi(self) -> None: base_environment(), "mount-local-cryptex", DDI_ACTION_TIMEOUT_MS, + self._device_operation_context( + "Install Local Xcode DDI", + "Device & DDI", + "local DDI worker, Apple TSS, and CoreDevice Cryptex service", + device, + (), + ), ) def remove_selected_ddi(self) -> None: @@ -3136,7 +3242,20 @@ def show_coredevice_details(self) -> None: except XcodeHandoffError as error: QMessageBox.critical(self, "CoreDevice Tool Unavailable", str(error)) return - self._start_action(command, arguments, base_environment(), "coredevice-details", XCODE_HANDOFF_TIMEOUT_MS) + self._start_action( + command, + arguments, + base_environment(), + "coredevice-details", + XCODE_HANDOFF_TIMEOUT_MS, + self._device_operation_context( + "CoreDevice Details", + "Device & DDI", + "Apple devicectl", + device, + (), + ), + ) def list_rvi_interfaces(self) -> None: try: @@ -3144,7 +3263,14 @@ def list_rvi_interfaces(self) -> None: except XcodeHandoffError as error: QMessageBox.critical(self, "RVI Tool Unavailable", str(error)) return - self._start_action(command, arguments, base_environment(), "rvi-status", XCODE_HANDOFF_TIMEOUT_MS) + self._start_action( + command, + arguments, + base_environment(), + "rvi-status", + XCODE_HANDOFF_TIMEOUT_MS, + self._host_operation_context("List RVI Interfaces", "Device & DDI", "Apple rvictl", ()), + ) def open_xcode_project(self) -> None: selected, _ = QFileDialog.getOpenFileName( @@ -3160,7 +3286,14 @@ def open_xcode_project(self) -> None: except XcodeHandoffError as error: QMessageBox.critical(self, "Invalid Xcode Project", str(error)) return - self._start_action(command, arguments, base_environment(), "open-xcode-project", XCODE_HANDOFF_TIMEOUT_MS) + self._start_action( + command, + arguments, + base_environment(), + "open-xcode-project", + XCODE_HANDOFF_TIMEOUT_MS, + self._host_operation_context("Open Xcode Project", "Device & DDI", "Apple xed", ()), + ) def open_xcode_artifact(self) -> None: selected, _ = QFileDialog.getOpenFileName( @@ -3184,12 +3317,29 @@ def _run_pmd3_action(self, arguments: tuple[str, ...], context: str) -> None: if device is None: self._show_no_device() return + titles = { + "developer-mode-status": "Check Developer Mode", + "mount-personalized": "Mount Personalized DDI", + "unmount-personalized": "Unmount Personalized DDI", + "uninstall-local-cryptex": "Uninstall Local DDI Cryptex", + "list-images": "List Developer Images", + } + title = titles.get(context) + if title is None: + raise KeyError(f"Unknown Device & DDI operation context: {context}") self._start_action( self._pmd3, arguments, device_environment(device.identifier), context, DDI_ACTION_TIMEOUT_MS, + self._device_operation_context( + title, + "Device & DDI", + "pymobiledevice3 selected-device transport", + device, + (), + ), ) def _start_action( @@ -3199,12 +3349,14 @@ def _start_action( environment: Mapping[str, str], context: str, timeout_milliseconds: int, + history_context: OperationContext, ) -> None: if self._action_controller.is_running(): QMessageBox.warning(self, "Action Running", "Wait for the current Device & DDI action to finish.") return self.action_output.appendPlainText(f"$ {command_text(program, arguments)}") self._action_context = context + self._begin_operation("device-and-ddi", history_context) self.mount_button.setEnabled(False) self.remove_button.setEnabled(False) self.coredevice_details_button.setEnabled(False) @@ -3227,6 +3379,7 @@ def _append_action_output(self, output: bytes) -> None: def _action_completed(self, result_object: object) -> None: if not isinstance(result_object, OperationResult): raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("device-and-ddi", result_object) context = self._action_context combined_output = result_object.stdout + result_object.stderr semantic_failure = output_indicates_failure(combined_output) @@ -4014,6 +4167,17 @@ def start_collection(self) -> None: self._collection_case_finished = False self.start_collection_button.setEnabled(False) self.stop_collection_button.setEnabled(True) + initial_output_paths = () if self._active_case_path is None else (str(self._active_case_path),) + self._begin_operation( + "evidence-collection", + self._device_operation_context( + "Collect and Finalize Evidence", + "Evidence Capture", + "typed evidence collector worker", + device, + initial_output_paths, + ), + ) self._collection_controller.start( worker, arguments, @@ -4032,15 +4196,18 @@ def _collection_event_received(self, event_object: object) -> None: if event.event in ("case-created", "case-attached") and event.path is not None: self._last_case_path = event.path self.open_case_button.setEnabled(True) + self._update_operation_output_paths("evidence-collection", (str(event.path),)) if event.event == "case-finished": self._collection_case_finished = True if event.path is not None: self._last_case_path = event.path self.open_case_button.setEnabled(True) + self._update_operation_output_paths("evidence-collection", (str(event.path),)) def _collection_completed(self, result_object: object) -> None: if not isinstance(result_object, OperationResult): raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("evidence-collection", result_object) exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) self.collection_output.appendPlainText( f"\nCollection process finished: {result_object.outcome}; exit {exit_label}." @@ -4105,6 +4272,15 @@ def _start_ipa_inspection(self) -> None: self.ipa_inspection_summary.setPlainText("Inspecting archive, provisioning profile, and code signature…") self.ipa_inspection_progress.setVisible(True) worker = worker_command("ipa-inspector") + self._begin_operation( + "ipa-inspection", + self._host_operation_context( + "Inspect IPA", + "Sideload IPA", + "local IPA inspection worker and macOS codesign", + (), + ), + ) self._ipa_inspection_controller.start( finite_process_request( worker, @@ -4119,6 +4295,7 @@ def _start_ipa_inspection(self) -> None: def _ipa_inspection_completed(self, result_object: object) -> None: if not isinstance(result_object, OperationResult): raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("ipa-inspection", result_object) stderr_text = result_object.stderr.decode("utf-8", errors="replace").strip() if result_object.outcome != "succeeded": exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) @@ -4210,6 +4387,16 @@ def _start_sideload_action(self, arguments: tuple[str, ...], context: str) -> No self.sideload_output.appendPlainText(f"\n$ pymobiledevice3 {shlex.join(arguments)}\n") self.sideload_status.setText(f"Running {context} operation on {device.display_name()}…") self.sideload_activity_progress.setVisible(True) + self._begin_operation( + "sideload-ipa", + self._device_operation_context( + "Install IPA" if context == "install" else context.replace("-", " ").title(), + "Sideload IPA", + "pymobiledevice3 selected-device transport", + device, + (), + ), + ) self._sideload_controller.start( finite_process_request( self._pmd3, @@ -4228,6 +4415,7 @@ def _append_sideload_output(self, output: bytes) -> None: def _sideload_completed(self, result_object: object) -> None: if not isinstance(result_object, OperationResult): raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("sideload-ipa", result_object) context = self._sideload_context semantic_failure = output_indicates_failure(result_object.stdout + result_object.stderr) succeeded = result_object.outcome == "succeeded" and not semantic_failure @@ -4277,6 +4465,20 @@ def _start_apps_action(self, arguments: tuple[str, ...], context: str) -> None: self._apps_context = context self.apps_output.appendPlainText(f"\n$ pymobiledevice3 {shlex.join(arguments)}\n") self.apps_status.setText(f"Running {context} operation on {device.display_name()}…") + titles = {"inventory": "Refresh Installed Apps", "uninstall": "Uninstall Application"} + title = titles.get(context) + if title is None: + raise KeyError(f"Unknown Installed Apps operation context: {context}") + self._begin_operation( + "installed-apps", + self._device_operation_context( + title, + "Installed Apps", + "pymobiledevice3 selected-device transport", + device, + (), + ), + ) self._apps_controller.start( finite_process_request( self._pmd3, @@ -4295,6 +4497,7 @@ def _append_apps_stderr(self, output: bytes) -> None: def _apps_completed(self, result_object: object) -> None: if not isinstance(result_object, OperationResult): raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("installed-apps", result_object) context = self._apps_context combined_output = result_object.stdout + result_object.stderr succeeded = result_object.outcome == "succeeded" and not output_indicates_failure(combined_output) @@ -4531,6 +4734,20 @@ def _start_backup_worker(self, action: BackupAction, request: BackupRequest) -> ) if action == "backup": self.backup_progress.setValue(0) + device = self.selected_device() + if device is None or device.identifier != request.udid: + raise RuntimeError("Backup operation target does not match the selected device") + output_paths = (str(request.destination / request.udid),) if action == "backup" else () + self._begin_operation( + "backup", + self._device_operation_context( + "Check Backup Encryption" if action == "status" else "Create Device Backup", + "Backup", + "pymobiledevice3 MobileBackup2 worker", + device, + output_paths, + ), + ) self._backup_controller.start( worker, action, @@ -4554,6 +4771,7 @@ def _handle_backup_event(self, event_object: object) -> None: self._backup_encryption_choice_changed(self.require_encryption_checkbox.isChecked()) if event.path is not None: self._last_backup_path = event.path + self._update_operation_output_paths("backup", (str(event.path),)) def _append_backup_stderr(self, output: bytes) -> None: self.backup_output.moveCursor(QTextCursor.MoveOperation.End) @@ -4562,6 +4780,7 @@ def _append_backup_stderr(self, output: bytes) -> None: def _backup_completed(self, result_object: object) -> None: if not isinstance(result_object, OperationResult): raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("backup", result_object) action = self._backup_action if result_object.outcome == "succeeded": self.backup_output.appendPlainText( @@ -5141,6 +5360,18 @@ def _run_console_arguments( approval = "" if profile.level == "read-only" else f"\n[safety approval: {profile.level}; acknowledgement accepted]" self.console_output.appendPlainText(f"\n[{title}]{approval}\n$ pymobiledevice3 {shlex.join(arguments)}\n") environment = base_environment() if device is None else device_environment(device.identifier) + history_context = ( + self._host_operation_context(title, "Command Center", "pymobiledevice3 host command", ()) + if device is None + else self._device_operation_context( + title, + "Command Center", + "pymobiledevice3 selected-device transport", + device, + (), + ) + ) + self._begin_operation("command-center", history_context) self._console_controller.start( self._pmd3, arguments, @@ -5166,6 +5397,7 @@ def _append_console_output(self, output: bytes) -> None: def _console_completed(self, result_object: object) -> None: if not isinstance(result_object, OperationResult): raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("command-center", result_object) exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) self.console_output.appendPlainText(f"\n[finished: {result_object.outcome}; exit {exit_label}]") if result_object.error_message: @@ -5381,6 +5613,15 @@ def refresh_selected_manpage(self) -> None: "Loading live help from the installed pymobiledevice3…\n\n" "This can take several seconds on the first Python import. Use Cancel Loading to stop immediately." ) + self._begin_operation( + "manpage", + self._host_operation_context( + f"Load Live Help: {entry.display_name()}", + "Man Pages", + "pymobiledevice3 host help route", + (), + ), + ) self._manpage_controller.start( finite_process_request( self._pmd3, @@ -5395,6 +5636,7 @@ def refresh_selected_manpage(self) -> None: def _manpage_completed(self, result_object: object) -> None: if not isinstance(result_object, OperationResult): raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("manpage", result_object) stdout = result_object.stdout.decode("utf-8", errors="replace") stderr = result_object.stderr.decode("utf-8", errors="replace") output = stdout or stderr diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 3cafaeb..6f5af91 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -80,10 +80,11 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise ValueError(f"Internal smoke test does not accept arguments: {tuple(arguments)}") os.environ["QT_QPA_PLATFORM"] = "offscreen" from PySide6.QtCore import SIGNAL - from PySide6.QtWidgets import QApplication, QLabel, QPushButton + from PySide6.QtWidgets import QApplication, QLabel, QPlainTextEdit, QPushButton, QTableWidget from ios_developer_toolkit.app import MainWindow from ios_developer_toolkit.backup_protocol import BackupRequest + from ios_developer_toolkit.operation_history import OperationHistoryDialog application = QApplication(["ios-developer-toolkit-smoke-test"]) window = MainWindow() @@ -221,7 +222,14 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError("Demo mode must disable live-device log collection") demo_mode_button.click() application.processEvents() - window._start_action(pymobiledevice3_command(), ("version",), {}, "smoke", 20_000) + window._start_action( + pymobiledevice3_command(), + ("version",), + {}, + "smoke", + 20_000, + window._host_operation_context("Smoke Device Action", "Device & DDI", "pymobiledevice3 host command", ()), + ) action_deadline = time.monotonic() + 20 while window._action_controller.is_running() and time.monotonic() < action_deadline: application.processEvents() @@ -253,6 +261,10 @@ def run_smoke_test(arguments: Sequence[str]) -> int: '"ApplicationType": "User"}}' ) window._apps_context = "inventory" + window._begin_operation( + "installed-apps", + window._host_operation_context("Smoke App Inventory", "Installed Apps", "synthetic smoke process", ()), + ) window._apps_controller.start( finite_process_request( ExecutableCommand(Path("/usr/bin/printf"), ()), @@ -288,6 +300,10 @@ def run_smoke_test(arguments: Sequence[str]) -> int: '"team_identifier":"SMOKETEAM","authorities":["Toolkit Smoke Authority"],' '"detail":"Synthetic smoke-test signature"}}' ) + window._begin_operation( + "ipa-inspection", + window._host_operation_context("Smoke IPA Inspection", "Sideload IPA", "synthetic smoke process", ()), + ) window._ipa_inspection_controller.start( finite_process_request( ExecutableCommand(Path("/usr/bin/printf"), ()), @@ -308,6 +324,10 @@ def run_smoke_test(arguments: Sequence[str]) -> int: if window._ipa_inspection is None or window._ipa_inspection.signature.status != "valid": raise RuntimeError(f"GUI IPA inspection controller rejected typed metadata: {window.sideload_status.text()}") window._sideload_context = "smoke" + window._begin_operation( + "sideload-ipa", + window._host_operation_context("Smoke IPA Operation", "Sideload IPA", "synthetic smoke process", ()), + ) window._sideload_controller.start( finite_process_request( ExecutableCommand(Path("/usr/bin/printf"), ()), @@ -335,6 +355,10 @@ def run_smoke_test(arguments: Sequence[str]) -> int: '\\"message\\":\\"Synthetic encryption status.\\",\\"encrypted\\":true}" }' ) window._backup_action = "status" + window._begin_operation( + "backup", + window._host_operation_context("Smoke Backup Status", "Backup", "synthetic smoke process", ()), + ) window._backup_controller.start( ExecutableCommand(Path("/usr/bin/awk"), (backup_smoke_program,)), "status", @@ -360,6 +384,15 @@ def run_smoke_test(arguments: Sequence[str]) -> int: '"status":"completed","failures":0}' ) window._collection_case_finished = False + window._begin_operation( + "evidence-collection", + window._host_operation_context( + "Smoke Evidence Collection", + "Evidence Capture", + "synthetic smoke process", + (), + ), + ) window._collection_controller.start( ExecutableCommand(Path("/usr/bin/printf"), ()), (synthetic_collection_event,), @@ -378,6 +411,19 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError(f"GUI evidence controller did not apply finalization: {window.collection_output.toPlainText()}") if "Collection process finished: succeeded; exit 0." not in window.collection_output.toPlainText(): raise RuntimeError(f"GUI evidence controller reported the wrong completion: {window.collection_output.toPlainText()}") + if len(window._operation_records) < 8: + raise RuntimeError(f"GUI session activity did not correlate typed operations: {len(window._operation_records)}") + activity_button = window.findChild(QPushButton, "sessionActivityButton") + if activity_button is None or f"({len(window._operation_records)})" not in activity_button.text(): + raise RuntimeError("GUI session activity count did not update after typed operations") + activity_dialog = OperationHistoryDialog(window._operation_records, window) + activity_table = activity_dialog.findChild(QTableWidget, "sessionActivityTable") + activity_preview = activity_dialog.findChild(QPlainTextEdit, "sessionActivityManifestPreview") + if activity_table is None or activity_table.rowCount() != len(window._operation_records): + raise RuntimeError("GUI session activity dialog did not render every typed operation") + if activity_preview is None or '"raw_output_included": false' not in activity_preview.toPlainText(): + raise RuntimeError("GUI session activity manifest preview did not preserve its raw-output boundary") + activity_dialog.close() window.close() application.processEvents() print(f"GUI smoke test passed with {len(buttons)} action buttons", flush=True) diff --git a/ios_developer_toolkit/operation_history.py b/ios_developer_toolkit/operation_history.py new file mode 100644 index 0000000..9963e64 --- /dev/null +++ b/ios_developer_toolkit/operation_history.py @@ -0,0 +1,347 @@ +from __future__ import annotations + +import hashlib +import json +import os +from dataclasses import dataclass, replace +from datetime import datetime +from pathlib import Path +from typing import TypeAlias + +from PySide6.QtCore import Qt +from PySide6.QtGui import QGuiApplication +from PySide6.QtWidgets import ( + QAbstractItemView, + QDialog, + QDialogButtonBox, + QFileDialog, + QHeaderView, + QLabel, + QMessageBox, + QPlainTextEdit, + QPushButton, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QWidget, +) + +from ios_developer_toolkit.qt_process import OperationResult, ProcessOutcome + + +JsonScalar: TypeAlias = str | int | bool | None +JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"] + + +class OperationHistoryError(ValueError): + """Raised when an operation record or explicit manifest export is invalid.""" + + +@dataclass(frozen=True) +class OperationContext: + title: str + workspace: str + target: str + transport: str + prerequisites: tuple[str, ...] + output_paths: tuple[str, ...] + + +@dataclass(frozen=True) +class OperationRecord: + identifier: str + title: str + workspace: str + target: str + transport: str + argv: tuple[str, ...] + started_at: str + finished_at: str + duration_milliseconds: int + outcome: ProcessOutcome + exit_code: int | None + error_message: str | None + stdout_bytes: int + stdout_sha256: str + stderr_bytes: int + stderr_sha256: str + prerequisites: tuple[str, ...] + output_paths: tuple[str, ...] + + +def operation_context( + title: str, + workspace: str, + target: str, + transport: str, + prerequisites: tuple[str, ...], + output_paths: tuple[str, ...], +) -> OperationContext: + required_values = { + "title": title.strip(), + "workspace": workspace.strip(), + "target": target.strip(), + "transport": transport.strip(), + } + empty_fields = tuple(name for name, value in required_values.items() if not value) + if empty_fields: + raise OperationHistoryError(f"Operation context fields cannot be empty: {', '.join(empty_fields)}") + normalized_prerequisites = tuple(value.strip() for value in prerequisites) + if any(not value for value in normalized_prerequisites): + raise OperationHistoryError("Operation prerequisites cannot contain empty values") + normalized_paths = _normalized_output_paths(output_paths) + return OperationContext( + required_values["title"], + required_values["workspace"], + required_values["target"], + required_values["transport"], + normalized_prerequisites, + normalized_paths, + ) + + +def with_output_paths(context: OperationContext, output_paths: tuple[str, ...]) -> OperationContext: + normalized_paths = _normalized_output_paths(output_paths) + return replace(context, output_paths=normalized_paths) + + +def operation_record(context: OperationContext, result: OperationResult) -> OperationRecord: + if not result.argv or not result.argv[0]: + raise OperationHistoryError("Operation result must contain a non-empty argument vector") + started = _parse_timestamp(result.started_at, "started_at") + finished = _parse_timestamp(result.finished_at, "finished_at") + duration_milliseconds = round((finished - started).total_seconds() * 1000) + if duration_milliseconds < 0: + raise OperationHistoryError("Operation finish time cannot precede its start time") + identity_payload = "\0".join( + (context.workspace, context.title, result.started_at, result.finished_at, *result.argv) + ).encode("utf-8") + identifier = hashlib.sha256(identity_payload).hexdigest()[:16] + return OperationRecord( + identifier, + context.title, + context.workspace, + context.target, + context.transport, + result.argv, + result.started_at, + result.finished_at, + duration_milliseconds, + result.outcome, + result.exit_code, + result.error_message, + len(result.stdout), + hashlib.sha256(result.stdout).hexdigest(), + len(result.stderr), + hashlib.sha256(result.stderr).hexdigest(), + context.prerequisites, + context.output_paths, + ) + + +def append_operation_record( + records: tuple[OperationRecord, ...], + record: OperationRecord, + maximum_records: int, +) -> tuple[OperationRecord, ...]: + if maximum_records <= 0: + raise OperationHistoryError(f"Operation history limit must be positive: {maximum_records}") + if any(existing.identifier == record.identifier for existing in records): + raise OperationHistoryError(f"Operation record identifier is duplicated: {record.identifier}") + return (*records, record)[-maximum_records:] + + +def operation_manifest(record: OperationRecord) -> dict[str, JsonValue]: + return { + "schema_version": 1, + "operation_id": record.identifier, + "title": record.title, + "workspace": record.workspace, + "target": record.target, + "transport": record.transport, + "argv": list(record.argv), + "timing": { + "started_at": record.started_at, + "finished_at": record.finished_at, + "duration_milliseconds": record.duration_milliseconds, + }, + "result": { + "outcome": record.outcome, + "exit_code": record.exit_code, + "error_message": record.error_message, + }, + "captured_output": { + "stdout_bytes": record.stdout_bytes, + "stdout_sha256": record.stdout_sha256, + "stderr_bytes": record.stderr_bytes, + "stderr_sha256": record.stderr_sha256, + "raw_output_included": False, + }, + "prerequisites": list(record.prerequisites), + "output_paths": list(record.output_paths), + "privacy_notice": ( + "This user-exported manifest omits raw command output but may contain device identifiers, " + "local paths, and other sensitive values from the exact argument vector and target label." + ), + } + + +def render_operation_manifest(record: OperationRecord) -> str: + return json.dumps(operation_manifest(record), indent=2, sort_keys=True) + "\n" + + +def write_operation_manifest(path: Path, record: OperationRecord) -> Path: + destination = path.expanduser().resolve() + if destination.suffix.casefold() != ".json": + raise OperationHistoryError(f"Operation manifest destination must end in .json: {destination}") + if not destination.parent.is_dir(): + raise OperationHistoryError(f"Operation manifest parent directory does not exist: {destination.parent}") + try: + descriptor = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + except FileExistsError as error: + raise OperationHistoryError(f"Refusing to overwrite existing operation manifest: {destination}") from error + except OSError as error: + raise OperationHistoryError(f"Could not create operation manifest at {destination}: {error}") from error + try: + payload = render_operation_manifest(record).encode("utf-8") + with os.fdopen(descriptor, "wb") as stream: + stream.write(payload) + stream.flush() + os.fsync(stream.fileno()) + except OSError as error: + destination.unlink(missing_ok=True) + raise OperationHistoryError(f"Could not write operation manifest at {destination}: {error}") from error + return destination + + +class OperationHistoryDialog(QDialog): + """Present session-only operation records and explicit manifest export controls.""" + + def __init__(self, records: tuple[OperationRecord, ...], parent: QWidget | None) -> None: + super().__init__(parent) + self._records = records + self.setObjectName("sessionActivityDialog") + self.setWindowTitle("Session Activity") + self.resize(980, 680) + layout = QVBoxLayout(self) + explanation = QLabel( + "Completed typed operations from this app session appear here. Nothing is saved automatically. " + "An exported JSON manifest omits raw output but can contain device identifiers and local paths." + ) + explanation.setWordWrap(True) + layout.addWidget(explanation) + + self.table = QTableWidget(len(records), 5) + self.table.setObjectName("sessionActivityTable") + self.table.setHorizontalHeaderLabels(("Finished", "Workspace", "Operation", "Target", "Outcome")) + self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.table.setAlternatingRowColors(True) + self.table.verticalHeader().setVisible(False) + for row, record in enumerate(reversed(records)): + values = (record.finished_at, record.workspace, record.title, record.target, record.outcome) + for column, value in enumerate(values): + item = QTableWidgetItem(value) + item.setData(Qt.ItemDataRole.UserRole, record.identifier) + self.table.setItem(row, column, item) + header = self.table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents) + self.table.itemSelectionChanged.connect(self._selection_changed) + layout.addWidget(self.table, 1) + + self.detail = QPlainTextEdit() + self.detail.setObjectName("sessionActivityManifestPreview") + self.detail.setReadOnly(True) + self.detail.setMaximumBlockCount(4000) + layout.addWidget(self.detail, 1) + + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) + self.copy_button = QPushButton("Copy Selected Manifest") + self.copy_button.setObjectName("copySessionActivityManifestButton") + self.copy_button.clicked.connect(self._copy_selected) + buttons.addButton(self.copy_button, QDialogButtonBox.ButtonRole.ActionRole) + self.save_button = QPushButton("Save Selected Manifest…") + self.save_button.setObjectName("saveSessionActivityManifestButton") + self.save_button.clicked.connect(self._save_selected) + buttons.addButton(self.save_button, QDialogButtonBox.ButtonRole.ActionRole) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + if records: + self.table.selectRow(0) + else: + self.detail.setPlainText("No typed operations have completed in this session.") + self.copy_button.setEnabled(False) + self.save_button.setEnabled(False) + + def _selected_record(self) -> OperationRecord: + selected_items = self.table.selectedItems() + if not selected_items: + raise OperationHistoryError("Select an operation before copying or saving its manifest") + identifier = selected_items[0].data(Qt.ItemDataRole.UserRole) + if not isinstance(identifier, str): + raise OperationHistoryError("Selected operation has no valid record identifier") + record = next((candidate for candidate in self._records if candidate.identifier == identifier), None) + if record is None: + raise OperationHistoryError(f"Selected operation record is unavailable: {identifier}") + return record + + def _selection_changed(self) -> None: + try: + record = self._selected_record() + except OperationHistoryError: + self.detail.clear() + self.copy_button.setEnabled(False) + self.save_button.setEnabled(False) + return + self.detail.setPlainText(render_operation_manifest(record)) + self.copy_button.setEnabled(True) + self.save_button.setEnabled(True) + + def _copy_selected(self) -> None: + try: + record = self._selected_record() + except OperationHistoryError as error: + QMessageBox.warning(self, "No Operation Selected", str(error)) + return + QGuiApplication.clipboard().setText(render_operation_manifest(record)) + + def _save_selected(self) -> None: + try: + record = self._selected_record() + except OperationHistoryError as error: + QMessageBox.warning(self, "No Operation Selected", str(error)) + return + suggested = str(Path.home() / f"ios-toolkit-operation-{record.identifier}.json") + selected, _ = QFileDialog.getSaveFileName(self, "Save operation manifest", suggested, "JSON (*.json)") + if not selected: + return + try: + destination = write_operation_manifest(Path(selected), record) + except OperationHistoryError as error: + QMessageBox.critical(self, "Could Not Save Manifest", str(error)) + return + QMessageBox.information(self, "Manifest Saved", f"Saved operation manifest to:\n{destination}") + + +def _parse_timestamp(value: str, field_name: str) -> datetime: + try: + parsed = datetime.fromisoformat(value) + except ValueError as error: + raise OperationHistoryError(f"Operation {field_name} is not a valid ISO-8601 timestamp: {value}") from error + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise OperationHistoryError(f"Operation {field_name} must include a timezone: {value}") + return parsed + + +def _normalized_output_paths(output_paths: tuple[str, ...]) -> tuple[str, ...]: + normalized: list[str] = [] + for value in output_paths: + if not value.strip(): + raise OperationHistoryError("Operation output paths cannot contain empty values") + normalized.append(str(Path(value).expanduser().resolve())) + return tuple(normalized) diff --git a/tests/test_operation_history.py b/tests/test_operation_history.py new file mode 100644 index 0000000..8d3ee7d --- /dev/null +++ b/tests/test_operation_history.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import json +import shutil +import stat +import tempfile +import unittest +from pathlib import Path + +from ios_developer_toolkit.operation_history import ( + OperationHistoryError, + append_operation_record, + operation_context, + operation_manifest, + operation_record, + write_operation_manifest, +) +from ios_developer_toolkit.qt_process import OperationResult + + +class OperationHistoryTests(unittest.TestCase): + def setUp(self) -> None: + self.context = operation_context( + "Inspect device", + "Command Center", + "Selected iPhone", + "pymobiledevice3", + ("device:selected", "trust:ready"), + ("/tmp/output.txt",), + ) + self.result = OperationResult( + ("/tool/pymobiledevice3", "lockdown", "info", "--udid", "PRIVATE-DEVICE-ID"), + "succeeded", + "2026-09-22T12:00:00+00:00", + "2026-09-22T12:00:01.250000+00:00", + 0, + None, + b"private stdout", + b"diagnostic stderr", + ) + + def test_builds_typed_record_with_timing_and_output_digests(self) -> None: + record = operation_record(self.context, self.result) + + self.assertEqual(record.duration_milliseconds, 1250) + self.assertEqual(record.stdout_bytes, len(b"private stdout")) + self.assertEqual(len(record.stdout_sha256), 64) + self.assertEqual(record.argv[-1], "PRIVATE-DEVICE-ID") + self.assertEqual(record.output_paths, (str(Path("/tmp/output.txt").resolve()),)) + + def test_manifest_omits_raw_output_but_retains_exact_argument_vector(self) -> None: + manifest = operation_manifest(operation_record(self.context, self.result)) + payload = json.dumps(manifest) + + self.assertNotIn("private stdout", payload) + self.assertNotIn("diagnostic stderr", payload) + self.assertIn("PRIVATE-DEVICE-ID", payload) + self.assertFalse(manifest["captured_output"]["raw_output_included"]) + + def test_append_is_bounded_and_rejects_duplicate_records(self) -> None: + first = operation_record(self.context, self.result) + second_result = OperationResult( + self.result.argv, + "failed", + "2026-09-22T12:01:00+00:00", + "2026-09-22T12:01:02+00:00", + 1, + "failed", + b"", + b"failed", + ) + second = operation_record(self.context, second_result) + + self.assertEqual(append_operation_record((first,), second, 1), (second,)) + with self.assertRaises(OperationHistoryError): + append_operation_record((first,), first, 10) + + def test_writes_private_manifest_without_overwriting(self) -> None: + temporary_directory = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, temporary_directory) + destination = temporary_directory / "operation.json" + record = operation_record(self.context, self.result) + + self.assertEqual(write_operation_manifest(destination, record), destination.resolve()) + self.assertEqual(stat.S_IMODE(destination.stat().st_mode), 0o600) + with self.assertRaises(OperationHistoryError): + write_operation_manifest(destination, record) + + +if __name__ == "__main__": + unittest.main() From 3d62143e69af25161b46c8eb2ab001c0cc250848 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:06:56 -0700 Subject: [PATCH 10/16] Add eligible action palette --- README.md | 20 ++- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- docs/screenshots/action-palette.png | Bin 0 -> 30944 bytes ios_developer_toolkit/action_palette.py | 165 ++++++++++++++++++ ios_developer_toolkit/app.py | 221 +++++++++++++++++++++++- ios_developer_toolkit/entrypoint.py | 29 +++- tests/test_action_palette.py | 62 +++++++ 7 files changed, 493 insertions(+), 7 deletions(-) create mode 100644 docs/screenshots/action-palette.png create mode 100644 ios_developer_toolkit/action_palette.py create mode 100644 tests/test_action_palette.py diff --git a/README.md b/README.md index 7309285..ab06b24 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ The screenshots use an illustrative device name, model, version, build, and UDID - [Evidence Capture](#evidence-capture) - [Man Pages](#man-pages) - [Scope and Safety](#scope-and-safety) + - [Eligible Action Palette](#eligible-action-palette) - [Session Activity and operation manifests](#session-activity-and-operation-manifests) - [Developer Disk Images explained](#developer-disk-images-explained) - [Guided command catalog](#guided-command-catalog) @@ -85,6 +86,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - **Retry Scan** performs an immediate usbmux device check, while **Reconnect & Retry…** opens a guided detection window without attempting to restart SIP-protected Apple services; - **Connection diagnostic** records whether usbmux did not launch, failed, returned malformed output, found no devices, or returned selectable devices; its privacy-safe summary is visible in Device & DDI and included in a sanitized support bundle; - **Selected command readiness** maps each guided Command Center action to the exact connection, trust, Developer Mode, DDI, tunnel, CoreDevice, DVT, or Web Inspector checks it needs, with a one-click route to the bounded read-only matrix; +- **Action Palette** (`⌘ K`) searches all workspaces, guided presets, utilities, and currently eligible read actions while withholding device-only operations until a physical target is selected; - **Session Activity** correlates completed typed operations with workspace, target, transport, exact argument vector, timing, terminal status, prerequisite snapshot, output paths, and output hashes without automatically persisting raw command output; - the desktop UI starts independently of the MobileBackup2 transport, and device discovery consumes output both while the child process runs and after it exits, so a fast successful `usbmux list` result is not lost before the picker is updated; - **Demo Mode** shows a prominently labeled simulated iPhone for walkthroughs and screenshots, while deliberately withholding a selected physical-device target and disabling device operations; @@ -93,10 +95,10 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one selected-device workflow; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 113 tests, embedded CLI checks, a 95-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 116 tests, embedded CLI checks, a 96-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. -The README contains 19 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. +The README contains 20 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. | Prepare the device and DDI | Observe live services | Run guided commands | |---|---|---| @@ -119,6 +121,7 @@ The README contains 19 sanitized screenshots. The six views below provide a quic | Keyboard-first access | Adds named controls, standard navigation, and application-wide workspace shortcuts. | Shortcuts never bypass action confirmation. | | Sanitized Support Bundle | Creates a reviewable local ZIP with environment/readiness summaries and a SHA-256 manifest. | Excludes device identity, captures, backups, command output, credentials, and common host/network identifiers. | | Demo Mode | Shows a local simulated iPhone for an honest product walkthrough or screenshot. | The banner identifies the simulation and no device service, command, mount, capture, backup, or location operation can run. | +| Eligible Action Palette | Searches workspaces, utilities, guided presets, and read actions that are valid for the current device and process state. | Selecting a preset opens it for review; it never runs automatically or bypasses confirmation. | | Session Activity | Correlates completed typed operations and previews an exportable structured JSON manifest. | Session-only by default; raw output is omitted, and explicit exports can still contain identifiers and local paths. | ## What the workbench covers @@ -142,7 +145,15 @@ The README contains 19 sanitized screenshots. The six views below provide a quic The interface gives named controls and descriptions to the primary device picker, workspace navigation, command and help browsers, app inventory, capability results, reports, and the keyboard alternative to Location Lab's mouse map. The offline map is intentionally skipped in keyboard tab order; use the coordinate importer or latitude and longitude fields instead. -Use **Keyboard Shortcuts** in the window header, or press `⌘ /`, for the complete reference. The most useful shortcuts are `⌘ L` to focus workspace navigation, `⌘ F` to focus contextual search, `⌘ R` to retry discovery, `⌘ 1` through `⌘ 0` to open the first ten workspaces, `⌘ ⇧ M` for Man Pages, and `⌘ ⇧ S` for Scope & Safety. `⌘ ⌥ ←` and `⌘ ⌥ →` move between workspaces. Tab, Shift-Tab, Space, Return, and Arrow keys retain their standard Qt behavior. Shortcuts never skip device-action confirmation or typed acknowledgements. +Use **Keyboard Shortcuts** in the window header, or press `⌘ /`, for the complete reference. The most useful shortcuts are `⌘ K` to open the eligible Action Palette, `⌘ L` to focus workspace navigation, `⌘ F` to focus contextual search, `⌘ R` to retry discovery, `⌘ 1` through `⌘ 0` to open the first ten workspaces, `⌘ ⇧ M` for Man Pages, and `⌘ ⇧ S` for Scope & Safety. `⌘ ⌥ ←` and `⌘ ⌥ →` move between workspaces. Tab, Shift-Tab, Space, Return, and Arrow keys retain their standard Qt behavior. Shortcuts never skip device-action confirmation or typed acknowledgements. + +### Eligible Action Palette + +![Eligible Action Palette](docs/screenshots/action-palette.png) + +Press `⌘ K` or use **Action Palette** below the workspace list to search the interface without memorizing where an operation lives. The result set is computed from the current app state: host-only presets remain available while disconnected, device-only presets appear only after a physical target is selected, and actions disappear while their process controller is busy. Workspace and utility navigation is always available. + +Choosing a guided preset opens Command Center with that preset selected and its exact command, prerequisites, risk, and confirmation path visible. It does not execute the command. Direct entries are limited to eligible read actions such as discovery, Developer Mode status, developer-image listing, CoreDevice or RVI details, the Capability Matrix, app inventory, backup-encryption status, Command Drift, and Man Pages help. State is checked again at activation so a device disconnect or newly busy controller cannot use a stale palette result. ### Session Activity and operation manifests @@ -1023,6 +1034,7 @@ Install or update Xcode if the candidate is absent. The toolkit requires the exp . ├── ios_developer_toolkit/ │ ├── app.py # PySide6 workbench and workflow orchestration +│ ├── action_palette.py # searchable state-eligible navigation and actions │ ├── action_safety.py # typed confirmation policy for state-changing actions │ ├── backup_process.py # password-safe backup-worker lifecycle controller │ ├── backup_protocol.py # dependency-free backup request/event schema @@ -1079,7 +1091,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 113 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 95-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 116 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 96-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index cc5b4dc..7ce76c6 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -121,7 +121,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P2 — deepen expert workflows without scope creep -* Maintain the session-local typed-operation journal and explicit structured JSON manifests. Add a universal command/action palette that only exposes eligible operations. +* Maintain the session-local typed-operation journal, explicit structured JSON manifests, and universal Action Palette that exposes only eligible operations. * Implement a guided MVT backup-analysis handoff with explicit consent, no password persistence, output isolation, and no “clean device” conclusion. * Add optional user-configured adapters for `go-ios`, `idb`, and `ipsw`, each with executable provenance and version display. * Publish a small documentation site split into quick start, architecture, safety, troubleshooting, release verification, and contributor paths. @@ -177,6 +177,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Added native Apple developer-tool handoffs for selected-device CoreDevice details, RVI status, Xcode projects, test results, and Instruments traces. | 106 tests and a 94-action GUI smoke passed; tests validate the exact selected-device and RVI commands and reject missing or unrelated local targets. | The toolkit displays native output and opens native formats; it does not claim a stable schema for human `devicectl` output or reimplement Xcode. | | 2026-09-22 | Migrated Command Center guided, advanced, finite, and streaming commands to a typed interactive-process lifecycle. | 109 tests and the 94-action GUI smoke passed; real child-process tests cover final stdout/stderr draining, launch failure, idempotent cancellation, and no arbitrary runtime limit. | Command output remains session-local unless the user explicitly preserves it through a task-specific evidence workflow. | | 2026-09-22 | Added a session-local operation journal and explicit per-operation JSON manifests across the primary typed workflows. | 113 tests and a 95-action GUI smoke passed; tests cover immutable bounded history, exact argument retention, output hashing without raw-output embedding, owner-only export, and overwrite refusal. | Capability Matrix, Location Lab, Live Logs, and Command Drift retain their stronger workflow-specific records rather than duplicating raw or high-volume events into this journal. | +| 2026-09-22 | Added a keyboard-first Action Palette computed from current device and process eligibility. | 116 tests and a 96-action GUI smoke passed; smoke coverage verifies disconnected-state preset filtering, host-preset access, search behavior, stable control identity, and the `⌘ K` shortcut. | Guided presets are selected for review rather than executed, and eligibility is checked again at activation. | ## Research sources diff --git a/docs/screenshots/action-palette.png b/docs/screenshots/action-palette.png new file mode 100644 index 0000000000000000000000000000000000000000..4527429c350239539887caae655e15db8ae2caaf GIT binary patch literal 30944 zcma&O1yEd3lr7po0s(?s@F2k@xJz({5Zv7@xJ!UwA-H?6;O-hUxVr@i?(XmonVDDf zf6agIS9Mo+b#-4kXUp1auiYUEa^gq`cnA;(1W8gtL=ggk9tF==ub{yvzt!YIzze*c zgt`L+g534|go>j@#)m-OLL@~#D7mKYFBxkospCVAO~2sc!SAp~DhZ*AK*4D4Yd0an zG?imCIg~gZ=vX4{BP+e`%4^CG3c2d+o|{YJ`EXEpboO)*G{>`*ezxc^;)+s7B=z|- z)7t=4p;uEByFP>vp;sgU-rk(6g5Xu5q!S+Q`2!0=IZp5m5~V%64nOcFOCtya`QLB2 zQ|+FA{BwFW0EPs-sX?YB06#!2boxIW;^YYJRX{@75)C6`S=79U!O0L0N`TO+50<>M z+;UhK$NXeWdep`Bj)MYP)g`{-hHue|DmZANWD$Iju0#{XuPE6sWGR6zxLn?1VkXYk zK^GIK2!RVO#8ieAkhg4nhRe~)Q#*V&^VhuIgXcGN?BXd{j5-aEr&F4Nfm7eM9<%fD zYY(UM(h(EZXKa>pX-p0{aulHkZ0&PzcLm7XeA?=qC4<0yn#+STiqI_8xNW2r5ww9`fM|&&>m6mO^55 z@^VG5;F@NZ!kf7oTOIEY-XHXhW?DVG1<;qXT-vhMZI<0KwWjVOGQ_`Upb?B2AODnu zXVig$R7`IYdZg6tVEdev3f*AjGoEkr{19C@oG?tNoFh=bA)|bJZM#zBRR`rmz$F@c zf4npB`-J6-rV}9s3oSk0Cly66UejUS!&yT;1+}fHbyG}CY-Klk2&dbnK}0K=kcPNy zQq9lmZ{1snKfG~aA^zgB^FGI}A3sap**tJ*5t&n>7L6n}S&e^tNm7!X*UK<8*dJy4 zF{?Ion9Cu{DPMQq`R3y9Tv#D=O4a+rowKf$OJX8!lTgpV(-S+lW-i1vaEh9P)1qR5 zjWk}5iiNUHGVV*AR`)73x{t~uRa<4SuQO0Fr`;~^vA{j0^ZDRH9-HlqhdVgcZ_W}* zYj$VzWw&%)4*AqJzD{0Y?NHEO#hv

    BW7f^PTgq+Fl2A!!Y7b8}g%cFSb-l}~Ts zv>uKg!!U)d-j9J06@m6s=Cq)u!*?Y>$Ll1GVKhtfKU9NVo@W_UxouPiz9@sdckV&(&w7yxGX5-g6j4`33qNAah zmU6%P+*v3J-jg!jdYPeAh38=<^_zrDqg|uP)*{b?{ zu{L@BZ9RWvfTK_Wzv5VQE#-n1r`8s~!f`_kIY=6BHh`&facS3OZ_)eXzS0eX@d>0o zWCAC8*@w5hS=e}$M$?UkFuGBp>C8-rI2nE@*B5) z9!yBTT5ndi_tu3pG<8$s3><-7La6)X^OLcf{H*|VXk7Svd11uD{MWEh0i7++~2n!MH=h zXKXUHyR1G5Z{fR7b`_A8Xm)r)i|M1HqD#i)`y!Pqm&Vx$%)YA9feN$m)^%4SQK0#= z98H`(=ORQm15O^jdJbW{rgWuorb4?Y{Uafi-q7g{F~4`7V!?%0Dx`gGYjgA=VN#6%NPD}kM>^QK!DGMv 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 literal 0 HcmV?d00001 diff --git a/ios_developer_toolkit/action_palette.py b/ios_developer_toolkit/action_palette.py new file mode 100644 index 0000000..f1d7027 --- /dev/null +++ b/ios_developer_toolkit/action_palette.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QLabel, + QLineEdit, + QListWidget, + QListWidgetItem, + QVBoxLayout, + QWidget, +) + + +class ActionPaletteError(ValueError): + """Raised when an eligible action palette cannot be represented safely.""" + + +@dataclass(frozen=True) +class ActionPaletteEntry: + identifier: str + title: str + category: str + summary: str + keywords: tuple[str, ...] + + +def action_palette_entry( + identifier: str, + title: str, + category: str, + summary: str, + keywords: tuple[str, ...], +) -> ActionPaletteEntry: + normalized_values = tuple(value.strip() for value in (identifier, title, category, summary)) + if any(not value for value in normalized_values): + raise ActionPaletteError("Action palette identifier, title, category, and summary must be non-empty") + normalized_keywords = tuple(keyword.strip() for keyword in keywords) + if any(not keyword for keyword in normalized_keywords): + raise ActionPaletteError("Action palette keywords cannot contain empty values") + return ActionPaletteEntry(*normalized_values, normalized_keywords) + + +def validate_action_palette(entries: tuple[ActionPaletteEntry, ...]) -> tuple[ActionPaletteEntry, ...]: + identifiers = tuple(entry.identifier for entry in entries) + if len(set(identifiers)) != len(identifiers): + duplicates = sorted(identifier for identifier in set(identifiers) if identifiers.count(identifier) > 1) + raise ActionPaletteError(f"Action palette identifiers must be unique: {', '.join(duplicates)}") + return entries + + +def filter_action_palette( + entries: tuple[ActionPaletteEntry, ...], + query: str, +) -> tuple[ActionPaletteEntry, ...]: + terms = tuple(term for term in query.strip().casefold().split() if term) + matching: list[tuple[int, str, str, ActionPaletteEntry]] = [] + for entry in entries: + title = entry.title.casefold() + haystack = " ".join((entry.title, entry.category, entry.summary, *entry.keywords)).casefold() + if not all(term in haystack for term in terms): + continue + rank = 0 if not terms or title.startswith(terms[0]) else 1 if any(term in title for term in terms) else 2 + matching.append((rank, entry.category.casefold(), title, entry)) + return tuple(item[3] for item in sorted(matching, key=lambda item: item[:3])) + + +class ActionPaletteDialog(QDialog): + """Search and return one action from the caller-provided eligible set.""" + + def __init__(self, entries: tuple[ActionPaletteEntry, ...], parent: QWidget | None) -> None: + super().__init__(parent) + self._entries = validate_action_palette(entries) + self._selected_identifier: str | None = None + self.setObjectName("actionPaletteDialog") + self.setWindowTitle("Action Palette") + self.resize(720, 520) + layout = QVBoxLayout(self) + heading = QLabel("Run or open an action that is eligible in the current app state") + heading.setWordWrap(True) + layout.addWidget(heading) + self.search = QLineEdit() + self.search.setObjectName("actionPaletteSearch") + self.search.setPlaceholderText("Search workspaces, commands, diagnostics, and utilities") + self.search.setAccessibleName("Search eligible actions") + self.search.textChanged.connect(self._filter_entries) + self.search.returnPressed.connect(self._accept_current) + layout.addWidget(self.search) + self.results = QListWidget() + self.results.setObjectName("actionPaletteResults") + self.results.setAccessibleName("Eligible action results") + self.results.currentItemChanged.connect(self._selection_changed) + self.results.itemActivated.connect(self._item_activated) + layout.addWidget(self.results, 1) + self.summary = QLabel() + self.summary.setObjectName("actionPaletteSummary") + self.summary.setWordWrap(True) + layout.addWidget(self.summary) + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Open | QDialogButtonBox.StandardButton.Cancel) + buttons.setObjectName("actionPaletteButtons") + open_button = buttons.button(QDialogButtonBox.StandardButton.Open) + cancel_button = buttons.button(QDialogButtonBox.StandardButton.Cancel) + open_button.setObjectName("actionPaletteOpenButton") + cancel_button.setObjectName("actionPaletteCancelButton") + open_button.setAccessibleName("Open selected eligible action") + cancel_button.setAccessibleName("Close action palette") + buttons.accepted.connect(self._accept_current) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + self._open_button = open_button + self._filter_entries() + self.search.setFocus(Qt.FocusReason.ShortcutFocusReason) + + def selected_identifier(self) -> str: + if self._selected_identifier is None: + raise ActionPaletteError("Action palette closed without selecting an eligible action") + return self._selected_identifier + + def _filter_entries(self) -> None: + matching = filter_action_palette(self._entries, self.search.text()) + self.results.blockSignals(True) + self.results.clear() + for entry in matching: + item = QListWidgetItem(f"{entry.title} · {entry.category}") + item.setData(Qt.ItemDataRole.UserRole, entry.identifier) + item.setToolTip(entry.summary) + self.results.addItem(item) + if matching: + self.results.setCurrentRow(0) + self.summary.setText(matching[0].summary) + else: + self.summary.setText("No eligible action matches this search in the current app state.") + self._open_button.setEnabled(bool(matching)) + self.results.blockSignals(False) + + def _selection_changed(self, current: QListWidgetItem | None, previous: QListWidgetItem | None) -> None: + del previous + if current is None: + self._open_button.setEnabled(False) + return + identifier = current.data(Qt.ItemDataRole.UserRole) + if not isinstance(identifier, str): + raise ActionPaletteError("Selected action palette row has no string identifier") + entry = next((candidate for candidate in self._entries if candidate.identifier == identifier), None) + if entry is None: + raise ActionPaletteError(f"Selected action palette entry is unavailable: {identifier}") + self.summary.setText(entry.summary) + self._open_button.setEnabled(True) + + def _item_activated(self, item: QListWidgetItem) -> None: + self.results.setCurrentItem(item) + self._accept_current() + + def _accept_current(self) -> None: + current = self.results.currentItem() + if current is None: + return + identifier = current.data(Qt.ItemDataRole.UserRole) + if not isinstance(identifier, str): + raise ActionPaletteError("Selected action palette row has no string identifier") + self._selected_identifier = identifier + self.accept() diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index 177c030..1365731 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -70,6 +70,12 @@ confirmation_phrase, guided_action_safety, ) +from ios_developer_toolkit.action_palette import ( + ActionPaletteDialog, + ActionPaletteEntry, + action_palette_entry, + validate_action_palette, +) from ios_developer_toolkit.backup_process import BackupProcessController from ios_developer_toolkit.backup_protocol import BackupAction, BackupEvent, BackupRequest, BackupRequestError from ios_developer_toolkit.case_workflow import CaseWorkflowError, create_guided_case @@ -724,6 +730,11 @@ def _build_ui(self) -> None: self.navigation_list.setObjectName("workspaceNavigation") self.navigation_list.setSpacing(2) sidebar_layout.addWidget(self.navigation_list, 1) + self.action_palette_button = QPushButton("Action Palette (⌘K)") + self.action_palette_button.setObjectName("actionPaletteButton") + self.action_palette_button.setToolTip("Search workspaces, guided commands, and currently eligible actions") + self.action_palette_button.clicked.connect(self.show_action_palette) + sidebar_layout.addWidget(self.action_palette_button) self.session_activity_button = QPushButton("Session Activity (0)") self.session_activity_button.setObjectName("sessionActivityButton") self.session_activity_button.setToolTip( @@ -791,6 +802,10 @@ def _configure_accessibility(self) -> None: self.session_activity_button.setAccessibleDescription( "Review completed typed operations and explicitly export a selected structured manifest." ) + self.action_palette_button.setAccessibleName("Action palette") + self.action_palette_button.setAccessibleDescription( + "Search workspaces, guided commands, and actions eligible in the current app state. Shortcut: Command K." + ) self.connection_banner.setAccessibleName("Device connection status") self.connection_banner.setAccessibleDescription( "Reports whether a trusted iPhone or iPad is currently available to the toolkit." @@ -839,10 +854,12 @@ def _configure_accessibility(self) -> None: QWidget.setTabOrder(self.reconnect_device_button, self.keyboard_shortcuts_button) QWidget.setTabOrder(self.keyboard_shortcuts_button, self.support_bundle_button) QWidget.setTabOrder(self.support_bundle_button, self.navigation_list) - QWidget.setTabOrder(self.navigation_list, self.session_activity_button) + QWidget.setTabOrder(self.navigation_list, self.action_palette_button) + QWidget.setTabOrder(self.action_palette_button, self.session_activity_button) def _configure_keyboard_shortcuts(self) -> None: self._add_application_shortcut("Meta+R", self._scanner_scan, "shortcutRetryDeviceScan") + self._add_application_shortcut("Meta+K", self.show_action_palette, "shortcutShowActionPalette") self._add_application_shortcut("Meta+L", self.focus_workspace_navigation, "shortcutFocusWorkspaceNavigation") self._add_application_shortcut("Meta+F", self.focus_workspace_search, "shortcutFocusWorkspaceSearch") self._add_application_shortcut("Meta+/", self.show_keyboard_shortcuts, "shortcutShowKeyboardReference") @@ -957,6 +974,7 @@ def show_keyboard_shortcuts(self) -> None: "" "" "" + "" "" "" "" @@ -979,6 +997,207 @@ def show_keyboard_shortcuts(self) -> None: layout.addWidget(buttons) dialog.exec() + def show_action_palette(self) -> None: + dialog = ActionPaletteDialog(self._eligible_action_palette_entries(), self) + if dialog.exec() != QDialog.DialogCode.Accepted: + return + self._execute_action_palette_entry(dialog.selected_identifier()) + + def _eligible_action_palette_entries(self) -> tuple[ActionPaletteEntry, ...]: + workspace_summaries = { + "Home": "Open the guided workflow overview.", + "Device & DDI": "Review the selected device, Developer Mode, DDI, and Apple tool handoffs.", + "Capability Matrix": "Inspect bounded connection and developer-service readiness evidence.", + "Location Lab": "Prepare explicit, clearable location simulation for app testing.", + "Live Logs": "Open independent raw-spooling log windows.", + "Command Center": "Choose a validated guided command or explicit advanced arguments.", + "Installed Apps": "Inspect the service-visible app inventory.", + "Backup": "Prepare MobileBackup2 or an external UFADE handoff.", + "Sideload IPA": "Inspect a local IPA before an eligible installation attempt.", + "Evidence Capture": "Prepare a scoped case and bounded evidence collection.", + "Man Pages": "Browse version-matched command routes and live help.", + "Scope & Safety": "Review authorization, privacy, and interpretation boundaries.", + } + entries: list[ActionPaletteEntry] = [ + action_palette_entry( + f"navigate:{workspace}", + f"Open {workspace}", + "Workspace", + workspace_summaries[workspace], + ("navigate", "workspace", workspace), + ) + for workspace in self._page_indices + ] + entries.extend( + ( + action_palette_entry( + "utility:session-activity", + "Open Session Activity", + "Utility", + "Review completed typed operations and explicitly export a selected JSON manifest.", + ("history", "journal", "manifest", "operations"), + ), + action_palette_entry( + "utility:keyboard-shortcuts", + "Open Keyboard Shortcuts", + "Utility", + "Review keyboard-first navigation without running a device action.", + ("accessibility", "keyboard", "hotkeys"), + ), + ) + ) + if self.refresh_devices_button.isEnabled() and not self._demo_mode: + entries.append( + action_palette_entry( + "action:retry-device-scan", + "Retry Device Scan", + "Eligible read action", + "Run one usbmux discovery refresh without restarting macOS services.", + ("connect", "detect", "usbmux", "iphone", "ipad"), + ) + ) + eligible_actions = ( + ( + "action:developer-mode-status", + "Check Developer Mode", + "Query the selected device's current Developer Mode status.", + ("developer", "amfi", "ddi"), + self.selected_device() is not None and not self._action_controller.is_running(), + ), + ( + "action:list-developer-images", + "List Developer Images", + "List mounted or installed developer support for the selected device.", + ("ddi", "mounter", "cryptex"), + self.selected_device() is not None and not self._action_controller.is_running(), + ), + ( + "action:coredevice-details", + "Show CoreDevice Details", + "Run bounded Apple devicectl details for the selected device.", + ("xcode", "devicectl", "coredevice"), + self.coredevice_details_button.isEnabled(), + ), + ( + "action:rvi-status", + "List RVI Interfaces", + "List current Apple Remote Virtual Interfaces without changing them.", + ("network", "pcap", "rvictl"), + self.rvi_status_button.isEnabled(), + ), + ( + "action:capability-matrix", + "Run Device Readiness Check", + "Run the bounded read-only Capability Matrix for the selected device.", + ("readiness", "trust", "ddi", "rsd", "dvt"), + self.refresh_capabilities_button.isEnabled(), + ), + ( + "action:refresh-installed-apps", + "Refresh Installed Apps", + "Load the service-visible application inventory for the selected device.", + ("apps", "inventory", "bundle"), + self.refresh_apps_button.isEnabled(), + ), + ( + "action:backup-encryption-status", + "Check Backup Encryption", + "Read the selected device's MobileBackup2 encryption state.", + ("backup", "mobilebackup2", "encrypted"), + self.check_encryption_button.isEnabled(), + ), + ( + "action:command-drift", + "Check Guided Command Drift", + "Verify every guided route against the installed CLI help without contacting a device.", + ("help", "syntax", "pymobiledevice3", "presets"), + self.command_drift_check_button.isEnabled(), + ), + ( + "action:refresh-live-help", + "Refresh Selected Live Help", + "Load live help for the currently selected Man Pages route.", + ("manpage", "documentation", "syntax"), + self.refresh_manpage_button.isEnabled(), + ), + ) + entries.extend( + action_palette_entry(identifier, title, "Eligible read action", summary, keywords) + for identifier, title, summary, keywords, eligible in eligible_actions + if eligible + ) + if not self._console_controller.is_running(): + device_available = self.selected_device() is not None + entries.extend( + action_palette_entry( + f"preset:{preset.identifier}", + f"Choose {preset.title}", + "Guided command", + f"Open this reviewed preset in Command Center without running it. {preset.summary}", + (preset.category, *preset.argument_template, "preset", preset.risk), + ) + for preset in self._presets + if not preset.requires_device or device_available + ) + return validate_action_palette(tuple(entries)) + + def _execute_action_palette_entry(self, identifier: str) -> None: + if identifier.startswith("navigate:"): + self.navigate_to_page_and_focus(identifier.removeprefix("navigate:")) + return + if identifier.startswith("preset:"): + self._select_palette_preset(identifier.removeprefix("preset:")) + return + actions: Mapping[str, Callable[[], None]] = { + "utility:session-activity": self.show_session_activity, + "utility:keyboard-shortcuts": self.show_keyboard_shortcuts, + "action:retry-device-scan": self._scanner_scan, + "action:developer-mode-status": self.check_developer_mode, + "action:list-developer-images": self.list_mounted_images, + "action:coredevice-details": self.show_coredevice_details, + "action:rvi-status": self.list_rvi_interfaces, + "action:capability-matrix": self.refresh_capability_matrix, + "action:refresh-installed-apps": self.refresh_app_inventory, + "action:backup-encryption-status": self.check_backup_encryption, + "action:command-drift": self.start_command_drift_check, + "action:refresh-live-help": self.refresh_selected_manpage, + } + action = actions.get(identifier) + if action is None: + raise KeyError(f"Unknown action palette entry: {identifier}") + current_identifiers = {entry.identifier for entry in self._eligible_action_palette_entries()} + if identifier not in current_identifiers: + QMessageBox.information( + self, + "Action No Longer Eligible", + "The device or operation state changed while the palette was open. Reopen the palette to refresh it.", + ) + return + action() + + def _select_palette_preset(self, identifier: str) -> None: + matching = tuple(preset for preset in self._presets if preset.identifier == identifier) + if len(matching) != 1: + raise CommandCatalogError(f"Expected one action-palette preset for {identifier!r}, found {len(matching)}") + preset = matching[0] + if self._console_controller.is_running() or (preset.requires_device and self.selected_device() is None): + QMessageBox.information( + self, + "Preset No Longer Eligible", + "The selected preset is no longer eligible in the current device or operation state.", + ) + return + self.navigate_to_page("Command Center") + self.command_category_combo.setCurrentText("All categories") + self.command_search_field.clear() + for row in range(self.command_preset_list.count()): + item = self.command_preset_list.item(row) + if item.data(Qt.ItemDataRole.UserRole) == preset.identifier: + self.command_preset_list.setCurrentRow(row) + self.preset_run_button.setFocus(Qt.FocusReason.ShortcutFocusReason) + return + raise CommandCatalogError(f"Eligible action-palette preset is missing from Command Center: {identifier}") + def show_session_activity(self) -> None: dialog = OperationHistoryDialog(self._operation_records, self) dialog.exec() diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 6f5af91..4c6899c 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -79,9 +79,10 @@ def run_smoke_test(arguments: Sequence[str]) -> int: if arguments: raise ValueError(f"Internal smoke test does not accept arguments: {tuple(arguments)}") os.environ["QT_QPA_PLATFORM"] = "offscreen" - from PySide6.QtCore import SIGNAL + from PySide6.QtCore import SIGNAL, Qt from PySide6.QtWidgets import QApplication, QLabel, QPlainTextEdit, QPushButton, QTableWidget + from ios_developer_toolkit.action_palette import ActionPaletteDialog from ios_developer_toolkit.app import MainWindow from ios_developer_toolkit.backup_protocol import BackupRequest from ios_developer_toolkit.operation_history import OperationHistoryDialog @@ -183,8 +184,34 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError("GUI live-help action did not complete within its bounded smoke-test window") if selected_manpage.command_path not in window._manpage_cache: raise RuntimeError(f"GUI live-help action did not cache successful output: {window.manpage_output.toPlainText()}") + action_palette_button = window.findChild(QPushButton, "actionPaletteButton") + if action_palette_button is None: + raise RuntimeError("GUI action-palette launcher is missing") + action_palette_entries = window._eligible_action_palette_entries() + action_palette_identifiers = {entry.identifier for entry in action_palette_entries} + if "preset:devices" not in action_palette_identifiers: + raise RuntimeError("GUI action palette omitted the host-only devices preset") + device_only_presets = { + f"preset:{preset.identifier}" for preset in window._presets if preset.requires_device + } + exposed_device_only_presets = device_only_presets & action_palette_identifiers + if exposed_device_only_presets: + raise RuntimeError( + f"GUI action palette exposed device-only presets without a selected device: " + f"{sorted(exposed_device_only_presets)}" + ) + action_palette_dialog = ActionPaletteDialog(action_palette_entries, window) + action_palette_dialog.search.setText("session manifest") + application.processEvents() + if action_palette_dialog.results.count() != 1: + raise RuntimeError("GUI action-palette search did not isolate the session-manifest utility") + action_palette_item = action_palette_dialog.results.item(0) + if action_palette_item.data(Qt.ItemDataRole.UserRole) != "utility:session-activity": + raise RuntimeError("GUI action-palette search selected an unexpected entry") + action_palette_dialog.close() expected_shortcuts = { "shortcutRetryDeviceScan", + "shortcutShowActionPalette", "shortcutFocusWorkspaceNavigation", "shortcutFocusWorkspaceSearch", "shortcutShowKeyboardReference", diff --git a/tests/test_action_palette.py b/tests/test_action_palette.py new file mode 100644 index 0000000..999dc9e --- /dev/null +++ b/tests/test_action_palette.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import unittest + +from ios_developer_toolkit.action_palette import ( + ActionPaletteError, + action_palette_entry, + filter_action_palette, + validate_action_palette, +) + + +class ActionPaletteTests(unittest.TestCase): + def setUp(self) -> None: + self.entries = ( + action_palette_entry( + "navigate:Live Logs", + "Open Live Logs", + "Workspace", + "Open independent logging streams.", + ("unified", "syslog", "oslog"), + ), + action_palette_entry( + "preset:lockdown", + "Choose Lockdown overview", + "Guided command", + "Prepare the read-only Lockdown preset for review.", + ("device", "pairing"), + ), + action_palette_entry( + "utility:session-activity", + "Open Session Activity", + "Utility", + "Review typed operation results.", + ("history", "manifest"), + ), + ) + + def test_filters_all_terms_across_titles_summaries_and_keywords(self) -> None: + self.assertEqual( + tuple(entry.identifier for entry in filter_action_palette(self.entries, "device pairing")), + ("preset:lockdown",), + ) + self.assertEqual( + tuple(entry.identifier for entry in filter_action_palette(self.entries, "manifest")), + ("utility:session-activity",), + ) + + def test_ranks_title_matches_before_keyword_matches(self) -> None: + matching = filter_action_palette(self.entries, "live") + + self.assertEqual(matching[0].identifier, "navigate:Live Logs") + + def test_rejects_duplicate_or_incomplete_entries(self) -> None: + with self.assertRaises(ActionPaletteError): + validate_action_palette((self.entries[0], self.entries[0])) + with self.assertRaises(ActionPaletteError): + action_palette_entry("", "Missing", "Utility", "Invalid.", ()) + + +if __name__ == "__main__": + unittest.main() From e2474faa443dd5ec17dffffd7627ecdcb82a3203 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:33:55 -0700 Subject: [PATCH 11/16] Add guided MVT analysis handoff --- .gitignore | 5 + README.md | 54 +- SOURCE_AVAILABILITY.md | 2 + THIRD_PARTY_NOTICES.md | 2 +- docs/PRODUCT_AUDIT_2026-09-21.md | 5 +- docs/screenshots/mvt-analysis.png | Bin 0 -> 184167 bytes ios_developer_toolkit/app.py | 672 ++++++++++++++++++++++++- ios_developer_toolkit/entrypoint.py | 65 +++ ios_developer_toolkit/mvt_connector.py | 277 ++++++++++ tests/test_mvt_connector.py | 128 +++++ 10 files changed, 1195 insertions(+), 15 deletions(-) create mode 100644 docs/screenshots/mvt-analysis.png create mode 100644 ios_developer_toolkit/mvt_connector.py create mode 100644 tests/test_mvt_connector.py diff --git a/.gitignore b/.gitignore index 7b9526e..30c2892 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ reports/ ios-case-*/ ufade-acquisitions/ UFADE Acquisitions/ +mvt-analyses/ +MVT Analyses/ +mvt-analysis-*/ location-logs/ Location Logs/ iOS Developer Toolkit Location Logs/ @@ -33,6 +36,8 @@ nuitka-crash-report.xml *.pcapng *.ufd *.ufdr +*.stix +*.stix2 # Packages, profiles, and developer-image payloads *.cer diff --git a/README.md b/README.md index ab06b24..432653d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ![iOS Developer Toolkit Home workspace](docs/screenshots/home.png) -The current interface organizes one trusted device connection into 12 focused workspaces. It mounts modern DDIs, checks device and developer-service readiness, runs validated `pymobiledevice3` presets, exposes the installed command help, simulates test locations, streams three forms of device logs, captures packets, inspects and installs eligible IPAs, inventories apps, creates encrypted backups, launches an isolated UFADE acquisition, and builds hashed evidence cases. +The current interface organizes one trusted device connection into 12 focused workspaces. It mounts modern DDIs, checks device and developer-service readiness, runs validated `pymobiledevice3` presets, exposes the installed command help, simulates test locations, streams three forms of device logs, captures packets, inspects and installs eligible IPAs, inventories apps, creates encrypted backups, launches an isolated UFADE acquisition, hands decrypted backups to an external MVT analysis, and builds hashed evidence cases. The screenshots use an illustrative device name, model, version, build, and UDID. They contain no real device capture, account identifier, backup, credential, or case evidence. @@ -88,17 +88,18 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - **Selected command readiness** maps each guided Command Center action to the exact connection, trust, Developer Mode, DDI, tunnel, CoreDevice, DVT, or Web Inspector checks it needs, with a one-click route to the bounded read-only matrix; - **Action Palette** (`⌘ K`) searches all workspaces, guided presets, utilities, and currently eligible read actions while withholding device-only operations until a physical target is selected; - **Session Activity** correlates completed typed operations with workspace, target, transport, exact argument vector, timing, terminal status, prerequisite snapshot, output paths, and output hashes without automatically persisting raw command output; +- **MVT Analysis** validates a user-installed `mvt-ios` executable and runs a consented decrypted-backup analysis with isolated output, opt-in indicators, network access off by default, no password input, and no clean-device verdict; - the desktop UI starts independently of the MobileBackup2 transport, and device discovery consumes output both while the child process runs and after it exits, so a fast successful `usbmux list` result is not lost before the picker is updated; - **Demo Mode** shows a prominently labeled simulated iPhone for walkthroughs and screenshots, while deliberately withholding a selected physical-device target and disabling device operations; - the manual **Capability Matrix** reports host, trust, Developer Mode, DDI, tunnel, DVT, CoreDevice, and related readiness as separate bounded results, then compares completed local probes across real devices without retaining raw UDIDs; - **DVT network activity** and **CoreDevice applications** are handled as long-running streams with explicit Stop controls instead of misleading finite snapshots; - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; -- app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one selected-device workflow; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 116 tests, embedded CLI checks, a 96-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, external MVT analysis, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one workbench; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 121 tests, embedded CLI checks, a 110-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. -The README contains 20 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. +The README contains 21 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. | Prepare the device and DDI | Observe live services | Run guided commands | |---|---|---| @@ -123,6 +124,7 @@ The README contains 20 sanitized screenshots. The six views below provide a quic | Demo Mode | Shows a local simulated iPhone for an honest product walkthrough or screenshot. | The banner identifies the simulation and no device service, command, mount, capture, backup, or location operation can run. | | Eligible Action Palette | Searches workspaces, utilities, guided presets, and read actions that are valid for the current device and process state. | Selecting a preset opens it for review; it never runs automatically or bypasses confirmation. | | Session Activity | Correlates completed typed operations and previews an exportable structured JSON manifest. | Session-only by default; raw output is omitted, and explicit exports can still contain identifiers and local paths. | +| Guided MVT handoff | Validates and records external MVT provenance, then analyzes one authorized decrypted backup into a new result path. | No password input; inherited password/IOC variables are removed, network is off by default, and no result is translated into a clean-device claim. | ## What the workbench covers @@ -135,7 +137,7 @@ The README contains 20 sanitized screenshots. The six views below provide a quic | **Live Logs** | Open independent Unified Logs, classic syslog, and DVT OSLog windows | Only DVT OSLog | Complete raw spool plus filtered working view | | **Command Center** | Run 49 guided commands or explicit advanced arguments | Command-specific | Validated parameters, risk label, exact preview, exit output | | **Installed Apps** | Search the service-visible app inventory and uninstall with confirmation | No | Names, bundle IDs, versions, types, optional sizes | -| **Backup** | Run MobileBackup2 or launch a separate UFADE environment | No | Full/incremental encrypted backup or external acquisition | +| **Backup** | Run MobileBackup2, launch separate UFADE, or analyze a decrypted backup with external MVT | No | Encrypted backup, external acquisition, or isolated forensic records | | **Sideload IPA** | Inspect a local IPA before attempting installation | No DDI for normal install | Archive, provisioning, and signature report | | **Evidence Capture** | Correlate snapshots, timed streams, screenshots, crashes, and PCAP | Partial coverage without it | Timestamped case, coverage states, manifest, SHA-256 inventory | | **Man Pages** | Browse 59 command routes instantly and request live help on demand | No | Version-matched syntax rather than copied examples | @@ -185,6 +187,7 @@ Highlights of the current build: - searchable app inventory with optional size calculation and confirmed uninstall; - MobileBackup2 encryption checks and new-password handling through a private helper input stream; - isolated external UFADE validation and launch without importing its dependencies into this project; +- consent-based external MVT validation and backup analysis without accepting passwords or weakening MVT's warning model; - a multi-source collector that retains failures as coverage evidence and hashes finalized artifacts; - no one-click erase, restore, activation, supervision, reboot, shutdown, or nonce-changing shortcut. @@ -599,7 +602,7 @@ An empty inventory is not proof that no apps exist. It may instead indicate devi ![Backup providers workspace](docs/screenshots/backup.png) -The Backup workspace keeps two providers isolated. +The Backup workspace keeps three providers isolated. #### MobileBackup2 @@ -668,6 +671,38 @@ The selected toolkit device is shown only as a cross-check; UFADE performs its o Use UFADE's own documentation to assess version compatibility, licensing, dependencies, and the forensic meaning of each output format. +#### MVT Analysis + +[MVT](https://github.com/mvt-project/mvt) is an independent forensic research tool intended for consented mobile-device analysis. It remains separately installed under the MVT License; the toolkit does not bundle, import, patch, update, relicense, or redistribute it. + +![Guided external MVT backup analysis](docs/screenshots/mvt-analysis.png) + +The guided handoff validates the selected `mvt-ios` executable asynchronously, records its resolved path, version, and SHA-256, and disables automatic MVT version and indicator update checks for a reproducible run. Choose one decrypted iTunes-style backup containing `Manifest.db` and `Info.plist`, a new output path that does not exist, and optional `.stix`, `.stix2`, or `.json` indicator files. An encrypted backup is rejected with instructions to prepare a protected decrypted working copy outside the toolkit. + +The toolkit has no MVT password field. It removes inherited MVT backup-password, implicit IOC, VirusTotal-key, profiling, and hashing variables before starting the external process. MVT receives an isolated temporary configuration directory that is deleted when the process completes. Network access is off by default; enabling it can allow shortened-URL resolution and other MVT requests. The selected source backup is never modified by the toolkit, and MVT must create a fresh result directory outside that source. + +Both acknowledgements are required: the operator must own the backup or have explicit authorization and consent, and must accept that successful completion or no findings does not prove that a device is clean, safe, uncompromised, or never targeted. The toolkit displays and records process outcome but does not parse MVT output into a verdict. Public indicators may be incomplete or stale; high-risk cases require qualified forensic support and appropriate non-public threat intelligence. + +##### Install and run MVT on macOS + +The **Copy Setup Commands** button follows MVT's separate-installation approach: + +```bash +brew install python3 pipx sqlite3 +pipx ensurepath +pipx install mvt +``` + +Then, in **Backup → MVT Analysis**: + +1. Click **Find Installed** or choose an absolute `mvt-ios` path, then click **Validate Installation**. +2. Choose the authorized decrypted backup. If the backup is encrypted, follow the [official decryption and backup-check guide](https://docs.mvt.re/en/latest/ios/backup/check/) outside this app; never place its password in a command, issue, or support bundle. +3. Choose a parent for a new result folder. Existing paths and locations inside the source backup are rejected. +4. Optionally select reviewed STIX2/JSON indicators, Fast mode, MVT hashing, or explicit network access. +5. Read and select both required acknowledgements, review the complete launch confirmation, and start the analysis. +6. Use **Stop** to request termination. A stopped or failed output directory is partial and must not be interpreted as a completed analysis. +7. Review MVT's `command.log`, structured records, alerts, timeline, hashes, tool version, and indicator provenance directly. Protect the output before sharing it. + ### Sideload IPA ![Sideload IPA workspace](docs/screenshots/sideload-ipa.png) @@ -927,9 +962,10 @@ Treat these outputs as potentially sensitive: - Unified Logs, classic syslog, DVT logs, process lists, and crash reports; - screenshots, AFC listings, GPX routes, and simulated coordinates; - MobileBackup2 and UFADE acquisitions; +- MVT source backups, indicator files, command logs, and analysis results; - IPA provisioning records and signing identities. -The repository `.gitignore` excludes the toolkit's common backup, case, capture, crash, log, packet, GPX, UFADE, DDI, certificate, profile, and IPA artifact patterns. That is a publication guard, not an access-control system. Store evidence outside a public checkout when possible, restrict filesystem permissions, encrypt sensitive archives, and review every staged file before committing. +The repository `.gitignore` excludes the toolkit's common backup, case, capture, crash, log, packet, GPX, UFADE, MVT, DDI, certificate, profile, and IPA artifact patterns. That is a publication guard, not an access-control system. Store evidence outside a public checkout when possible, restrict filesystem permissions, encrypt sensitive archives, and review every staged file before committing. ### Capability is not observed behavior @@ -1056,6 +1092,7 @@ Install or update Xcode if the candidate is absent. The toolkit requires the exp │ ├── local_ddi.py # local Xcode candidate/Cryptex workflow │ ├── location_lab.py # coordinates, GPX, routes, saved places, evidence │ ├── models.py # typed device and collection models +│ ├── mvt_connector.py # external MVT provenance, request, and isolation policy │ ├── operation_history.py # session journal, output digests, and explicit JSON export │ ├── entrypoint.py # packaged internal CLI and worker dispatch │ ├── qt_process.py # typed, bounded finite-process lifecycle controller @@ -1091,7 +1128,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 116 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 96-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 121 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 110-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. @@ -1105,6 +1142,7 @@ Windows and Linux would require a separate host implementation or deliberately i - [`DeveloperDiskImage`](https://github.com/doronz88/DeveloperDiskImage) supplies the downloadable modern DDI payload used by upstream auto-mount. - [Apple Developer Mode documentation](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device) describes the on-device security workflow. - [`UFADE`](https://github.com/prosch88/UFADE) is supported only as a separately installed and independently licensed external provider. +- [`MVT`](https://github.com/mvt-project/mvt) is supported only as a separately installed external analysis provider under its own license and warning model. - [`ostrace`](https://github.com/BerkayCaglar/ostrace) informed live-log interaction design; no GPL source is copied, imported, or linked into this MIT project. - [`LocationSimulator`](https://github.com/Schlaubischlump/LocationSimulator) informed the offline map/teleport workflow. Its GPL source is not copied or linked, and its public backend does not support iOS 17 or later. - [Natural Earth](https://www.naturalearthdata.com/) provides the public-domain 1:110m land geometry rendered into the bundled offline Location Lab map. diff --git a/SOURCE_AVAILABILITY.md b/SOURCE_AVAILABILITY.md index a8f021d..833d23b 100644 --- a/SOURCE_AVAILABILITY.md +++ b/SOURCE_AVAILABILITY.md @@ -13,3 +13,5 @@ The prebuilt application is accompanied by an architecture-specific CycloneDX SB The release-critical upstream source locations and license information are recorded in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). In particular, the packaged `pymobiledevice3` release is available at its [matching upstream tag](https://github.com/doronz88/pymobiledevice3/tree/v11.15.1), including its GPL-3.0-or-later license. The project’s public tagged source, package inventory, and embedded notices are intended to make the source and license boundary inspectable before redistribution. PySide6/Qt, Nuitka, CPython, and every other dependency remain subject to their own terms. Consult the generated `Contents/Resources/Licenses/` inventory in the application and the matching SBOM for the exact package set. This document is an availability and attribution statement, not legal advice. + +Optional UFADE and MVT integrations launch user-managed external installations. Their source is not part of the application bundle or release SBOM; consult their upstream repositories and licenses for the exact external version selected by the operator. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index c04f5a7..04934ce 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -20,7 +20,7 @@ Each architecture-specific release also contains: The generated package inventory is intentionally more detailed than this summary and includes transitive Python dependencies. A package whose wheel does not contain a license text is identified as such in the inventory and linked to its declared project source when available. -UFADE is an optional, separately installed external provider. The toolkit does not bundle UFADE. Other projects named in the README as design references are not copied, imported, or linked unless the README explicitly says otherwise. +UFADE and MVT are optional, separately installed external providers. The toolkit does not bundle either project. MVT remains subject to the [MVT License](https://license.mvt.re/1.1/) and its consent and interpretation boundaries. Other projects named in the README as design references are not copied, imported, or linked unless the README explicitly says otherwise. See [SOURCE_AVAILABILITY.md](SOURCE_AVAILABILITY.md) for the project source location, matching tagged source, and upstream source locations for bundled third-party components. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 7ce76c6..c46a1ed 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -10,7 +10,7 @@ The correct next investment is therefore a **reliable startup and device-discove ## What exists today -The product has twelve workspaces: Home, Device & DDI, Capability Matrix, Location Lab, Live Logs, Command Center, Installed Apps, Backup, Sideload IPA, Evidence Capture, Man Pages, and Scope & Safety. It currently provides 49 declarative guided command presets, a live-help/command-drift check, DDI mounting, RSD/CoreDevice/DVT checks, GPX location simulation with cleanup, separate Unified/syslog/oslog windows, installed app inventory, encrypted MobileBackup2 workflow, UFADE setup guidance, IPA inspection and installation, RVI/PCAP and artifact collection, guided case intake, support bundles, compatibility history, and keyboard-first navigation. +The product has twelve workspaces: Home, Device & DDI, Capability Matrix, Location Lab, Live Logs, Command Center, Installed Apps, Backup, Sideload IPA, Evidence Capture, Man Pages, and Scope & Safety. It currently provides 49 declarative guided command presets, a live-help/command-drift check, DDI mounting, RSD/CoreDevice/DVT checks, GPX location simulation with cleanup, separate Unified/syslog/oslog windows, installed app inventory, encrypted MobileBackup2 workflow, isolated UFADE launch, guided external MVT analysis, IPA inspection and installation, RVI/PCAP and artifact collection, guided case intake, support bundles, compatibility history, and keyboard-first navigation. The repository is a Python 3.10+ PySide6 project with a bundled `pymobiledevice3` runtime model. `ios_developer_toolkit/app.py` is a 5,600+ line `MainWindow`, while domain modules cover capability probing, collectors, live logs, location testing, IPA inspection, support bundles, and device compatibility. CI runs unit tests, compile checks, CLI help checks, and a headless GUI smoke test on macOS. Tagged release CI produces Apple Silicon and Intel bundles, CycloneDX SBOMs, checksums, and GitHub attestations. The app is ad-hoc signed, not Developer ID signed or notarized. @@ -122,7 +122,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P2 — deepen expert workflows without scope creep * Maintain the session-local typed-operation journal, explicit structured JSON manifests, and universal Action Palette that exposes only eligible operations. -* Implement a guided MVT backup-analysis handoff with explicit consent, no password persistence, output isolation, and no “clean device” conclusion. +* Maintain the guided MVT backup-analysis handoff with explicit consent, no password persistence, output isolation, and no “clean device” conclusion. * Add optional user-configured adapters for `go-ios`, `idb`, and `ipsw`, each with executable provenance and version display. * Publish a small documentation site split into quick start, architecture, safety, troubleshooting, release verification, and contributor paths. @@ -178,6 +178,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Migrated Command Center guided, advanced, finite, and streaming commands to a typed interactive-process lifecycle. | 109 tests and the 94-action GUI smoke passed; real child-process tests cover final stdout/stderr draining, launch failure, idempotent cancellation, and no arbitrary runtime limit. | Command output remains session-local unless the user explicitly preserves it through a task-specific evidence workflow. | | 2026-09-22 | Added a session-local operation journal and explicit per-operation JSON manifests across the primary typed workflows. | 113 tests and a 95-action GUI smoke passed; tests cover immutable bounded history, exact argument retention, output hashing without raw-output embedding, owner-only export, and overwrite refusal. | Capability Matrix, Location Lab, Live Logs, and Command Drift retain their stronger workflow-specific records rather than duplicating raw or high-volume events into this journal. | | 2026-09-22 | Added a keyboard-first Action Palette computed from current device and process eligibility. | 116 tests and a 96-action GUI smoke passed; smoke coverage verifies disconnected-state preset filtering, host-preset access, search behavior, stable control identity, and the `⌘ K` shortcut. | Guided presets are selected for review rather than executed, and eligibility is checked again at activation. | +| 2026-09-22 | Added a guided external MVT handoff for consented decrypted-backup analysis. | 121 tests and a 110-action GUI smoke passed; tests cover executable provenance, secret-environment removal, backup structure/encryption checks, isolated output, explicit IOC arguments, offline defaults, version validation, and an end-to-end synthetic analysis process. | MVT stays separately installed; the toolkit accepts no password and never translates completion or absent findings into a clean-device verdict. | ## Research sources diff --git a/docs/screenshots/mvt-analysis.png b/docs/screenshots/mvt-analysis.png new file mode 100644 index 0000000000000000000000000000000000000000..551316440b2e214940de5228e2cd6da856b52812 GIT binary patch literal 184167 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ðl+?Cg?d7FIyM$E9 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 op86j1L+Q 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 literal 0 HcmV?d00001 diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index 1365731..24e4aaa 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -5,6 +5,7 @@ import shlex import subprocess import sys +import tempfile from collections.abc import Callable from datetime import datetime, timezone from pathlib import Path @@ -179,6 +180,24 @@ ) from ios_developer_toolkit.live_logs import LiveLogError, LiveLogWindow, log_stream_specs, stream_spec from ios_developer_toolkit.models import DeviceDataError, IOSDevice, parse_devices_json +from ios_developer_toolkit.mvt_connector import ( + MVT_BACKUP_GUIDE_URL, + MVT_INSTALLATION_URL, + MVT_REPOSITORY_URL, + MVTAnalysisRequest, + MVTExecutable, + MVTInstallation, + MVTValidationError, + create_mvt_analysis_request, + discover_mvt_executables, + inspect_mvt_executable, + mvt_analysis_arguments, + mvt_command, + mvt_environment, + mvt_setup_commands, + mvt_version_arguments, + parse_mvt_version_output, +) from ios_developer_toolkit.operation_history import ( OperationContext, OperationHistoryDialog, @@ -517,6 +536,60 @@ def __init__(self) -> None: layout.addWidget(buttons) +class MVTGuideDialog(QDialog): + def __init__(self) -> None: + super().__init__() + self.setWindowTitle("Analyze a Backup with MVT") + self.resize(820, 690) + layout = QVBoxLayout(self) + heading = QLabel("Consent-based external MVT backup analysis") + heading.setObjectName("mvtGuideHeading") + heading.setFont(QFont(heading.font().family(), 18, QFont.Weight.DemiBold)) + layout.addWidget(heading) + instructions = QTextBrowser() + instructions.setObjectName("mvtGuideContent") + instructions.setOpenExternalLinks(True) + instructions.setHtml( + f""" +

    1. Install MVT separately

    +

    Use Copy Setup Commands in the MVT Analysis tab, run the commands in Terminal, then choose the + resulting mvt-ios executable. The toolkit does not bundle, import, update, or modify MVT.

    + +

    2. Prepare a consented backup copy

    +

    Choose one iTunes-style backup folder containing Manifest.db and Info.plist. + MVT analyzes a decrypted backup. If the source is encrypted, decrypt a protected working copy outside this + toolkit using MVT's official instructions. Do not paste a password into this application: it has no backup + password field and removes inherited MVT password variables from the child process.

    + +

    3. Isolate the results

    +

    Choose a new output path that does not exist and is outside the source backup. The toolkit refuses an + existing path so a new run cannot mix with earlier results. MVT creates JSON records and its own command log + in that folder. Optional input hashes can substantially increase runtime on a large backup.

    + +

    4. Decide whether to supply indicators or network access

    +

    STIX2/JSON indicator files are opt-in. Network access is off by default, which prevents shortened-URL + resolution and other MVT network requests during the run. Enable it only after reviewing the selected + indicators and the privacy implications. Automatic version and indicator update checks remain disabled for + a reproducible handoff.

    + +

    5. Interpret the output carefully

    +

    MVT extracts forensic records and can identify matches against supplied indicators. A completed run, + zero alerts, or no *_detected.json files does not establish that a device is clean, safe, + uncompromised, or never targeted. Public indicators can be incomplete or stale. Preserve the original backup, + record tool and indicator versions, and seek qualified forensic assistance for high-risk cases.

    + +
    + """ + ) + layout.addWidget(instructions, 1) + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) + buttons.setObjectName("mvtGuideButtons") + buttons.accepted.connect(self.accept) + layout.addWidget(buttons) + + class MainWindow(QMainWindow): def __init__(self) -> None: super().__init__() @@ -588,6 +661,16 @@ def __init__(self) -> None: self._backup_encryption_state: bool | None = None self._last_backup_path: Path | None = None self._ufade_installation: UFADEInstallation | None = None + self._mvt_controller = InteractiveProcessController(self) + self._mvt_controller.stdout_received.connect(self._append_mvt_output) + self._mvt_controller.stderr_received.connect(self._append_mvt_output) + self._mvt_controller.completed.connect(self._mvt_completed) + self._mvt_installation: MVTInstallation | None = None + self._mvt_pending_executable: MVTExecutable | None = None + self._mvt_operation = "" + self._mvt_request: MVTAnalysisRequest | None = None + self._mvt_ioc_paths: tuple[Path, ...] = () + self._mvt_temporary_config: tempfile.TemporaryDirectory[str] | None = None self._location_process: QProcess | None = None self._location_operation = "" self._location_arguments: tuple[str, ...] = () @@ -1012,7 +1095,7 @@ def _eligible_action_palette_entries(self) -> tuple[ActionPaletteEntry, ...]: "Live Logs": "Open independent raw-spooling log windows.", "Command Center": "Choose a validated guided command or explicit advanced arguments.", "Installed Apps": "Inspect the service-visible app inventory.", - "Backup": "Prepare MobileBackup2 or an external UFADE handoff.", + "Backup": "Prepare MobileBackup2, external UFADE acquisition, or consented MVT analysis.", "Sideload IPA": "Inspect a local IPA before an eligible installation attempt.", "Evidence Capture": "Prepare a scoped case and bounded evidence collection.", "Man Pages": "Browse version-matched command routes and live help.", @@ -2188,8 +2271,9 @@ def _build_backup_tab(self) -> QWidget: heading.setFont(QFont(heading.font().family(), 20, QFont.Weight.Bold)) layout.addWidget(heading) explanation = QLabel( - "Choose the built-in MobileBackup2 workflow or launch a separately installed UFADE forensic acquisition. " - "The providers use isolated runtimes and do not share passwords or dependencies." + "Create a MobileBackup2 backup, launch a separately installed UFADE acquisition, or hand a decrypted " + "backup to an independently installed MVT analysis. The providers use isolated runtimes and do not share " + "passwords or dependencies." ) explanation.setWordWrap(True) layout.addWidget(explanation) @@ -2198,8 +2282,10 @@ def _build_backup_tab(self) -> QWidget: provider_tabs.setObjectName("backupProviderTabs") provider_tabs.addTab(self._build_mobilebackup_page(), "MobileBackup2") provider_tabs.addTab(self._build_ufade_backup_page(), "UFADE External") + provider_tabs.addTab(self._build_mvt_analysis_page(), "MVT Analysis") provider_tabs.setTabToolTip(0, "Toolkit-managed full or incremental iTunes-style backup") provider_tabs.setTabToolTip(1, "Launch an independently installed UFADE acquisition environment") + provider_tabs.setTabToolTip(2, "Analyze a consented decrypted backup with an independently installed MVT CLI") layout.addWidget(provider_tabs, 1) return page @@ -2437,6 +2523,192 @@ def _build_ufade_backup_page(self) -> QWidget: scroll.setWidget(page) return scroll + def _build_mvt_analysis_page(self) -> QWidget: + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(12) + + overview = QLabel( + "MVT (Mobile Verification Toolkit) is an independent forensic research tool with its own license and " + "warning model. This guided handoff validates and runs a user-installed mvt-ios executable against a " + "decrypted backup; it does not bundle MVT, accept backup passwords, or declare a device clean." + ) + overview.setObjectName("mvtProviderExplanation") + overview.setWordWrap(True) + layout.addWidget(overview) + + guide_group = QGroupBox("Install, prepare, and interpret") + guide_layout = QVBoxLayout(guide_group) + guide_text = QLabel( + "1. Install MVT separately. 2. Validate its executable and version. 3. Select one authorized, decrypted " + "iTunes-style backup. 4. Choose a new isolated output path and optional STIX2 files. 5. Review consent, " + "network, and interpretation boundaries before starting." + ) + guide_text.setObjectName("mvtQuickStart") + guide_text.setWordWrap(True) + guide_layout.addWidget(guide_text) + guide_controls = QHBoxLayout() + guide_button = QPushButton("Open Full Walkthrough") + guide_button.setObjectName("openMVTGuideButton") + guide_button.clicked.connect(self.show_mvt_guide) + guide_controls.addWidget(guide_button) + setup_button = QPushButton("Copy Setup Commands") + setup_button.setObjectName("copyMVTSetupButton") + setup_button.clicked.connect(self.copy_mvt_setup_commands) + guide_controls.addWidget(setup_button) + official_button = QPushButton("Open Official Guide") + official_button.setObjectName("openMVTOfficialGuideButton") + official_button.clicked.connect(self.open_mvt_official_guide) + guide_controls.addWidget(official_button) + repository_button = QPushButton("Open MVT Repository") + repository_button.setObjectName("openMVTRepositoryButton") + repository_button.clicked.connect(self.open_mvt_repository) + guide_controls.addWidget(repository_button) + guide_controls.addStretch() + guide_layout.addLayout(guide_controls) + layout.addWidget(guide_group) + + setup_group = QGroupBox("External MVT executable") + setup_layout = QFormLayout(setup_group) + executable_row = QHBoxLayout() + discovered = discover_mvt_executables(Path.home(), os.environ.get("PATH", "")) + self.mvt_executable_field = QLineEdit(str(discovered[0]) if discovered else "") + self.mvt_executable_field.setObjectName("mvtExecutable") + self.mvt_executable_field.setPlaceholderText("Absolute path to an independently installed mvt-ios executable") + self.mvt_executable_field.textChanged.connect(self._invalidate_mvt_validation) + executable_row.addWidget(self.mvt_executable_field, 1) + self.choose_mvt_executable_button = QPushButton("Choose…") + self.choose_mvt_executable_button.setObjectName("chooseMVTExecutableButton") + self.choose_mvt_executable_button.clicked.connect(self.choose_mvt_executable) + executable_row.addWidget(self.choose_mvt_executable_button) + self.find_mvt_executable_button = QPushButton("Find Installed") + self.find_mvt_executable_button.setObjectName("findMVTExecutableButton") + self.find_mvt_executable_button.clicked.connect(self.find_mvt_executable) + executable_row.addWidget(self.find_mvt_executable_button) + setup_layout.addRow("mvt-ios", executable_row) + self.mvt_validation_status = QLabel("MVT installation has not been validated") + self.mvt_validation_status.setObjectName("mvtValidationStatus") + self.mvt_validation_status.setWordWrap(True) + setup_layout.addRow("Status", self.mvt_validation_status) + self.validate_mvt_button = QPushButton("Validate Installation") + self.validate_mvt_button.setObjectName("validateMVTButton") + self.validate_mvt_button.clicked.connect(self.validate_mvt_from_ui) + setup_layout.addRow(self.validate_mvt_button) + layout.addWidget(setup_group) + + paths_group = QGroupBox("Analysis input and isolated output") + paths_layout = QFormLayout(paths_group) + backup_row = QHBoxLayout() + self.mvt_backup_field = QLineEdit() + self.mvt_backup_field.setObjectName("mvtBackupPath") + self.mvt_backup_field.setPlaceholderText("Decrypted backup folder containing Manifest.db and Info.plist") + self.mvt_backup_field.textChanged.connect(self._update_mvt_controls) + backup_row.addWidget(self.mvt_backup_field, 1) + self.choose_mvt_backup_button = QPushButton("Choose…") + self.choose_mvt_backup_button.setObjectName("chooseMVTBackupButton") + self.choose_mvt_backup_button.clicked.connect(self.choose_mvt_backup) + backup_row.addWidget(self.choose_mvt_backup_button) + paths_layout.addRow("Decrypted backup", backup_row) + output_row = QHBoxLayout() + default_output = ( + Path.home() + / "Documents" + / "MVT Analyses" + / datetime.now(timezone.utc).strftime("mvt-analysis-%Y%m%d-%H%M%S") + ) + self.mvt_output_field = QLineEdit(str(default_output)) + self.mvt_output_field.setObjectName("mvtOutputPath") + self.mvt_output_field.setPlaceholderText("A new path that does not already exist") + self.mvt_output_field.textChanged.connect(self._update_mvt_controls) + output_row.addWidget(self.mvt_output_field, 1) + self.choose_mvt_output_button = QPushButton("Choose Parent…") + self.choose_mvt_output_button.setObjectName("chooseMVTOutputButton") + self.choose_mvt_output_button.clicked.connect(self.choose_mvt_output_parent) + output_row.addWidget(self.choose_mvt_output_button) + self.open_mvt_output_button = QPushButton("Open Results") + self.open_mvt_output_button.setObjectName("openMVTOutputButton") + self.open_mvt_output_button.clicked.connect(self.open_mvt_output_directory) + output_row.addWidget(self.open_mvt_output_button) + paths_layout.addRow("New result path", output_row) + layout.addWidget(paths_group) + + indicator_group = QGroupBox("Optional indicators and processing") + indicator_layout = QFormLayout(indicator_group) + indicator_row = QHBoxLayout() + self.mvt_ioc_status = QLabel("No STIX2/JSON indicator files selected") + self.mvt_ioc_status.setObjectName("mvtIOCStatus") + self.mvt_ioc_status.setWordWrap(True) + indicator_row.addWidget(self.mvt_ioc_status, 1) + self.choose_mvt_iocs_button = QPushButton("Choose IOC Files…") + self.choose_mvt_iocs_button.setObjectName("chooseMVTIOCFilesButton") + self.choose_mvt_iocs_button.clicked.connect(self.choose_mvt_ioc_files) + indicator_row.addWidget(self.choose_mvt_iocs_button) + self.clear_mvt_iocs_button = QPushButton("Clear") + self.clear_mvt_iocs_button.setObjectName("clearMVTIOCFilesButton") + self.clear_mvt_iocs_button.clicked.connect(self.clear_mvt_ioc_files) + indicator_row.addWidget(self.clear_mvt_iocs_button) + indicator_layout.addRow("Indicators", indicator_row) + self.mvt_fast_checkbox = QCheckBox("Fast mode: skip time- or resource-intensive features") + self.mvt_fast_checkbox.setObjectName("mvtFastMode") + indicator_layout.addRow(self.mvt_fast_checkbox) + self.mvt_hashes_checkbox = QCheckBox("Ask MVT to hash processed input and result files (may be slow)") + self.mvt_hashes_checkbox.setObjectName("mvtHashFiles") + indicator_layout.addRow(self.mvt_hashes_checkbox) + self.mvt_network_checkbox = QCheckBox( + "Allow MVT network requests, including shortened-URL resolution during IOC checks" + ) + self.mvt_network_checkbox.setObjectName("mvtAllowNetwork") + self.mvt_network_checkbox.setChecked(False) + indicator_layout.addRow(self.mvt_network_checkbox) + layout.addWidget(indicator_group) + + consent_group = QGroupBox("Required consent and interpretation boundary") + consent_layout = QVBoxLayout(consent_group) + self.mvt_authorization_checkbox = QCheckBox( + "I own this backup or have explicit authorization and consent to analyze it with MVT." + ) + self.mvt_authorization_checkbox.setObjectName("mvtAuthorizationAcknowledgement") + self.mvt_authorization_checkbox.toggled.connect(self._update_mvt_controls) + consent_layout.addWidget(self.mvt_authorization_checkbox) + self.mvt_interpretation_checkbox = QCheckBox( + "I understand that a successful run or no findings does not prove the device is clean, safe, or uncompromised." + ) + self.mvt_interpretation_checkbox.setObjectName("mvtInterpretationAcknowledgement") + self.mvt_interpretation_checkbox.toggled.connect(self._update_mvt_controls) + consent_layout.addWidget(self.mvt_interpretation_checkbox) + layout.addWidget(consent_group) + + controls = QHBoxLayout() + self.run_mvt_button = QPushButton("Run MVT Backup Analysis…") + self.run_mvt_button.setObjectName("runMVTAnalysisButton") + self.run_mvt_button.clicked.connect(self.run_mvt_analysis) + controls.addWidget(self.run_mvt_button) + self.stop_mvt_button = QPushButton("Stop") + self.stop_mvt_button.setObjectName("stopMVTAnalysisButton") + self.stop_mvt_button.clicked.connect(self.stop_mvt_analysis) + controls.addWidget(self.stop_mvt_button) + controls.addStretch() + layout.addLayout(controls) + self.mvt_status = QLabel( + "Validate MVT, select a decrypted backup and new output path, then acknowledge both boundaries." + ) + self.mvt_status.setObjectName("mvtAnalysisStatus") + self.mvt_status.setWordWrap(True) + layout.addWidget(self.mvt_status) + self.mvt_output = QPlainTextEdit() + self.mvt_output.setObjectName("mvtAnalysisOutput") + self.mvt_output.setReadOnly(True) + self.mvt_output.setMaximumBlockCount(7000) + self.mvt_output.setMinimumHeight(180) + layout.addWidget(self.mvt_output, 1) + self._update_mvt_controls() + scroll = QScrollArea() + scroll.setObjectName("mvtAnalysisScrollArea") + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setWidget(page) + return scroll + def _build_command_center_page(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) @@ -5035,6 +5307,7 @@ def _update_backup_controls(self) -> None: self.open_backup_button.setEnabled(not running) if hasattr(self, "launch_ufade_button"): self.launch_ufade_button.setEnabled(device_available and not running) + self._update_mvt_controls() self._backup_encryption_choice_changed(self.require_encryption_checkbox.isChecked()) def stop_backup(self) -> None: @@ -5279,6 +5552,393 @@ def open_ufade_output_directory(self) -> None: return QDesktopServices.openUrl(QUrl.fromLocalFile(str(destination))) + def _invalidate_mvt_validation(self, value: str) -> None: + del value + self._mvt_installation = None + self._mvt_pending_executable = None + self.mvt_validation_status.setText("MVT installation has not been validated") + self._update_mvt_controls() + + def mvt_executable_path(self) -> Path: + value = self.mvt_executable_field.text().strip() + if not value: + raise MVTValidationError("Choose an independently installed mvt-ios executable") + return Path(value).expanduser() + + def mvt_backup_path(self) -> Path: + value = self.mvt_backup_field.text().strip() + if not value: + raise MVTValidationError("Choose a decrypted iTunes-style backup folder") + return Path(value).expanduser() + + def mvt_output_path(self) -> Path: + value = self.mvt_output_field.text().strip() + if not value: + raise MVTValidationError("Choose a new, non-empty MVT result path") + return Path(value).expanduser() + + def choose_mvt_executable(self) -> None: + selected, _ = QFileDialog.getOpenFileName( + self, + "Choose external mvt-ios executable", + self.mvt_executable_field.text(), + "Executable (*)", + ) + if selected: + self.mvt_executable_field.setText(selected) + + def find_mvt_executable(self) -> None: + candidates = discover_mvt_executables(Path.home(), os.environ.get("PATH", "")) + if not candidates: + QMessageBox.information( + self, + "MVT Not Found", + "No executable mvt-ios was found in PATH, ~/.local/bin, /opt/homebrew/bin, or /usr/local/bin. " + "Use Copy Setup Commands or choose the executable manually.", + ) + return + self.mvt_executable_field.setText(str(candidates[0])) + self.mvt_status.setText(f"Found {len(candidates)} MVT executable candidate(s); validate the selected path.") + + def choose_mvt_backup(self) -> None: + selected = QFileDialog.getExistingDirectory( + self, + "Choose decrypted iTunes-style backup", + self.mvt_backup_field.text() or str(Path.home()), + ) + if selected: + self.mvt_backup_field.setText(selected) + + def choose_mvt_output_parent(self) -> None: + current_value = self.mvt_output_field.text().strip() + current = Path(current_value).expanduser() if current_value else Path.home() / "Documents" / "MVT Analyses" + selected = QFileDialog.getExistingDirectory( + self, + "Choose parent folder for a new MVT analysis", + str(current.parent), + ) + if not selected: + return + destination = Path(selected) / datetime.now(timezone.utc).strftime("mvt-analysis-%Y%m%d-%H%M%S") + self.mvt_output_field.setText(str(destination)) + + def choose_mvt_ioc_files(self) -> None: + selected, _ = QFileDialog.getOpenFileNames( + self, + "Choose MVT STIX2 indicator files", + str(Path.home()), + "MVT indicators (*.stix *.stix2 *.json)", + ) + if not selected: + return + self._mvt_ioc_paths = tuple(Path(path) for path in selected) + self._refresh_mvt_ioc_status() + + def clear_mvt_ioc_files(self) -> None: + self._mvt_ioc_paths = () + self._refresh_mvt_ioc_status() + + def _refresh_mvt_ioc_status(self) -> None: + if not self._mvt_ioc_paths: + self.mvt_ioc_status.setText("No STIX2/JSON indicator files selected") + else: + names = ", ".join(path.name for path in self._mvt_ioc_paths) + self.mvt_ioc_status.setText(f"{len(self._mvt_ioc_paths)} selected: {names}") + + def copy_mvt_setup_commands(self) -> None: + QApplication.clipboard().setText("\n".join(mvt_setup_commands())) + self.mvt_output.appendPlainText( + "Copied official-style macOS pipx setup commands. Run them in Terminal, reopen the app if PATH changed, " + "then click Find Installed and Validate Installation." + ) + + def show_mvt_guide(self) -> None: + MVTGuideDialog().exec() + + def open_mvt_official_guide(self) -> None: + if not QDesktopServices.openUrl(QUrl(MVT_BACKUP_GUIDE_URL)): + QMessageBox.critical( + self, + "Could Not Open MVT Guide", + f"macOS could not open the official MVT backup-analysis guide:\n{MVT_BACKUP_GUIDE_URL}", + ) + + def open_mvt_repository(self) -> None: + if not QDesktopServices.openUrl(QUrl(MVT_REPOSITORY_URL)): + QMessageBox.critical( + self, + "Could Not Open MVT Repository", + f"macOS could not open the MVT repository:\n{MVT_REPOSITORY_URL}", + ) + + def _prepare_mvt_environment(self, allow_network: bool) -> Mapping[str, str]: + if self._mvt_temporary_config is not None: + raise RuntimeError("MVT temporary configuration already exists for an active operation") + temporary_config = tempfile.TemporaryDirectory(prefix="ios-developer-toolkit-mvt-") + self._mvt_temporary_config = temporary_config + return mvt_environment(base_environment(), Path(temporary_config.name), allow_network) + + def _clear_mvt_temporary_config(self) -> None: + temporary_config = self._mvt_temporary_config + self._mvt_temporary_config = None + if temporary_config is not None: + temporary_config.cleanup() + + def validate_mvt_from_ui(self) -> None: + if self._mvt_controller.is_running(): + QMessageBox.warning(self, "MVT Operation Running", "Stop or wait for the active MVT operation first.") + return + try: + executable = inspect_mvt_executable(self.mvt_executable_path()) + environment = self._prepare_mvt_environment(False) + except (MVTValidationError, OSError) as error: + self._clear_mvt_temporary_config() + self.mvt_validation_status.setText(f"Validation failed: {error}") + self.mvt_output.appendPlainText(f"MVT validation failed: {error}") + return + self._mvt_operation = "validate" + self._mvt_request = None + self._mvt_pending_executable = executable + arguments = mvt_version_arguments() + self.mvt_output.appendPlainText( + f"\n$ {executable.path} {shlex.join(arguments)}\n" + f"Executable SHA-256: {executable.sha256}" + ) + self.mvt_validation_status.setText("Validating the external MVT version without update or network checks…") + self._begin_operation( + "mvt", + self._host_operation_context( + "Validate MVT Installation", + "Backup", + "external MVT CLI with isolated temporary configuration", + (), + ), + ) + self._mvt_controller.start( + mvt_command(executable), + arguments, + environment, + Path.home(), + PROCESS_TERMINATE_GRACE_MS, + ) + self._update_mvt_controls() + + def run_mvt_analysis(self) -> None: + if self._mvt_controller.is_running(): + QMessageBox.warning(self, "MVT Operation Running", "Stop or wait for the active MVT operation first.") + return + installation = self._mvt_installation + if installation is None: + QMessageBox.critical(self, "MVT Not Validated", "Validate the selected MVT installation first.") + return + if not self.mvt_authorization_checkbox.isChecked() or not self.mvt_interpretation_checkbox.isChecked(): + QMessageBox.critical( + self, + "Acknowledgements Required", + "Confirm both the authorization/consent and interpretation boundaries before running MVT.", + ) + return + try: + request = create_mvt_analysis_request( + installation, + self.mvt_backup_path(), + self.mvt_output_path(), + self._mvt_ioc_paths, + self.mvt_fast_checkbox.isChecked(), + self.mvt_hashes_checkbox.isChecked(), + self.mvt_network_checkbox.isChecked(), + ) + except (MVTValidationError, OSError) as error: + QMessageBox.critical(self, "Invalid MVT Analysis Request", str(error)) + self.mvt_status.setText(f"MVT analysis request was rejected: {error}") + return + indicator_summary = ( + "none" if not request.ioc_files else ", ".join(path.name for path in request.ioc_files) + ) + warning = ( + f"Run external MVT {request.installation.version} backup analysis?\n\n" + f"Executable: {request.installation.executable.path}\n" + f"Executable SHA-256: {request.installation.executable.sha256}\n" + f"Backup: {request.backup.path}\n" + f"New output: {request.output}\n" + f"Indicators: {indicator_summary}\n" + f"Network access: {'allowed' if request.allow_network else 'blocked'}\n" + f"Fast mode: {'on' if request.fast else 'off'}\n" + f"Hash files: {'on' if request.hashes else 'off'}\n\n" + "The output can contain sensitive device, account, communication, browsing, and application records. " + "No findings does not prove the device is clean, safe, or uncompromised." + ) + profile = guided_action_safety("host-write") + if not self._confirm_action("Run External MVT Analysis", warning, profile, None): + return + self._start_mvt_analysis_request(request) + + def _start_mvt_analysis_request(self, request: MVTAnalysisRequest) -> None: + if self._mvt_controller.is_running(): + raise RuntimeError("Cannot start MVT analysis while another MVT process is running") + try: + request = create_mvt_analysis_request( + request.installation, + request.backup.path, + request.output, + request.ioc_files, + request.fast, + request.hashes, + request.allow_network, + ) + request.output.parent.mkdir(parents=True, exist_ok=True) + environment = self._prepare_mvt_environment(request.allow_network) + except (MVTValidationError, OSError) as error: + self._clear_mvt_temporary_config() + QMessageBox.critical( + self, + "Could Not Prepare MVT Analysis", + f"The confirmed MVT request changed or could not be prepared: {error}", + ) + self.mvt_status.setText(f"MVT analysis did not start: {error}") + return + arguments = mvt_analysis_arguments(request) + self._mvt_operation = "analyze" + self._mvt_request = request + self.mvt_output.appendPlainText( + f"\n[safety approval: host-write; authorization and interpretation acknowledged]\n" + f"$ {request.installation.executable.path} {shlex.join(arguments)}\n" + f"Network access: {'allowed' if request.allow_network else 'blocked'}" + ) + self.mvt_status.setText("MVT backup analysis is running. Use Stop to request termination.") + self._begin_operation( + "mvt", + self._host_operation_context( + "Analyze Backup with MVT", + "Backup", + "external MVT CLI with isolated temporary configuration", + (str(request.output),), + ), + ) + self._mvt_controller.start( + mvt_command(request.installation.executable), + arguments, + environment, + request.output.parent, + PROCESS_TERMINATE_GRACE_MS, + ) + self._update_mvt_controls() + + def _append_mvt_output(self, output: bytes) -> None: + self.mvt_output.moveCursor(QTextCursor.MoveOperation.End) + self.mvt_output.insertPlainText(output.decode("utf-8", errors="replace")) + + def _mvt_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Expected OperationResult, received {type(result_object).__name__}") + self._complete_operation("mvt", result_object) + operation = self._mvt_operation + request = self._mvt_request + self._mvt_operation = "" + self._mvt_request = None + self._clear_mvt_temporary_config() + combined = (result_object.stdout + result_object.stderr).decode("utf-8", errors="replace") + exit_label = "not available" if result_object.exit_code is None else str(result_object.exit_code) + if operation == "validate" and result_object.outcome == "succeeded": + pending_executable = self._mvt_pending_executable + if pending_executable is None: + raise RuntimeError("MVT validation completed without a pending executable") + try: + version = parse_mvt_version_output(combined) + except MVTValidationError as error: + self._mvt_installation = None + self.mvt_validation_status.setText(f"Validation failed: {error}") + self.mvt_status.setText("MVT installation validation failed; review the complete output.") + else: + self._mvt_installation = MVTInstallation(pending_executable, version) + self.mvt_validation_status.setText( + f"Validated external MVT {version}; executable SHA-256 {pending_executable.sha256}." + ) + self.mvt_status.setText("MVT is validated. Select and review the analysis request before running it.") + elif operation == "analyze" and result_object.outcome == "succeeded" and request is not None: + self.mvt_status.setText( + f"MVT completed and wrote its results under {request.output}. Review its logs and structured records; " + "absence of alerts or detected files does not prove the device is clean or uncompromised." + ) + else: + if operation == "validate": + self._mvt_installation = None + self.mvt_validation_status.setText( + f"Validation {result_object.outcome.replace('-', ' ')}; exit {exit_label}." + ) + elif operation == "analyze": + self.mvt_status.setText( + f"MVT analysis {result_object.outcome.replace('-', ' ')}; exit {exit_label}. " + "The isolated output may be partial and must not be treated as a completed analysis." + ) + else: + raise RuntimeError(f"MVT process completed with unknown operation: {operation!r}") + if operation == "analyze": + self.mvt_authorization_checkbox.setChecked(False) + self.mvt_interpretation_checkbox.setChecked(False) + self._mvt_pending_executable = None + if result_object.error_message: + self.mvt_output.appendPlainText(f"\nProcess error: {result_object.error_message}") + self.mvt_output.appendPlainText( + f"\n[finished: {result_object.outcome}; exit {exit_label}]\n" + ) + self._update_mvt_controls() + + def stop_mvt_analysis(self) -> None: + if not self._mvt_controller.is_running(): + return + self.mvt_status.setText("Stopping the external MVT process; any analysis output remains partial.") + self._mvt_controller.cancel() + + def _update_mvt_controls(self) -> None: + if not hasattr(self, "run_mvt_button"): + return + running = self._mvt_controller.is_running() + validated = self._mvt_installation is not None + acknowledged = ( + self.mvt_authorization_checkbox.isChecked() + and self.mvt_interpretation_checkbox.isChecked() + ) + request_paths_present = bool( + self.mvt_backup_field.text().strip() and self.mvt_output_field.text().strip() + ) + self.validate_mvt_button.setEnabled(not running and bool(self.mvt_executable_field.text().strip())) + self.run_mvt_button.setEnabled(not running and validated and acknowledged and request_paths_present) + self.stop_mvt_button.setEnabled(running) + for control in ( + self.mvt_executable_field, + self.mvt_backup_field, + self.mvt_output_field, + self.mvt_fast_checkbox, + self.mvt_hashes_checkbox, + self.mvt_network_checkbox, + self.mvt_authorization_checkbox, + self.mvt_interpretation_checkbox, + self.choose_mvt_executable_button, + self.find_mvt_executable_button, + self.choose_mvt_backup_button, + self.choose_mvt_output_button, + self.choose_mvt_iocs_button, + self.clear_mvt_iocs_button, + ): + control.setEnabled(not running) + self.open_mvt_output_button.setEnabled(not running) + + def open_mvt_output_directory(self) -> None: + try: + destination = self.mvt_output_path() + except MVTValidationError as error: + QMessageBox.critical(self, "Invalid MVT Output Path", str(error)) + return + if not destination.is_dir(): + QMessageBox.information( + self, + "MVT Result Folder Not Found", + f"The result folder does not exist yet:\n{destination}", + ) + return + QDesktopServices.openUrl(QUrl.fromLocalFile(str(destination))) + def _filter_command_presets(self) -> None: selected_identifier = self._current_preset.identifier if self._current_preset is not None else None category = self.command_category_combo.currentText() @@ -6143,6 +6803,7 @@ def closeEvent(self, event: QCloseEvent) -> None: sideload_running = self._sideload_controller.is_running() backup_running = self._backup_controller.is_running() collection_running = self._collection_controller.is_running() + mvt_running = self._mvt_controller.is_running() critical_processes = tuple( process for process in (self._location_process,) @@ -6155,13 +6816,14 @@ def closeEvent(self, event: QCloseEvent) -> None: or sideload_running or backup_running or collection_running + or mvt_running or self._console_controller.is_running() or critical_processes ) if active_operations and not self._close_after_collection: should_close = self._confirm( "Stop Active Operations?", - "A DDI, evidence, app, backup, Location Lab, or Command Center operation is still running. " + "A DDI, evidence, app, backup, MVT, Location Lab, or Command Center operation is still running. " "Stop it, allow cleanup/finalization, and close the app?", ) if not should_close: @@ -6186,6 +6848,8 @@ def closeEvent(self, event: QCloseEvent) -> None: self._ipa_inspection_controller.shutdown(10000, 3000) self._sideload_controller.shutdown(10000, 3000) self._backup_controller.shutdown(10000, 3000) + self._mvt_controller.shutdown(10000, 3000) + self._clear_mvt_temporary_config() self._collection_controller.shutdown(10000, 3000) capability_process = self._capability_process if capability_process is not None and capability_process.state() != QProcess.ProcessState.NotRunning: diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 4c6899c..460de24 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -2,6 +2,7 @@ import os import sys +import tempfile import time from collections.abc import Callable, Sequence from pathlib import Path @@ -85,6 +86,7 @@ def run_smoke_test(arguments: Sequence[str]) -> int: from ios_developer_toolkit.action_palette import ActionPaletteDialog from ios_developer_toolkit.app import MainWindow from ios_developer_toolkit.backup_protocol import BackupRequest + from ios_developer_toolkit.mvt_connector import create_mvt_analysis_request from ios_developer_toolkit.operation_history import OperationHistoryDialog application = QApplication(["ios-developer-toolkit-smoke-test"]) @@ -209,6 +211,69 @@ def run_smoke_test(arguments: Sequence[str]) -> int: if action_palette_item.data(Qt.ItemDataRole.UserRole) != "utility:session-activity": raise RuntimeError("GUI action-palette search selected an unexpected entry") action_palette_dialog.close() + with tempfile.TemporaryDirectory() as mvt_temporary_directory: + mvt_root = Path(mvt_temporary_directory) + mvt_executable = mvt_root / "mvt-ios" + mvt_executable.write_text( + "#!/bin/sh\n" + "for argument in \"$@\"; do\n" + " if [ \"$argument\" = \"version\" ]; then\n" + " printf \"MVT - Mobile Verification Toolkit\\nVersion: 2026.9.21\\n\"\n" + " exit 0\n" + " fi\n" + "done\n" + "output=\"\"\n" + "while [ \"$#\" -gt 0 ]; do\n" + " if [ \"$1\" = \"--output\" ]; then\n" + " shift\n" + " output=\"$1\"\n" + " fi\n" + " shift\n" + "done\n" + "mkdir -p \"$output\"\n" + "printf \"{\\\"synthetic\\\":true}\\n\" > \"$output/info.json\"\n" + "printf \"Synthetic MVT analysis completed\\n\"\n", + encoding="utf-8", + ) + mvt_executable.chmod(0o700) + mvt_backup = mvt_root / "backup" + mvt_backup.mkdir() + (mvt_backup / "Manifest.db").write_bytes(b"synthetic manifest") + (mvt_backup / "Info.plist").write_bytes(b"synthetic info") + mvt_output = mvt_root / "analysis" + window.mvt_executable_field.setText(str(mvt_executable)) + window.validate_mvt_from_ui() + mvt_validation_deadline = time.monotonic() + 10 + while window._mvt_controller.is_running() and time.monotonic() < mvt_validation_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._mvt_controller.is_running() or window._mvt_installation is None: + raise RuntimeError(f"GUI MVT validation did not complete: {window.mvt_output.toPlainText()}") + if window._mvt_installation.version != "2026.9.21": + raise RuntimeError("GUI MVT validation retained an unexpected version") + request = create_mvt_analysis_request( + window._mvt_installation, + mvt_backup, + mvt_output, + (), + False, + False, + False, + ) + window._start_mvt_analysis_request(request) + mvt_analysis_deadline = time.monotonic() + 10 + while window._mvt_controller.is_running() and time.monotonic() < mvt_analysis_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + if window._mvt_controller.is_running(): + window._mvt_controller.cancel() + raise RuntimeError("GUI MVT analysis did not complete within its bounded smoke-test window") + if not (mvt_output / "info.json").is_file(): + raise RuntimeError(f"GUI MVT analysis did not create isolated output: {window.mvt_output.toPlainText()}") + if "does not prove" not in window.mvt_status.text(): + raise RuntimeError("GUI MVT completion omitted the no-clean-device interpretation boundary") expected_shortcuts = { "shortcutRetryDeviceScan", "shortcutShowActionPalette", diff --git a/ios_developer_toolkit/mvt_connector.py b/ios_developer_toolkit/mvt_connector.py new file mode 100644 index 0000000..d2a484f --- /dev/null +++ b/ios_developer_toolkit/mvt_connector.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +import hashlib +import os +import plistlib +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Mapping + +from ios_developer_toolkit.runtime import ExecutableCommand + + +MVT_REPOSITORY_URL = "https://github.com/mvt-project/mvt" +MVT_INSTALLATION_URL = "https://docs.mvt.re/en/latest/install/" +MVT_BACKUP_GUIDE_URL = "https://docs.mvt.re/en/latest/ios/backup/check/" +MVT_ENVIRONMENT_KEYS_TO_REMOVE = ( + "MVT_ANDROID_BACKUP_PASSWORD", + "MVT_HASH_FILES", + "MVT_IOS_BACKUP_PASSWORD", + "MVT_PROFILE", + "MVT_STIX2", + "MVT_VT_API_KEY", +) + + +class MVTValidationError(ValueError): + """Raised when an external MVT analysis request is unsafe or incomplete.""" + + +@dataclass(frozen=True) +class MVTExecutable: + path: Path + sha256: str + + +@dataclass(frozen=True) +class MVTInstallation: + executable: MVTExecutable + version: str + + +@dataclass(frozen=True) +class MVTBackup: + path: Path + encrypted: bool | None + + +@dataclass(frozen=True) +class MVTAnalysisRequest: + installation: MVTInstallation + backup: MVTBackup + output: Path + ioc_files: tuple[Path, ...] + fast: bool + hashes: bool + allow_network: bool + + +def mvt_setup_commands() -> tuple[str, ...]: + return ( + "brew install python3 pipx sqlite3", + "pipx ensurepath", + "pipx install mvt", + ) + + +def discover_mvt_executables(home: Path, path_environment: str) -> tuple[Path, ...]: + candidates: list[Path] = [] + path_entries = tuple(Path(entry) for entry in path_environment.split(os.pathsep) if entry) + locations = ( + *(entry / "mvt-ios" for entry in path_entries), + home.expanduser() / ".local" / "bin" / "mvt-ios", + Path("/opt/homebrew/bin/mvt-ios"), + Path("/usr/local/bin/mvt-ios"), + ) + for location in locations: + expanded = location.expanduser() + if expanded.is_file() and os.access(expanded, os.X_OK): + resolved = expanded.resolve() + if resolved not in candidates: + candidates.append(resolved) + return tuple(candidates) + + +def _sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as input_file: + for block in iter(lambda: input_file.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def inspect_mvt_executable(path: Path) -> MVTExecutable: + expanded = path.expanduser() + if not expanded.is_absolute(): + raise MVTValidationError(f"MVT executable must be an absolute path: {expanded}") + resolved = expanded.resolve() + if not resolved.is_file(): + raise MVTValidationError(f"MVT executable does not exist: {resolved}") + if not os.access(resolved, os.X_OK): + raise MVTValidationError(f"MVT executable is not executable: {resolved}") + return MVTExecutable(resolved, _sha256_file(resolved)) + + +def mvt_command(executable: MVTExecutable) -> ExecutableCommand: + environment_arguments = tuple( + argument + for key in MVT_ENVIRONMENT_KEYS_TO_REMOVE + for argument in ("-u", key) + ) + return ExecutableCommand(Path("/usr/bin/env"), (*environment_arguments, str(executable.path))) + + +def mvt_version_arguments() -> tuple[str, ...]: + return ("--disable-update-check", "--disable-indicator-update-check", "version") + + +def parse_mvt_version_output(output: str) -> str: + without_ansi = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", output) + match = re.search(r"(?im)^\s*Version:\s*([A-Za-z0-9][A-Za-z0-9._+-]*)\s*$", without_ansi) + if match is None: + raise MVTValidationError("MVT version output did not contain a recognizable 'Version:' line") + return match.group(1) + + +def _is_backup_folder(path: Path) -> bool: + return (path / "Manifest.db").is_file() and (path / "Info.plist").is_file() + + +def _resolved_backup_folder(path: Path) -> Path: + expanded = path.expanduser() + if not expanded.is_absolute(): + raise MVTValidationError(f"MVT backup path must be absolute: {expanded}") + resolved = expanded.resolve() + if not resolved.is_dir(): + raise MVTValidationError(f"MVT backup directory does not exist: {resolved}") + if _is_backup_folder(resolved): + return resolved + candidates = tuple( + candidate + for candidate in sorted(resolved.iterdir()) + if candidate.is_dir() and _is_backup_folder(candidate) + ) + if len(candidates) == 1: + return candidates[0] + if len(candidates) > 1: + raise MVTValidationError( + f"Multiple iTunes-style backups were found under {resolved}; choose one folder containing Manifest.db and Info.plist" + ) + raise MVTValidationError( + f"No iTunes-style backup was found at {resolved}; expected Manifest.db and Info.plist" + ) + + +def _backup_encryption_state(backup: Path) -> bool | None: + manifest_path = backup / "Manifest.plist" + if not manifest_path.is_file(): + return None + try: + with manifest_path.open("rb") as manifest_file: + manifest = plistlib.load(manifest_file) + except (OSError, plistlib.InvalidFileException) as error: + raise MVTValidationError(f"Could not read backup encryption metadata at {manifest_path}: {error}") from error + if not isinstance(manifest, Mapping): + raise MVTValidationError(f"Backup Manifest.plist root is not a dictionary: {manifest_path}") + encrypted = manifest.get("IsEncrypted") + if encrypted is None: + return None + if not isinstance(encrypted, bool): + raise MVTValidationError(f"Backup Manifest.plist has a non-boolean IsEncrypted value: {manifest_path}") + return encrypted + + +def inspect_mvt_backup(path: Path) -> MVTBackup: + resolved = _resolved_backup_folder(path) + encrypted = _backup_encryption_state(resolved) + if encrypted is True: + raise MVTValidationError( + "The selected backup is encrypted. Decrypt a protected working copy with MVT outside this toolkit, then select that copy. " + "The toolkit does not request, retain, transmit, or place backup passwords in command arguments." + ) + return MVTBackup(resolved, encrypted) + + +def validate_mvt_output(path: Path, backup: MVTBackup) -> Path: + expanded = path.expanduser() + if not expanded.is_absolute(): + raise MVTValidationError(f"MVT output path must be absolute: {expanded}") + resolved = expanded.resolve(strict=False) + if resolved == Path(resolved.anchor): + raise MVTValidationError(f"MVT output cannot be a filesystem root: {resolved}") + if resolved.exists(): + raise MVTValidationError( + f"MVT output already exists: {resolved}. Choose a new empty analysis path so results cannot mix with an earlier run." + ) + if resolved.is_relative_to(backup.path): + raise MVTValidationError( + f"MVT output cannot be inside the source backup: {resolved}" + ) + return resolved + + +def validate_mvt_ioc_files(paths: tuple[Path, ...]) -> tuple[Path, ...]: + supported_suffixes = {".json", ".stix", ".stix2"} + validated: list[Path] = [] + for path in paths: + expanded = path.expanduser() + if not expanded.is_absolute(): + raise MVTValidationError(f"MVT IOC path must be absolute: {expanded}") + resolved = expanded.resolve() + if not resolved.is_file(): + raise MVTValidationError(f"MVT IOC file does not exist: {resolved}") + if resolved.suffix.casefold() not in supported_suffixes: + raise MVTValidationError( + f"MVT IOC file must use .stix, .stix2, or .json: {resolved}" + ) + if resolved not in validated: + validated.append(resolved) + return tuple(validated) + + +def create_mvt_analysis_request( + installation: MVTInstallation, + backup_path: Path, + output_path: Path, + ioc_paths: tuple[Path, ...], + fast: bool, + hashes: bool, + allow_network: bool, +) -> MVTAnalysisRequest: + current_executable = inspect_mvt_executable(installation.executable.path) + if current_executable.sha256 != installation.executable.sha256: + raise MVTValidationError("The MVT executable changed after validation; validate the installation again") + backup = inspect_mvt_backup(backup_path) + output = validate_mvt_output(output_path, backup) + ioc_files = validate_mvt_ioc_files(ioc_paths) + return MVTAnalysisRequest(installation, backup, output, ioc_files, fast, hashes, allow_network) + + +def mvt_analysis_arguments(request: MVTAnalysisRequest) -> tuple[str, ...]: + arguments = [ + "--disable-update-check", + "--disable-indicator-update-check", + "check-backup", + "--output", + str(request.output), + ] + if request.fast: + arguments.append("--fast") + if request.hashes: + arguments.append("--hashes") + for ioc_file in request.ioc_files: + arguments.extend(("--iocs", str(ioc_file))) + arguments.append(str(request.backup.path)) + return tuple(arguments) + + +def mvt_environment( + base: Mapping[str, str], + config_directory: Path, + allow_network: bool, +) -> Mapping[str, str]: + resolved_config = config_directory.expanduser().resolve() + if not resolved_config.is_dir(): + raise MVTValidationError(f"MVT temporary configuration directory does not exist: {resolved_config}") + environment = { + key: value + for key, value in base.items() + if key not in MVT_ENVIRONMENT_KEYS_TO_REMOVE + } + environment["MVT_CONFIG_FOLDER"] = str(resolved_config) + environment["MVT_NETWORK_ACCESS_ALLOWED"] = "true" if allow_network else "false" + environment["MVT_NETWORK_TIMEOUT"] = "15" + environment["NO_COLOR"] = "1" + environment["PYTHONUNBUFFERED"] = "1" + return environment diff --git a/tests/test_mvt_connector.py b/tests/test_mvt_connector.py new file mode 100644 index 0000000..cbc633a --- /dev/null +++ b/tests/test_mvt_connector.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import os +import plistlib +import stat +import tempfile +import unittest +from pathlib import Path + +from ios_developer_toolkit.mvt_connector import ( + MVTAnalysisRequest, + MVTBackup, + MVTInstallation, + MVTValidationError, + create_mvt_analysis_request, + inspect_mvt_backup, + inspect_mvt_executable, + mvt_analysis_arguments, + mvt_command, + mvt_environment, + parse_mvt_version_output, + validate_mvt_output, +) + + +class MVTConnectorTests(unittest.TestCase): + def test_inspects_executable_and_removes_inherited_secret_routes(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + executable_path = root / "mvt-ios" + executable_path.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + executable_path.chmod(executable_path.stat().st_mode | stat.S_IXUSR) + executable = inspect_mvt_executable(executable_path) + command = mvt_command(executable) + + self.assertEqual(len(executable.sha256), 64) + self.assertEqual(command.program, Path("/usr/bin/env")) + self.assertIn("MVT_IOS_BACKUP_PASSWORD", command.prefix_arguments) + self.assertEqual(command.prefix_arguments[-1], str(executable_path.resolve())) + + def test_parses_current_version_output_and_rejects_unrecognized_output(self) -> None: + self.assertEqual( + parse_mvt_version_output("MVT - Mobile Verification Toolkit\nVersion: 2026.9.21\n"), + "2026.9.21", + ) + with self.assertRaises(MVTValidationError): + parse_mvt_version_output("unknown program\n") + + def test_resolves_one_backup_and_rejects_encrypted_input(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + backup = root / "device-backup" + backup.mkdir() + (backup / "Manifest.db").write_bytes(b"database") + (backup / "Info.plist").write_bytes(plistlib.dumps({"Device Name": "Example"})) + + inspection = inspect_mvt_backup(root) + self.assertEqual(inspection.path, backup.resolve()) + self.assertIsNone(inspection.encrypted) + + (backup / "Manifest.plist").write_bytes(plistlib.dumps({"IsEncrypted": True})) + with self.assertRaises(MVTValidationError): + inspect_mvt_backup(backup) + + def test_requires_new_output_outside_the_backup(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + backup_path = root / "backup" + backup_path.mkdir() + backup = MVTBackup(backup_path.resolve(), False) + isolated_output = validate_mvt_output(root / "analysis", backup) + self.assertEqual(isolated_output, (root / "analysis").resolve()) + with self.assertRaises(MVTValidationError): + validate_mvt_output(backup_path / "analysis", backup) + existing = root / "existing" + existing.mkdir() + with self.assertRaises(MVTValidationError): + validate_mvt_output(existing, backup) + + def test_builds_offline_analysis_without_password_or_implicit_iocs(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + executable_path = root / "mvt-ios" + executable_path.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + executable_path.chmod(executable_path.stat().st_mode | stat.S_IXUSR) + executable = inspect_mvt_executable(executable_path) + installation = MVTInstallation(executable, "2026.9.21") + backup_path = root / "backup" + backup_path.mkdir() + (backup_path / "Manifest.db").write_bytes(b"database") + (backup_path / "Info.plist").write_bytes(plistlib.dumps({})) + ioc_path = root / "indicators.stix2" + ioc_path.write_text("{}", encoding="utf-8") + request = create_mvt_analysis_request( + installation, + backup_path, + root / "analysis", + (ioc_path,), + True, + True, + False, + ) + arguments = mvt_analysis_arguments(request) + + self.assertIsInstance(request, MVTAnalysisRequest) + self.assertIn("--fast", arguments) + self.assertIn("--hashes", arguments) + self.assertIn(str(ioc_path.resolve()), arguments) + self.assertNotIn("password", " ".join(arguments).casefold()) + + config_directory = root / "config" + config_directory.mkdir() + environment = mvt_environment( + { + "PATH": os.environ.get("PATH", ""), + "MVT_IOS_BACKUP_PASSWORD": "secret", + "MVT_STIX2": "/unexpected", + }, + config_directory, + False, + ) + self.assertNotIn("MVT_IOS_BACKUP_PASSWORD", environment) + self.assertNotIn("MVT_STIX2", environment) + self.assertEqual(environment["MVT_NETWORK_ACCESS_ALLOWED"], "false") + + +if __name__ == "__main__": + unittest.main() From 8770d78a74455808aa826250510b1302843f8d89 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:56:15 -0700 Subject: [PATCH 12/16] Add optional ecosystem tool adapters --- README.md | 45 ++- SOURCE_AVAILABILITY.md | 2 +- THIRD_PARTY_NOTICES.md | 2 +- docs/PRODUCT_AUDIT_2026-09-21.md | 5 +- docs/screenshots/ecosystem-tools.png | Bin 0 -> 149380 bytes ios_developer_toolkit/app.py | 468 +++++++++++++++++++++++- ios_developer_toolkit/entrypoint.py | 52 +++ ios_developer_toolkit/external_tools.py | 247 +++++++++++++ ios_developer_toolkit/file_integrity.py | 12 + ios_developer_toolkit/gui_pages.py | 2 +- ios_developer_toolkit/mvt_connector.py | 12 +- tests/test_external_tools.py | 114 ++++++ 12 files changed, 937 insertions(+), 24 deletions(-) create mode 100644 docs/screenshots/ecosystem-tools.png create mode 100644 ios_developer_toolkit/external_tools.py create mode 100644 ios_developer_toolkit/file_integrity.py create mode 100644 tests/test_external_tools.py diff --git a/README.md b/README.md index 432653d..d4e044f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ![iOS Developer Toolkit Home workspace](docs/screenshots/home.png) -The current interface organizes one trusted device connection into 12 focused workspaces. It mounts modern DDIs, checks device and developer-service readiness, runs validated `pymobiledevice3` presets, exposes the installed command help, simulates test locations, streams three forms of device logs, captures packets, inspects and installs eligible IPAs, inventories apps, creates encrypted backups, launches an isolated UFADE acquisition, hands decrypted backups to an external MVT analysis, and builds hashed evidence cases. +The current interface organizes Apple-device work into 13 focused workspaces. It mounts modern DDIs, checks device and developer-service readiness, runs validated `pymobiledevice3` presets, exposes the installed command help, simulates test locations, streams three forms of device logs, captures packets, inspects and installs eligible IPAs, inventories apps, creates encrypted backups, launches an isolated UFADE acquisition, hands decrypted backups to an external MVT analysis, validates optional ecosystem adapters, and builds hashed evidence cases. The screenshots use an illustrative device name, model, version, build, and UDID. They contain no real device capture, account identifier, backup, credential, or case evidence. @@ -43,6 +43,7 @@ The screenshots use an illustrative device name, model, version, build, and UDID - [Backup](#backup) - [Sideload IPA](#sideload-ipa) - [Evidence Capture](#evidence-capture) + - [Ecosystem Tools](#ecosystem-tools) - [Man Pages](#man-pages) - [Scope and Safety](#scope-and-safety) - [Eligible Action Palette](#eligible-action-palette) @@ -89,6 +90,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - **Action Palette** (`⌘ K`) searches all workspaces, guided presets, utilities, and currently eligible read actions while withholding device-only operations until a physical target is selected; - **Session Activity** correlates completed typed operations with workspace, target, transport, exact argument vector, timing, terminal status, prerequisite snapshot, output paths, and output hashes without automatically persisting raw command output; - **MVT Analysis** validates a user-installed `mvt-ios` executable and runs a consented decrypted-backup analysis with isolated output, opt-in indicators, network access off by default, no password input, and no clean-device verdict; +- **Ecosystem Tools** validates user-selected go-ios, idb Companion, and ipsw executables by path, SHA-256, and version/build identity, then enables one bounded read-only inventory probe per adapter; - the desktop UI starts independently of the MobileBackup2 transport, and device discovery consumes output both while the child process runs and after it exits, so a fast successful `usbmux list` result is not lost before the picker is updated; - **Demo Mode** shows a prominently labeled simulated iPhone for walkthroughs and screenshots, while deliberately withholding a selected physical-device target and disabling device operations; - the manual **Capability Matrix** reports host, trust, Developer Mode, DDI, tunnel, DVT, CoreDevice, and related readiness as separate bounded results, then compares completed local probes across real devices without retaining raw UDIDs; @@ -96,10 +98,10 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, external MVT analysis, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one workbench; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 121 tests, embedded CLI checks, a 110-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 126 tests, embedded CLI checks, a 134-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. -The README contains 21 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. +The README contains 22 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. | Prepare the device and DDI | Observe live services | Run guided commands | |---|---|---| @@ -140,6 +142,7 @@ The README contains 21 sanitized screenshots. The six views below provide a quic | **Backup** | Run MobileBackup2, launch separate UFADE, or analyze a decrypted backup with external MVT | No | Encrypted backup, external acquisition, or isolated forensic records | | **Sideload IPA** | Inspect a local IPA before attempting installation | No DDI for normal install | Archive, provisioning, and signature report | | **Evidence Capture** | Correlate snapshots, timed streams, screenshots, crashes, and PCAP | Partial coverage without it | Timestamped case, coverage states, manifest, SHA-256 inventory | +| **Ecosystem Tools** | Validate optional go-ios, idb Companion, and ipsw installations; run bounded inventory probes | Uses each external tool's own requirements | Resolved path, SHA-256, version/build, raw session output | | **Man Pages** | Browse 59 command routes instantly and request live help on demand | No | Version-matched syntax rather than copied examples | | **Scope & Safety** | Keep access and interpretation limits visible | No | Operational boundaries inside the app | @@ -147,7 +150,7 @@ The README contains 21 sanitized screenshots. The six views below provide a quic The interface gives named controls and descriptions to the primary device picker, workspace navigation, command and help browsers, app inventory, capability results, reports, and the keyboard alternative to Location Lab's mouse map. The offline map is intentionally skipped in keyboard tab order; use the coordinate importer or latitude and longitude fields instead. -Use **Keyboard Shortcuts** in the window header, or press `⌘ /`, for the complete reference. The most useful shortcuts are `⌘ K` to open the eligible Action Palette, `⌘ L` to focus workspace navigation, `⌘ F` to focus contextual search, `⌘ R` to retry discovery, `⌘ 1` through `⌘ 0` to open the first ten workspaces, `⌘ ⇧ M` for Man Pages, and `⌘ ⇧ S` for Scope & Safety. `⌘ ⌥ ←` and `⌘ ⌥ →` move between workspaces. Tab, Shift-Tab, Space, Return, and Arrow keys retain their standard Qt behavior. Shortcuts never skip device-action confirmation or typed acknowledgements. +Use **Keyboard Shortcuts** in the window header, or press `⌘ /`, for the complete reference. The most useful shortcuts are `⌘ K` to open the eligible Action Palette, `⌘ L` to focus workspace navigation, `⌘ F` to focus contextual search, `⌘ R` to retry discovery, `⌘ 1` through `⌘ 0` to open the first ten workspaces, `⌘ ⇧ E` for Ecosystem Tools, `⌘ ⇧ M` for Man Pages, and `⌘ ⇧ S` for Scope & Safety. `⌘ ⌥ ←` and `⌘ ⌥ →` move between workspaces. Tab, Shift-Tab, Space, Return, and Arrow keys retain their standard Qt behavior. Shortcuts never skip device-action confirmation or typed acknowledgements. ### Eligible Action Palette @@ -155,13 +158,13 @@ Use **Keyboard Shortcuts** in the window header, or press `⌘ /`, for the compl Press `⌘ K` or use **Action Palette** below the workspace list to search the interface without memorizing where an operation lives. The result set is computed from the current app state: host-only presets remain available while disconnected, device-only presets appear only after a physical target is selected, and actions disappear while their process controller is busy. Workspace and utility navigation is always available. -Choosing a guided preset opens Command Center with that preset selected and its exact command, prerequisites, risk, and confirmation path visible. It does not execute the command. Direct entries are limited to eligible read actions such as discovery, Developer Mode status, developer-image listing, CoreDevice or RVI details, the Capability Matrix, app inventory, backup-encryption status, Command Drift, and Man Pages help. State is checked again at activation so a device disconnect or newly busy controller cannot use a stale palette result. +Choosing a guided preset opens Command Center with that preset selected and its exact command, prerequisites, risk, and confirmation path visible. It does not execute the command. Direct entries are limited to eligible read actions such as discovery, Developer Mode status, developer-image listing, CoreDevice or RVI details, the Capability Matrix, app inventory, backup-encryption status, Command Drift, Man Pages help, and already-validated external-tool probes. State is checked again at activation so a device disconnect, changed external binary, or newly busy controller cannot use a stale palette result. ### Session Activity and operation manifests ![Session Activity and structured operation manifest](docs/screenshots/session-activity.png) -**Session Activity** in the sidebar shows completed operations from typed controllers in the current app session. The journal currently covers Device & DDI and Apple handoffs, Command Center, Installed Apps, IPA inspection and installation, MobileBackup2, Evidence Capture, and live Man Pages. Periodic discovery, Command Drift's internal per-route probes, and raw live-log streams are intentionally excluded; those have their own aggregate reports or evidence sidecars. +**Session Activity** in the sidebar shows completed operations from typed controllers in the current app session. The journal currently covers Device & DDI and Apple handoffs, Command Center, Installed Apps, IPA inspection and installation, MobileBackup2, MVT, Ecosystem Tools, Evidence Capture, and live Man Pages. Periodic discovery, Command Drift's internal per-route probes, and raw live-log streams are intentionally excluded; those have their own aggregate reports or evidence sidecars. Each record distinguishes the workspace and target from the transport, exact argument vector, start and finish timestamps, duration, terminal process outcome, exit code, process error, prerequisite-state snapshot, declared output paths, and SHA-256 plus byte count for each captured output channel. The manifest does **not** embed stdout or stderr. The UI keeps at most 250 records in memory and writes nothing automatically. @@ -761,6 +764,30 @@ The collector retries failed snapshots once, keeps the final artifact and a comp **Stop/Finalize** sends a graceful stop request and gives the collector up to two minutes to close streams, write `manifest.json`, and regenerate `SHA256SUMS.txt`. Closing the application while a collection is active waits for that finalization instead of immediately killing the worker. If finalization is not confirmed, the guided case remains active for review or retry rather than being labeled complete. +### Ecosystem Tools + +![Optional ecosystem tool adapters](docs/screenshots/ecosystem-tools.png) + +Ecosystem Tools is an interoperability surface for three independently maintained MIT-licensed projects. Nothing is bundled, auto-downloaded, auto-updated, imported as a Python dependency, or treated as trusted merely because it was found on `PATH`. + +| Adapter | Installation command shown by the app | Validation | Bounded probe | +|---|---|---|---| +| [go-ios](https://github.com/danielpaulus/go-ios) | `npm install -g go-ios` | `ios --version` | `ios list --details` | +| [Meta idb](https://github.com/facebook/idb) | `brew install facebook/fb/idb` | `idb_companion --version` | `idb_companion --list 1` | +| [blacktop ipsw](https://github.com/blacktop/ipsw) | `brew install blacktop/tap/ipsw` | `ipsw version` | `ipsw idev list` | + +For each adapter: + +1. Click **Find Installed** or choose an executable manually. +2. Review the resolved path, SHA-256, version/build arguments, and third-party execution warning. +3. Validate the executable. A changed hash invalidates the installation before any probe. +4. Review the probe's exact argument vector and independent target-selection boundary. +5. Run or stop the 30-second read-only probe. The raw output stays in the session and the typed result appears in Session Activity. + +The adapters remove inherited target-routing and known credential variables such as `IDB_UDID`, `IDB_COMPANION`, `P12_PASSWORD`, and IPSW/GitHub API tokens before launch. That prevents an invisible environment value from selecting a remote target or supplying a credential to these specific probes. It is not a sandbox or a guarantee that a third-party executable performs no other I/O. + +go-ios is a separate device protocol implementation and may require its own tunnel setup for modern iOS. idb is a client/companion automation system whose current companion reports build identity rather than a semantic client version. ipsw is primarily a firmware and Apple-platform research suite; only its local `idev list` surface is exposed here. The toolkit does not reconcile their inventories with its selected-device state, infer that one tool is more authoritative, or expose mutating commands from these projects. + ### Man Pages ![Man Pages and Possibilities workspace](docs/screenshots/man-pages.png) @@ -963,6 +990,7 @@ Treat these outputs as potentially sensitive: - screenshots, AFC listings, GPX routes, and simulated coordinates; - MobileBackup2 and UFADE acquisitions; - MVT source backups, indicator files, command logs, and analysis results; +- go-ios, idb, and ipsw inventory output, which can include device or simulator identifiers; - IPA provisioning records and signing identities. The repository `.gitignore` excludes the toolkit's common backup, case, capture, crash, log, packet, GPX, UFADE, MVT, DDI, certificate, profile, and IPA artifact patterns. That is a publication guard, not an access-control system. Store evidence outside a public checkout when possible, restrict filesystem permissions, encrypt sensitive archives, and review every staged file before committing. @@ -1084,6 +1112,8 @@ Install or update Xcode if the candidate is absent. The toolkit requires the exp │ ├── collection_process.py # evidence-worker lifecycle and graceful finalization │ ├── collection_protocol.py # validated collector JSON-line events │ ├── device_compatibility.py # redacted local real-device readiness history +│ ├── external_tools.py # optional executable provenance and probe policies +│ ├── file_integrity.py # shared streaming file SHA-256 helper │ ├── gui_pages.py # stateless Home, Live Logs, Safety pages and styling │ ├── installed_apps.py # app inventory validation and formatting │ ├── interactive_process.py # typed user-stoppable process lifecycle controller @@ -1128,7 +1158,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 121 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 110-button offscreen GUI smoke test, verifies live help from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 126 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 134-button offscreen GUI smoke test, verifies live help and a synthetic external-adapter lifecycle from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. @@ -1143,6 +1173,7 @@ Windows and Linux would require a separate host implementation or deliberately i - [Apple Developer Mode documentation](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device) describes the on-device security workflow. - [`UFADE`](https://github.com/prosch88/UFADE) is supported only as a separately installed and independently licensed external provider. - [`MVT`](https://github.com/mvt-project/mvt) is supported only as a separately installed external analysis provider under its own license and warning model. +- [`go-ios`](https://github.com/danielpaulus/go-ios), [`idb`](https://github.com/facebook/idb), and [`ipsw`](https://github.com/blacktop/ipsw) are supported only through user-selected, separately installed MIT-licensed executables; no source or binary from these projects is bundled. - [`ostrace`](https://github.com/BerkayCaglar/ostrace) informed live-log interaction design; no GPL source is copied, imported, or linked into this MIT project. - [`LocationSimulator`](https://github.com/Schlaubischlump/LocationSimulator) informed the offline map/teleport workflow. Its GPL source is not copied or linked, and its public backend does not support iOS 17 or later. - [Natural Earth](https://www.naturalearthdata.com/) provides the public-domain 1:110m land geometry rendered into the bundled offline Location Lab map. diff --git a/SOURCE_AVAILABILITY.md b/SOURCE_AVAILABILITY.md index 833d23b..50c8fc9 100644 --- a/SOURCE_AVAILABILITY.md +++ b/SOURCE_AVAILABILITY.md @@ -14,4 +14,4 @@ The release-critical upstream source locations and license information are recor PySide6/Qt, Nuitka, CPython, and every other dependency remain subject to their own terms. Consult the generated `Contents/Resources/Licenses/` inventory in the application and the matching SBOM for the exact package set. This document is an availability and attribution statement, not legal advice. -Optional UFADE and MVT integrations launch user-managed external installations. Their source is not part of the application bundle or release SBOM; consult their upstream repositories and licenses for the exact external version selected by the operator. +Optional UFADE, MVT, go-ios, idb, and ipsw integrations launch user-managed external installations. Their source is not part of the application bundle or release SBOM; consult their upstream repositories and licenses for the exact external version selected by the operator. The Ecosystem Tools workspace records the resolved executable path, SHA-256, and reported version or build identity for go-ios, idb Companion, and ipsw before enabling a probe. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 04934ce..6f2d49b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -20,7 +20,7 @@ Each architecture-specific release also contains: The generated package inventory is intentionally more detailed than this summary and includes transitive Python dependencies. A package whose wheel does not contain a license text is identified as such in the inventory and linked to its declared project source when available. -UFADE and MVT are optional, separately installed external providers. The toolkit does not bundle either project. MVT remains subject to the [MVT License](https://license.mvt.re/1.1/) and its consent and interpretation boundaries. Other projects named in the README as design references are not copied, imported, or linked unless the README explicitly says otherwise. +UFADE, MVT, go-ios, idb, and ipsw are optional, separately installed external providers. The toolkit does not bundle those projects. MVT remains subject to the [MVT License](https://license.mvt.re/1.1/) and its consent and interpretation boundaries. [go-ios](https://github.com/danielpaulus/go-ios), [idb](https://github.com/facebook/idb), and [ipsw](https://github.com/blacktop/ipsw) each declare the MIT License in their upstream repositories. Their adapter only validates and launches a user-selected executable; their source and binary remain outside this project and its release SBOM. Other projects named in the README as design references are not copied, imported, or linked unless the README explicitly says otherwise. See [SOURCE_AVAILABILITY.md](SOURCE_AVAILABILITY.md) for the project source location, matching tagged source, and upstream source locations for bundled third-party components. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index c46a1ed..353f588 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -10,7 +10,7 @@ The correct next investment is therefore a **reliable startup and device-discove ## What exists today -The product has twelve workspaces: Home, Device & DDI, Capability Matrix, Location Lab, Live Logs, Command Center, Installed Apps, Backup, Sideload IPA, Evidence Capture, Man Pages, and Scope & Safety. It currently provides 49 declarative guided command presets, a live-help/command-drift check, DDI mounting, RSD/CoreDevice/DVT checks, GPX location simulation with cleanup, separate Unified/syslog/oslog windows, installed app inventory, encrypted MobileBackup2 workflow, isolated UFADE launch, guided external MVT analysis, IPA inspection and installation, RVI/PCAP and artifact collection, guided case intake, support bundles, compatibility history, and keyboard-first navigation. +The product has thirteen workspaces: Home, Device & DDI, Capability Matrix, Location Lab, Live Logs, Command Center, Installed Apps, Backup, Sideload IPA, Evidence Capture, Ecosystem Tools, Man Pages, and Scope & Safety. It currently provides 49 declarative guided command presets, a live-help/command-drift check, DDI mounting, RSD/CoreDevice/DVT checks, GPX location simulation with cleanup, separate Unified/syslog/oslog windows, installed app inventory, encrypted MobileBackup2 workflow, isolated UFADE launch, guided external MVT analysis, provenance-checked go-ios/idb/ipsw adapters, IPA inspection and installation, RVI/PCAP and artifact collection, guided case intake, support bundles, compatibility history, and keyboard-first navigation. The repository is a Python 3.10+ PySide6 project with a bundled `pymobiledevice3` runtime model. `ios_developer_toolkit/app.py` is a 5,600+ line `MainWindow`, while domain modules cover capability probing, collectors, live logs, location testing, IPA inspection, support bundles, and device compatibility. CI runs unit tests, compile checks, CLI help checks, and a headless GUI smoke test on macOS. Tagged release CI produces Apple Silicon and Intel bundles, CycloneDX SBOMs, checksums, and GitHub attestations. The app is ad-hoc signed, not Developer ID signed or notarized. @@ -123,7 +123,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack * Maintain the session-local typed-operation journal, explicit structured JSON manifests, and universal Action Palette that exposes only eligible operations. * Maintain the guided MVT backup-analysis handoff with explicit consent, no password persistence, output isolation, and no “clean device” conclusion. -* Add optional user-configured adapters for `go-ios`, `idb`, and `ipsw`, each with executable provenance and version display. +* Maintain optional user-configured adapters for `go-ios`, `idb`, and `ipsw`, each with executable provenance, version/build display, bounded read-only probes, and an explicit independent-target boundary. * Publish a small documentation site split into quick start, architecture, safety, troubleshooting, release verification, and contributor paths. ### P3 — ecosystem growth and scale @@ -179,6 +179,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Added a session-local operation journal and explicit per-operation JSON manifests across the primary typed workflows. | 113 tests and a 95-action GUI smoke passed; tests cover immutable bounded history, exact argument retention, output hashing without raw-output embedding, owner-only export, and overwrite refusal. | Capability Matrix, Location Lab, Live Logs, and Command Drift retain their stronger workflow-specific records rather than duplicating raw or high-volume events into this journal. | | 2026-09-22 | Added a keyboard-first Action Palette computed from current device and process eligibility. | 116 tests and a 96-action GUI smoke passed; smoke coverage verifies disconnected-state preset filtering, host-preset access, search behavior, stable control identity, and the `⌘ K` shortcut. | Guided presets are selected for review rather than executed, and eligibility is checked again at activation. | | 2026-09-22 | Added a guided external MVT handoff for consented decrypted-backup analysis. | 121 tests and a 110-action GUI smoke passed; tests cover executable provenance, secret-environment removal, backup structure/encryption checks, isolated output, explicit IOC arguments, offline defaults, version validation, and an end-to-end synthetic analysis process. | MVT stays separately installed; the toolkit accepts no password and never translates completion or absent findings into a clean-device verdict. | +| 2026-09-22 | Added separately installed go-ios, idb Companion, and ipsw adapters with provenance validation. | 126 tests and a 134-action GUI smoke passed; tests cover catalog identity, discovery, executable hashing, changed-binary rejection, upstream version/build formats, secret and target-routing removal, and a synthetic validate/probe lifecycle. | These tools keep their own discovery, pairing, tunnel, target, network, licensing, and support models; only bounded inventory probes are exposed. | ## Research sources diff --git a/docs/screenshots/ecosystem-tools.png b/docs/screenshots/ecosystem-tools.png new file mode 100644 index 0000000000000000000000000000000000000000..c66e0e595ff0d626805e2821f9079f2bfa5b50ae GIT binary patch literal 149380 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 Path: @@ -671,6 +692,19 @@ def __init__(self) -> None: self._mvt_request: MVTAnalysisRequest | None = None self._mvt_ioc_paths: tuple[Path, ...] = () self._mvt_temporary_config: tempfile.TemporaryDirectory[str] | None = None + self._external_tool_controller = FiniteProcessController(self) + self._external_tool_controller.completed.connect(self._external_tool_completed) + self._external_tool_installations: dict[ExternalToolIdentifier, ExternalToolInstallation] = {} + self._external_tool_pending_executable: ExternalToolExecutable | None = None + self._external_tool_active_identifier: ExternalToolIdentifier | None = None + self._external_tool_operation: Literal["validate", "probe"] | None = None + self._external_tool_fields: dict[ExternalToolIdentifier, QLineEdit] = {} + self._external_tool_statuses: dict[ExternalToolIdentifier, QLabel] = {} + self._external_tool_outputs: dict[ExternalToolIdentifier, QPlainTextEdit] = {} + self._external_tool_validate_buttons: dict[ExternalToolIdentifier, QPushButton] = {} + self._external_tool_probe_buttons: dict[ExternalToolIdentifier, QPushButton] = {} + self._external_tool_stop_buttons: dict[ExternalToolIdentifier, QPushButton] = {} + self._external_tool_path_buttons: dict[ExternalToolIdentifier, tuple[QPushButton, QPushButton]] = {} self._location_process: QProcess | None = None self._location_operation = "" self._location_arguments: tuple[str, ...] = () @@ -844,6 +878,7 @@ def _build_ui(self) -> None: ("Backup", self._build_backup_tab()), ("Sideload IPA", self._build_sideload_tab()), ("Evidence Capture", self._build_collection_tab()), + ("Ecosystem Tools", self._build_external_tools_page()), ("Man Pages", self._build_manpages_page()), ("Scope & Safety", self._build_safety_tab()), ) @@ -931,6 +966,15 @@ def _configure_accessibility(self) -> None: "Offline mouse coordinate picker. For keyboard-first location entry, use the coordinate importer, latitude, and longitude fields." ) self.location_map.setFocusPolicy(Qt.FocusPolicy.NoFocus) + for spec in external_tool_specs(): + self._external_tool_fields[spec.identifier].setAccessibleName(f"{spec.title} executable path") + self._external_tool_fields[spec.identifier].setAccessibleDescription( + f"Absolute path to the separately installed {spec.executable_name} executable." + ) + self._external_tool_outputs[spec.identifier].setAccessibleName(f"{spec.title} adapter output") + self._external_tool_outputs[spec.identifier].setAccessibleDescription( + "Session-local raw version validation or read-only probe output from the external tool." + ) QWidget.setTabOrder(self.device_combo, self.demo_mode_button) QWidget.setTabOrder(self.demo_mode_button, self.refresh_devices_button) QWidget.setTabOrder(self.refresh_devices_button, self.reconnect_device_button) @@ -959,6 +1003,7 @@ def _configure_keyboard_shortcuts(self) -> None: ("Meta+8", "Backup"), ("Meta+9", "Sideload IPA"), ("Meta+0", "Evidence Capture"), + ("Meta+Shift+E", "Ecosystem Tools"), ("Meta+Shift+M", "Man Pages"), ("Meta+Shift+S", "Scope & Safety"), ) @@ -996,6 +1041,7 @@ def navigate_to_page_and_focus(self, name: str) -> None: "Backup": self.backup_destination_field, "Sideload IPA": self.ipa_path_field, "Evidence Capture": self.case_title_field, + "Ecosystem Tools": self._external_tool_fields["go-ios"], "Man Pages": self.manpage_search_field, "Scope & Safety": self.navigation_list, } @@ -1062,6 +1108,7 @@ def show_keyboard_shortcuts(self) -> None: "

    Official MVT installation · + Official iOS backup-analysis guide · + MVT source repository

    " "" "" + "" "" "" "" @@ -1098,6 +1145,7 @@ def _eligible_action_palette_entries(self) -> tuple[ActionPaletteEntry, ...]: "Backup": "Prepare MobileBackup2, external UFADE acquisition, or consented MVT analysis.", "Sideload IPA": "Inspect a local IPA before an eligible installation attempt.", "Evidence Capture": "Prepare a scoped case and bounded evidence collection.", + "Ecosystem Tools": "Validate optional go-ios, idb, and ipsw adapters and run bounded read-only probes.", "Man Pages": "Browse version-matched command routes and live help.", "Scope & Safety": "Review authorization, privacy, and interpretation boundaries.", } @@ -1209,6 +1257,17 @@ def _eligible_action_palette_entries(self) -> tuple[ActionPaletteEntry, ...]: for identifier, title, summary, keywords, eligible in eligible_actions if eligible ) + entries.extend( + action_palette_entry( + f"action:external-tool:{spec.identifier}", + spec.probe_title, + "Eligible external read action", + f"Run the validated {spec.title} adapter probe after reviewing its independent target boundary.", + ("external", "adapter", spec.identifier, "inventory", "provenance"), + ) + for spec in external_tool_specs() + if self._external_tool_probe_buttons[spec.identifier].isEnabled() + ) if not self._console_controller.is_running(): device_available = self.selected_device() is not None entries.extend( @@ -1231,6 +1290,21 @@ def _execute_action_palette_entry(self, identifier: str) -> None: if identifier.startswith("preset:"): self._select_palette_preset(identifier.removeprefix("preset:")) return + if identifier.startswith("action:external-tool:"): + current_identifiers = {entry.identifier for entry in self._eligible_action_palette_entries()} + if identifier not in current_identifiers: + QMessageBox.information( + self, + "Action No Longer Eligible", + "The validated external tool or process state changed while the palette was open.", + ) + return + tool_identifier = identifier.removeprefix("action:external-tool:") + matching = tuple(spec.identifier for spec in external_tool_specs() if spec.identifier == tool_identifier) + if len(matching) != 1: + raise KeyError(f"Unknown external-tool action palette entry: {identifier}") + self.run_external_tool_probe(matching[0]) + return actions: Mapping[str, Callable[[], None]] = { "utility:session-activity": self.show_session_activity, "utility:keyboard-shortcuts": self.show_keyboard_shortcuts, @@ -1402,6 +1476,13 @@ def _support_bundle_context(self) -> SupportBundleContext: SupportStatus("developer_mode", self.developer_mode_status.text()), SupportStatus("capability_matrix", self.capability_status.text()), SupportStatus("command_drift", self.command_drift_status.text()), + *( + SupportStatus( + f"external_tool_{spec.identifier.replace('-', '_')}", + self._external_tool_statuses[spec.identifier].text(), + ) + for spec in external_tool_specs() + ), ) redactions = tuple( value @@ -2885,6 +2966,386 @@ def _build_command_center_page(self) -> QWidget: self._update_command_drift_controls() return page + def _build_external_tools_page(self) -> QWidget: + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(12) + + heading = QLabel("Ecosystem Tools") + heading.setObjectName("pageTitle") + heading.setFont(QFont(heading.font().family(), 20, QFont.Weight.Bold)) + layout.addWidget(heading) + explanation = QLabel( + "Connect optional third-party tools without bundling or silently trusting them. Each adapter records the " + "resolved executable, SHA-256, and version or build identity before enabling one bounded read-only probe." + ) + explanation.setWordWrap(True) + layout.addWidget(explanation) + boundary = QLabel( + "These tools use their own discovery, pairing, tunnel, simulator, device, network, and support models. " + "Their output is not merged into toolkit capability claims. Choosing an executable authorizes third-party " + "code to run locally only after the displayed path and hash are confirmed." + ) + boundary.setObjectName("externalToolsBoundary") + boundary.setWordWrap(True) + layout.addWidget(boundary) + + self.external_tool_tabs = QTabWidget() + self.external_tool_tabs.setObjectName("externalToolTabs") + for spec in external_tool_specs(): + self.external_tool_tabs.addTab(self._build_external_tool_tab(spec), spec.title) + layout.addWidget(self.external_tool_tabs, 1) + self._update_external_tool_controls() + return page + + def _build_external_tool_tab(self, spec: ExternalToolSpec) -> QWidget: + suffix = EXTERNAL_TOOL_OBJECT_SUFFIXES[spec.identifier] + tab = QWidget() + layout = QVBoxLayout(tab) + layout.setSpacing(10) + + scope = QLabel( + f"{spec.title} · {spec.license_name} · separately installed
    {spec.scope}" + ) + scope.setWordWrap(True) + layout.addWidget(scope) + + path_layout = QHBoxLayout() + path_field = QLineEdit() + path_field.setObjectName(f"external{suffix}ExecutablePath") + path_field.setPlaceholderText(f"Absolute path to {spec.executable_name}") + path_field.textChanged.connect(self._external_tool_text_handler(spec.identifier)) + path_layout.addWidget(path_field, 1) + choose_button = QPushButton("Choose…") + choose_button.setObjectName(f"external{suffix}ChooseButton") + choose_button.clicked.connect(self._external_tool_button_handler(spec.identifier, self.choose_external_tool)) + path_layout.addWidget(choose_button) + find_button = QPushButton("Find Installed") + find_button.setObjectName(f"external{suffix}FindButton") + find_button.clicked.connect(self._external_tool_button_handler(spec.identifier, self.find_external_tool)) + path_layout.addWidget(find_button) + layout.addLayout(path_layout) + + status = QLabel("Not validated. The toolkit has not executed this optional tool.") + status.setObjectName(f"external{suffix}Status") + status.setWordWrap(True) + layout.addWidget(status) + + actions = QHBoxLayout() + validate_button = QPushButton("Validate Version && SHA-256") + validate_button.setObjectName(f"external{suffix}ValidateButton") + validate_button.clicked.connect(self._external_tool_button_handler(spec.identifier, self.validate_external_tool)) + actions.addWidget(validate_button) + probe_button = QPushButton(spec.probe_title) + probe_button.setObjectName(f"external{suffix}ProbeButton") + probe_button.clicked.connect(self._external_tool_button_handler(spec.identifier, self.run_external_tool_probe)) + actions.addWidget(probe_button) + stop_button = QPushButton("Stop") + stop_button.setObjectName(f"external{suffix}StopButton") + stop_button.clicked.connect(self._external_tool_button_handler(spec.identifier, self.stop_external_tool)) + actions.addWidget(stop_button) + actions.addStretch() + layout.addLayout(actions) + + resources = QHBoxLayout() + setup_button = QPushButton("Copy Setup Command") + setup_button.setObjectName(f"external{suffix}SetupButton") + setup_button.clicked.connect(self._external_tool_button_handler(spec.identifier, self.copy_external_tool_setup)) + resources.addWidget(setup_button) + documentation_button = QPushButton("Official Documentation") + documentation_button.setObjectName(f"external{suffix}DocumentationButton") + documentation_button.clicked.connect( + self._external_tool_button_handler(spec.identifier, self.open_external_tool_documentation) + ) + resources.addWidget(documentation_button) + repository_button = QPushButton("Source Repository") + repository_button.setObjectName(f"external{suffix}RepositoryButton") + repository_button.clicked.connect( + self._external_tool_button_handler(spec.identifier, self.open_external_tool_repository) + ) + resources.addWidget(repository_button) + resources.addStretch() + layout.addLayout(resources) + + output = QPlainTextEdit() + output.setObjectName(f"external{suffix}Output") + output.setReadOnly(True) + output.setMaximumBlockCount(12000) + output.setPlaceholderText( + "Version validation and probe output appears here. It remains session-local unless you explicitly preserve it." + ) + layout.addWidget(output, 1) + + self._external_tool_fields[spec.identifier] = path_field + self._external_tool_statuses[spec.identifier] = status + self._external_tool_outputs[spec.identifier] = output + self._external_tool_validate_buttons[spec.identifier] = validate_button + self._external_tool_probe_buttons[spec.identifier] = probe_button + self._external_tool_stop_buttons[spec.identifier] = stop_button + self._external_tool_path_buttons[spec.identifier] = (choose_button, find_button) + return tab + + def _external_tool_button_handler( + self, + identifier: ExternalToolIdentifier, + action: Callable[[ExternalToolIdentifier], None], + ) -> Callable[[bool], None]: + def handle(checked: bool) -> None: + del checked + action(identifier) + + return handle + + def _external_tool_text_handler( + self, + identifier: ExternalToolIdentifier, + ) -> Callable[[str], None]: + def handle(value: str) -> None: + self._invalidate_external_tool(identifier, value) + + return handle + + def _invalidate_external_tool(self, identifier: ExternalToolIdentifier, value: str) -> None: + del value + self._external_tool_installations.pop(identifier, None) + self._external_tool_statuses[identifier].setText( + "Not validated. The toolkit has not executed this optional tool." + ) + self._update_external_tool_controls() + + def external_tool_path(self, identifier: ExternalToolIdentifier) -> Path: + value = self._external_tool_fields[identifier].text().strip() + if not value: + spec = external_tool_spec(identifier) + raise ExternalToolValidationError(f"Choose an absolute path to {spec.executable_name}") + return Path(value) + + def choose_external_tool(self, identifier: ExternalToolIdentifier) -> None: + spec = external_tool_spec(identifier) + selected, _ = QFileDialog.getOpenFileName( + self, + f"Choose separately installed {spec.executable_name}", + self._external_tool_fields[identifier].text() or str(Path.home()), + ) + if selected: + self._external_tool_fields[identifier].setText(selected) + + def find_external_tool(self, identifier: ExternalToolIdentifier) -> None: + spec = external_tool_spec(identifier) + candidates = discover_external_tool_executables(spec, Path.home(), os.environ.get("PATH", "")) + if not candidates: + QMessageBox.information( + self, + f"{spec.title} Not Found", + f"No executable named {spec.executable_name!r} was found in PATH, ~/.local/bin, " + "/opt/homebrew/bin, or /usr/local/bin. Use the official setup command or choose a reviewed path.", + ) + return + self._external_tool_fields[identifier].setText(str(candidates[0])) + self._external_tool_statuses[identifier].setText( + f"Found {len(candidates)} candidate(s). Validate the selected executable before probing." + ) + + def copy_external_tool_setup(self, identifier: ExternalToolIdentifier) -> None: + spec = external_tool_spec(identifier) + QApplication.clipboard().setText("\n".join(spec.setup_commands)) + self._external_tool_outputs[identifier].appendPlainText( + "Copied official setup command for manual review and execution in Terminal:\n" + + "\n".join(spec.setup_commands) + ) + + def open_external_tool_documentation(self, identifier: ExternalToolIdentifier) -> None: + spec = external_tool_spec(identifier) + if not QDesktopServices.openUrl(QUrl(spec.documentation_url)): + QMessageBox.critical( + self, + "Could Not Open Documentation", + f"macOS could not open the official {spec.title} documentation:\n{spec.documentation_url}", + ) + + def open_external_tool_repository(self, identifier: ExternalToolIdentifier) -> None: + spec = external_tool_spec(identifier) + if not QDesktopServices.openUrl(QUrl(spec.repository_url)): + QMessageBox.critical( + self, + "Could Not Open Repository", + f"macOS could not open the official {spec.title} repository:\n{spec.repository_url}", + ) + + def validate_external_tool(self, identifier: ExternalToolIdentifier) -> None: + if self._external_tool_controller.is_running(): + QMessageBox.warning(self, "External Tool Running", "Stop or wait for the active external tool first.") + return + spec = external_tool_spec(identifier) + try: + executable = inspect_external_tool_executable(spec, self.external_tool_path(identifier)) + except (ExternalToolValidationError, OSError) as error: + QMessageBox.critical(self, f"Invalid {spec.title} Executable", str(error)) + self._external_tool_statuses[identifier].setText(f"Validation rejected: {error}") + return + warning = ( + f"Run this separately installed {spec.title} executable to read its version or build identity?\n\n" + f"Path: {executable.path}\n" + f"SHA-256: {executable.sha256}\n" + f"Arguments: {shlex.join(spec.version_arguments)}\n\n" + "This executes third-party code on the Mac. The toolkit does not install, update, sandbox, endorse, or " + "redistribute it. Confirm only if you recognize and trust the exact path and hash." + ) + if not self._confirm(f"Validate {spec.title}", warning): + return + try: + self._start_external_tool_process(identifier, "validate", executable, spec.version_arguments) + except (ExternalToolValidationError, OSError) as error: + QMessageBox.critical(self, f"Could Not Start {spec.title}", str(error)) + self._external_tool_statuses[identifier].setText(f"Validation did not start: {error}") + + def run_external_tool_probe(self, identifier: ExternalToolIdentifier) -> None: + if self._external_tool_controller.is_running(): + QMessageBox.warning(self, "External Tool Running", "Stop or wait for the active external tool first.") + return + spec = external_tool_spec(identifier) + installation = self._external_tool_installations.get(identifier) + if installation is None: + QMessageBox.critical(self, f"{spec.title} Not Validated", "Validate the selected executable first.") + return + try: + validated = validate_external_tool_installation(spec, installation) + except (ExternalToolValidationError, OSError) as error: + self._external_tool_installations.pop(identifier, None) + self._external_tool_statuses[identifier].setText(f"Probe rejected: {error}") + self._update_external_tool_controls() + QMessageBox.critical(self, f"Could Not Run {spec.title}", str(error)) + return + warning = ( + f"Run the bounded read-only {spec.title} probe?\n\n" + f"Identity: {validated.version_or_build}\n" + f"Path: {validated.executable.path}\n" + f"SHA-256: {validated.executable.sha256}\n" + f"Arguments: {shlex.join(spec.probe_arguments)}\n\n" + f"{spec.scope}\n\n" + "The external tool chooses its own visible targets and does not use the toolkit's selected-device state. " + "Its output may contain device or simulator identifiers and remains session-local unless you preserve it." + ) + if not self._confirm(f"Run {spec.probe_title}", warning): + return + try: + self._start_external_tool_process(identifier, "probe", validated.executable, spec.probe_arguments) + except (ExternalToolValidationError, OSError) as error: + QMessageBox.critical(self, f"Could Not Start {spec.title}", str(error)) + self._external_tool_statuses[identifier].setText(f"Probe did not start: {error}") + + def _start_external_tool_process( + self, + identifier: ExternalToolIdentifier, + operation: Literal["validate", "probe"], + executable: ExternalToolExecutable, + arguments: tuple[str, ...], + ) -> None: + if self._external_tool_controller.is_running(): + raise RuntimeError("Cannot start an external tool while another adapter process is running") + spec = external_tool_spec(identifier) + current = inspect_external_tool_executable(spec, executable.path) + if current.sha256 != executable.sha256: + raise ExternalToolValidationError( + f"{spec.title} executable changed after review; inspect and validate it again" + ) + self._external_tool_active_identifier = identifier + self._external_tool_operation = operation + self._external_tool_pending_executable = executable if operation == "validate" else None + output = self._external_tool_outputs[identifier] + output.clear() + output.appendPlainText( + f"$ {executable.path} {shlex.join(arguments)}\n" + f"Executable SHA-256: {executable.sha256}\n" + "Inherited tool-routing and secret environment variables are removed for this adapter.\n" + ) + title = f"Validate {spec.title}" if operation == "validate" else spec.probe_title + self._begin_operation( + "external-tool", + self._host_operation_context(title, "Ecosystem Tools", f"external {spec.title} CLI", ()), + ) + self._external_tool_statuses[identifier].setText(f"{title} is running with a 30-second deadline…") + request = finite_process_request( + external_tool_command(spec, executable), + arguments, + external_tool_environment(base_environment(), spec), + EXTERNAL_TOOL_TIMEOUT_MS, + PROCESS_TERMINATE_GRACE_MS, + ) + self._external_tool_controller.start(request) + self._update_external_tool_controls() + + def _external_tool_completed(self, result_object: object) -> None: + if not isinstance(result_object, OperationResult): + raise TypeError(f"Unexpected external-tool result type: {type(result_object).__name__}") + identifier = self._external_tool_active_identifier + operation = self._external_tool_operation + if identifier is None or operation is None: + raise RuntimeError("External tool completed without active adapter state") + spec = external_tool_spec(identifier) + output = self._external_tool_outputs[identifier] + combined = (result_object.stdout + result_object.stderr).decode("utf-8", errors="replace") + if combined: + output.appendPlainText(combined.rstrip()) + if result_object.error_message: + output.appendPlainText(f"Process error: {result_object.error_message}") + output.appendPlainText( + f"Outcome: {result_object.outcome}; exit: " + f"{'unavailable' if result_object.exit_code is None else result_object.exit_code}" + ) + self._complete_operation("external-tool", result_object) + if result_object.outcome == "succeeded" and operation == "validate": + pending = self._external_tool_pending_executable + if pending is None: + raise RuntimeError("External tool validation completed without a pending executable") + try: + identity = parse_external_tool_version(spec, combined) + except ExternalToolValidationError as error: + self._external_tool_installations.pop(identifier, None) + self._external_tool_statuses[identifier].setText(f"Version validation failed: {error}") + else: + self._external_tool_installations[identifier] = ExternalToolInstallation(pending, identity) + self._external_tool_statuses[identifier].setText( + f"Validated {spec.title} {identity}; SHA-256 {pending.sha256}. Read-only probe is enabled." + ) + elif result_object.outcome == "succeeded" and operation == "probe": + self._external_tool_statuses[identifier].setText( + f"{spec.probe_title} completed. Review the raw third-party output; it is not a toolkit capability verdict." + ) + else: + self._external_tool_statuses[identifier].setText( + f"{spec.title} {operation} {result_object.outcome}; review the complete output." + ) + if operation == "validate": + self._external_tool_installations.pop(identifier, None) + self._external_tool_active_identifier = None + self._external_tool_operation = None + self._external_tool_pending_executable = None + self._update_external_tool_controls() + + def stop_external_tool(self, identifier: ExternalToolIdentifier) -> None: + if self._external_tool_active_identifier != identifier or not self._external_tool_controller.is_running(): + return + spec = external_tool_spec(identifier) + self._external_tool_statuses[identifier].setText(f"Stopping {spec.title}…") + self._external_tool_controller.cancel() + + def _update_external_tool_controls(self) -> None: + running = self._external_tool_controller.is_running() + active = self._external_tool_active_identifier + for spec in external_tool_specs(): + identifier = spec.identifier + has_path = bool(self._external_tool_fields[identifier].text().strip()) + self._external_tool_fields[identifier].setEnabled(not running) + choose_button, find_button = self._external_tool_path_buttons[identifier] + choose_button.setEnabled(not running) + find_button.setEnabled(not running) + self._external_tool_validate_buttons[identifier].setEnabled(not running and has_path) + self._external_tool_probe_buttons[identifier].setEnabled( + not running and identifier in self._external_tool_installations + ) + self._external_tool_stop_buttons[identifier].setEnabled(running and active == identifier) + def _build_manpages_page(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) @@ -6804,6 +7265,7 @@ def closeEvent(self, event: QCloseEvent) -> None: backup_running = self._backup_controller.is_running() collection_running = self._collection_controller.is_running() mvt_running = self._mvt_controller.is_running() + external_tool_running = self._external_tool_controller.is_running() critical_processes = tuple( process for process in (self._location_process,) @@ -6817,13 +7279,14 @@ def closeEvent(self, event: QCloseEvent) -> None: or backup_running or collection_running or mvt_running + or external_tool_running or self._console_controller.is_running() or critical_processes ) if active_operations and not self._close_after_collection: should_close = self._confirm( "Stop Active Operations?", - "A DDI, evidence, app, backup, MVT, Location Lab, or Command Center operation is still running. " + "A DDI, evidence, app, backup, MVT, ecosystem-tool, Location Lab, or Command Center operation is still running. " "Stop it, allow cleanup/finalization, and close the app?", ) if not should_close: @@ -6850,6 +7313,7 @@ def closeEvent(self, event: QCloseEvent) -> None: self._backup_controller.shutdown(10000, 3000) self._mvt_controller.shutdown(10000, 3000) self._clear_mvt_temporary_config() + self._external_tool_controller.shutdown(10000, 3000) self._collection_controller.shutdown(10000, 3000) capability_process = self._capability_process if capability_process is not None and capability_process.state() != QProcess.ProcessState.NotRunning: diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index 460de24..f1dfeb2 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -86,6 +86,7 @@ def run_smoke_test(arguments: Sequence[str]) -> int: from ios_developer_toolkit.action_palette import ActionPaletteDialog from ios_developer_toolkit.app import MainWindow from ios_developer_toolkit.backup_protocol import BackupRequest + from ios_developer_toolkit.external_tools import external_tool_spec, inspect_external_tool_executable from ios_developer_toolkit.mvt_connector import create_mvt_analysis_request from ios_developer_toolkit.operation_history import OperationHistoryDialog @@ -274,6 +275,56 @@ def run_smoke_test(arguments: Sequence[str]) -> int: raise RuntimeError(f"GUI MVT analysis did not create isolated output: {window.mvt_output.toPlainText()}") if "does not prove" not in window.mvt_status.text(): raise RuntimeError("GUI MVT completion omitted the no-clean-device interpretation boundary") + with tempfile.TemporaryDirectory() as external_tool_temporary_directory: + external_root = Path(external_tool_temporary_directory) + go_ios_path = external_root / "ios" + go_ios_path.write_text( + "#!/bin/sh\n" + "if [ \"$1\" = \"--version\" ]; then\n" + " printf '{\"version\":\"1.3.2-smoke\"}\\n'\n" + " exit 0\n" + "fi\n" + "printf '{\"deviceList\":[{\"name\":\"Synthetic iPhone\",\"udid\":\"REDACTED\"}]}\\n'\n", + encoding="utf-8", + ) + go_ios_path.chmod(0o700) + go_ios_spec = external_tool_spec("go-ios") + go_ios_executable = inspect_external_tool_executable(go_ios_spec, go_ios_path) + window._external_tool_fields["go-ios"].setText(str(go_ios_path)) + window._start_external_tool_process( + "go-ios", + "validate", + go_ios_executable, + go_ios_spec.version_arguments, + ) + external_validation_deadline = time.monotonic() + 10 + while window._external_tool_controller.is_running() and time.monotonic() < external_validation_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + installation = window._external_tool_installations.get("go-ios") + if window._external_tool_controller.is_running() or installation is None: + raise RuntimeError( + f"GUI go-ios validation did not complete: {window._external_tool_outputs['go-ios'].toPlainText()}" + ) + if installation.version_or_build != "1.3.2-smoke": + raise RuntimeError("GUI go-ios adapter retained an unexpected version") + window._start_external_tool_process( + "go-ios", + "probe", + installation.executable, + go_ios_spec.probe_arguments, + ) + external_probe_deadline = time.monotonic() + 10 + while window._external_tool_controller.is_running() and time.monotonic() < external_probe_deadline: + application.processEvents() + time.sleep(0.001) + application.processEvents() + external_output = window._external_tool_outputs["go-ios"].toPlainText() + if window._external_tool_controller.is_running() or "Synthetic iPhone" not in external_output: + raise RuntimeError(f"GUI go-ios probe did not preserve output: {external_output}") + if "not a toolkit capability verdict" not in window._external_tool_statuses["go-ios"].text(): + raise RuntimeError("GUI external-tool probe omitted the interpretation boundary") expected_shortcuts = { "shortcutRetryDeviceScan", "shortcutShowActionPalette", @@ -283,6 +334,7 @@ def run_smoke_test(arguments: Sequence[str]) -> int: "shortcutPreviousWorkspace", "shortcutNextWorkspace", "shortcutOpenCommandCenter", + "shortcutOpenEcosystemTools", "shortcutOpenManPages", "shortcutOpenScopeAndSafety", } diff --git a/ios_developer_toolkit/external_tools.py b/ios_developer_toolkit/external_tools.py new file mode 100644 index 0000000..ef1d17c --- /dev/null +++ b/ios_developer_toolkit/external_tools.py @@ -0,0 +1,247 @@ +from __future__ import annotations + +import json +import os +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Literal, Mapping + +from ios_developer_toolkit.file_integrity import sha256_file +from ios_developer_toolkit.runtime import ExecutableCommand + + +ExternalToolIdentifier = Literal["go-ios", "idb", "ipsw"] + + +class ExternalToolValidationError(ValueError): + """Raised when an optional external-tool adapter cannot be used safely.""" + + +@dataclass(frozen=True) +class ExternalToolSpec: + identifier: ExternalToolIdentifier + title: str + executable_name: str + repository_url: str + documentation_url: str + license_name: str + setup_commands: tuple[str, ...] + version_arguments: tuple[str, ...] + probe_arguments: tuple[str, ...] + probe_title: str + scope: str + environment_keys_to_remove: tuple[str, ...] + + +@dataclass(frozen=True) +class ExternalToolExecutable: + spec_identifier: ExternalToolIdentifier + path: Path + sha256: str + + +@dataclass(frozen=True) +class ExternalToolInstallation: + executable: ExternalToolExecutable + version_or_build: str + + +def external_tool_specs() -> tuple[ExternalToolSpec, ...]: + return ( + ExternalToolSpec( + "go-ios", + "go-ios", + "ios", + "https://github.com/danielpaulus/go-ios", + "https://github.com/danielpaulus/go-ios#readme", + "MIT", + ("npm install -g go-ios",), + ("--version",), + ("list", "--details"), + "List devices with go-ios", + "A separate cross-platform iOS protocol stack. The probe asks go-ios to enumerate devices using its own pairing and tunnel state.", + ("GO_IOS_DEVICEKIT_URL", "GO_IOS_WDA_URL", "P12_PASSWORD"), + ), + ExternalToolSpec( + "idb", + "Meta idb Companion", + "idb_companion", + "https://github.com/facebook/idb", + "https://fbidb.io/", + "MIT", + ("brew install facebook/fb/idb",), + ("--version",), + ("--list", "1"), + "List idb targets", + "The macOS companion for idb simulator and device automation. The probe lists targets visible to the companion without starting its server mode.", + ("IDB_COMPANION", "IDB_COMPANION_TLS", "IDB_UDID"), + ), + ExternalToolSpec( + "ipsw", + "blacktop ipsw", + "ipsw", + "https://github.com/blacktop/ipsw", + "https://blacktop.github.io/ipsw/", + "MIT", + ("brew install blacktop/tap/ipsw",), + ("version",), + ("idev", "list"), + "List devices with ipsw idev", + "A firmware and Apple-platform research suite. The probe uses its optional idev surface only to enumerate locally visible devices.", + ( + "GITHUB_TOKEN", + "GH_TOKEN", + "IPSW_APPSTORE_API_KEY", + "IPSW_APPSTORE_API_SECRET", + ), + ), + ) + + +def external_tool_spec(identifier: ExternalToolIdentifier) -> ExternalToolSpec: + matches = tuple(spec for spec in external_tool_specs() if spec.identifier == identifier) + if len(matches) != 1: + raise ExternalToolValidationError( + f"Expected one external tool specification for {identifier!r}, found {len(matches)}" + ) + return matches[0] + + +def discover_external_tool_executables( + spec: ExternalToolSpec, + home: Path, + path_environment: str, +) -> tuple[Path, ...]: + path_entries = tuple(Path(entry) for entry in path_environment.split(os.pathsep) if entry) + locations = ( + *(entry / spec.executable_name for entry in path_entries), + home.expanduser() / ".local" / "bin" / spec.executable_name, + Path("/opt/homebrew/bin") / spec.executable_name, + Path("/usr/local/bin") / spec.executable_name, + ) + candidates: list[Path] = [] + for location in locations: + expanded = location.expanduser() + if expanded.is_file() and os.access(expanded, os.X_OK): + resolved = expanded.resolve() + if resolved not in candidates: + candidates.append(resolved) + return tuple(candidates) + + +def inspect_external_tool_executable( + spec: ExternalToolSpec, + path: Path, +) -> ExternalToolExecutable: + expanded = path.expanduser() + if not expanded.is_absolute(): + raise ExternalToolValidationError( + f"{spec.title} executable path must be absolute: {expanded}" + ) + resolved = expanded.resolve() + if not resolved.is_file(): + raise ExternalToolValidationError( + f"{spec.title} executable does not exist: {resolved}" + ) + if not os.access(resolved, os.X_OK): + raise ExternalToolValidationError( + f"{spec.title} path is not executable: {resolved}" + ) + return ExternalToolExecutable(spec.identifier, resolved, sha256_file(resolved)) + + +def validate_external_tool_installation( + spec: ExternalToolSpec, + installation: ExternalToolInstallation, +) -> ExternalToolInstallation: + if installation.executable.spec_identifier != spec.identifier: + raise ExternalToolValidationError( + f"Validated executable belongs to {installation.executable.spec_identifier}, not {spec.identifier}" + ) + current = inspect_external_tool_executable(spec, installation.executable.path) + if current.sha256 != installation.executable.sha256: + raise ExternalToolValidationError( + f"{spec.title} executable changed after validation; validate it again before running a probe" + ) + return installation + + +def external_tool_command( + spec: ExternalToolSpec, + executable: ExternalToolExecutable, +) -> ExecutableCommand: + if executable.spec_identifier != spec.identifier: + raise ExternalToolValidationError( + f"Cannot run {executable.spec_identifier} executable as {spec.identifier}" + ) + environment_arguments = tuple( + argument + for key in spec.environment_keys_to_remove + for argument in ("-u", key) + ) + return ExecutableCommand( + Path("/usr/bin/env"), + (*environment_arguments, str(executable.path)), + ) + + +def external_tool_environment(base: Mapping[str, str], spec: ExternalToolSpec) -> Mapping[str, str]: + environment = { + key: value + for key, value in base.items() + if key not in spec.environment_keys_to_remove + } + environment["NO_COLOR"] = "1" + environment["PYTHONUNBUFFERED"] = "1" + return environment + + +def parse_external_tool_version(spec: ExternalToolSpec, output: str) -> str: + without_ansi = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", output).strip() + if spec.identifier == "go-ios": + return _parse_go_ios_version(without_ansi) + if spec.identifier == "idb": + return _parse_idb_build(without_ansi) + if spec.identifier == "ipsw": + return _parse_ipsw_version(without_ansi) + raise ExternalToolValidationError(f"Unsupported external tool identifier: {spec.identifier}") + + +def _parse_go_ios_version(output: str) -> str: + for payload in _json_object_lines(output): + version = payload.get("version") + if isinstance(version, str) and version.strip(): + return version.strip() + match = re.search(r"(?im)^\s*(?:go-ios\s+)?([A-Za-z0-9][A-Za-z0-9._+-]*)\s*$", output) + if match is None: + raise ExternalToolValidationError("go-ios version output was not recognized") + return match.group(1) + + +def _parse_idb_build(output: str) -> str: + for payload in _json_object_lines(output): + build_date = payload.get("build_date") + build_time = payload.get("build_time") + if isinstance(build_date, str) and build_date.strip() and isinstance(build_time, str) and build_time.strip(): + return f"build {build_date.strip()} {build_time.strip()}" + raise ExternalToolValidationError("idb companion build output did not contain build_date and build_time") + + +def _parse_ipsw_version(output: str) -> str: + match = re.search(r"(?im)^\s*Version:\s*([^,\s]+)", output) + if match is None: + raise ExternalToolValidationError("ipsw version output did not contain a recognizable Version line") + return match.group(1) + + +def _json_object_lines(output: str) -> tuple[dict[str, object], ...]: + objects: list[dict[str, object]] = [] + for line in output.splitlines(): + try: + payload = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(payload, dict) and all(isinstance(key, str) for key in payload): + objects.append(payload) + return tuple(objects) diff --git a/ios_developer_toolkit/file_integrity.py b/ios_developer_toolkit/file_integrity.py new file mode 100644 index 0000000..e2acddb --- /dev/null +++ b/ios_developer_toolkit/file_integrity.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as input_file: + for block in iter(lambda: input_file.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() diff --git a/ios_developer_toolkit/gui_pages.py b/ios_developer_toolkit/gui_pages.py index df8b3b0..7bce10d 100644 --- a/ios_developer_toolkit/gui_pages.py +++ b/ios_developer_toolkit/gui_pages.py @@ -250,7 +250,7 @@ def toolkit_stylesheet() -> str: #protocolStackSummary { font-family: Menlo; color: #34435a; } #connectionBanner { background: #e9f2ff; border: 1px solid #afcff8; border-radius: 8px; padding: 10px; } #collectionPrivacyWarning { background: #fff5df; border: 1px solid #e7c36a; border-radius: 8px; padding: 10px; } - #installedAppsPrivacyWarning, #backupEncryptionWarning, #locationPrivacyWarning, #capabilityMatrixBoundary { background: #fff5df; border: 1px solid #e7c36a; border-radius: 8px; padding: 10px; } + #installedAppsPrivacyWarning, #backupEncryptionWarning, #locationPrivacyWarning, #capabilityMatrixBoundary, #externalToolsBoundary { background: #fff5df; border: 1px solid #e7c36a; border-radius: 8px; padding: 10px; } #capabilityMatrixStatus { background: #e9f2ff; border: 1px solid #afcff8; border-radius: 8px; padding: 9px; } #appSubtitle { color: #596273; } """ diff --git a/ios_developer_toolkit/mvt_connector.py b/ios_developer_toolkit/mvt_connector.py index d2a484f..dab8e3a 100644 --- a/ios_developer_toolkit/mvt_connector.py +++ b/ios_developer_toolkit/mvt_connector.py @@ -1,6 +1,5 @@ from __future__ import annotations -import hashlib import os import plistlib import re @@ -9,6 +8,7 @@ from typing import Mapping from ios_developer_toolkit.runtime import ExecutableCommand +from ios_developer_toolkit.file_integrity import sha256_file MVT_REPOSITORY_URL = "https://github.com/mvt-project/mvt" @@ -83,14 +83,6 @@ def discover_mvt_executables(home: Path, path_environment: str) -> tuple[Path, . return tuple(candidates) -def _sha256_file(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as input_file: - for block in iter(lambda: input_file.read(1024 * 1024), b""): - digest.update(block) - return digest.hexdigest() - - def inspect_mvt_executable(path: Path) -> MVTExecutable: expanded = path.expanduser() if not expanded.is_absolute(): @@ -100,7 +92,7 @@ def inspect_mvt_executable(path: Path) -> MVTExecutable: raise MVTValidationError(f"MVT executable does not exist: {resolved}") if not os.access(resolved, os.X_OK): raise MVTValidationError(f"MVT executable is not executable: {resolved}") - return MVTExecutable(resolved, _sha256_file(resolved)) + return MVTExecutable(resolved, sha256_file(resolved)) def mvt_command(executable: MVTExecutable) -> ExecutableCommand: diff --git a/tests/test_external_tools.py b/tests/test_external_tools.py new file mode 100644 index 0000000..1b238f2 --- /dev/null +++ b/tests/test_external_tools.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import os +import stat +import tempfile +import unittest +from pathlib import Path + +from ios_developer_toolkit.external_tools import ( + ExternalToolInstallation, + ExternalToolValidationError, + discover_external_tool_executables, + external_tool_command, + external_tool_environment, + external_tool_spec, + external_tool_specs, + inspect_external_tool_executable, + parse_external_tool_version, + validate_external_tool_installation, +) + + +class ExternalToolTests(unittest.TestCase): + def test_catalog_has_unique_current_adapters(self) -> None: + specs = external_tool_specs() + self.assertEqual(tuple(spec.identifier for spec in specs), ("go-ios", "idb", "ipsw")) + self.assertEqual(len({spec.executable_name for spec in specs}), len(specs)) + self.assertTrue(all(spec.license_name == "MIT" for spec in specs)) + self.assertTrue(all(spec.version_arguments and spec.probe_arguments for spec in specs)) + + def test_discovers_expected_executable_and_records_provenance(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + spec = external_tool_spec("go-ios") + executable_path = root / spec.executable_name + executable_path.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + executable_path.chmod(executable_path.stat().st_mode | stat.S_IXUSR) + + candidates = discover_external_tool_executables(spec, root, str(root)) + executable = inspect_external_tool_executable(spec, executable_path) + + self.assertEqual(candidates, (executable_path.resolve(),)) + self.assertEqual(executable.spec_identifier, "go-ios") + self.assertEqual(len(executable.sha256), 64) + + def test_rejects_relative_path_and_changed_executable(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + spec = external_tool_spec("ipsw") + with self.assertRaises(ExternalToolValidationError): + inspect_external_tool_executable(spec, Path("ipsw")) + + executable_path = root / spec.executable_name + executable_path.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + executable_path.chmod(0o700) + executable = inspect_external_tool_executable(spec, executable_path) + installation = ExternalToolInstallation(executable, "3.1.723") + executable_path.write_text("#!/bin/sh\nexit 1\n", encoding="utf-8") + with self.assertRaises(ExternalToolValidationError): + validate_external_tool_installation(spec, installation) + + def test_parses_each_upstream_version_or_build_shape(self) -> None: + self.assertEqual( + parse_external_tool_version( + external_tool_spec("go-ios"), + 'diagnostic line\n{"version":"1.3.2"}\n', + ), + "1.3.2", + ) + self.assertEqual( + parse_external_tool_version( + external_tool_spec("idb"), + '{"build_date":"2026-09-22","build_time":"11:12:36"}', + ), + "build 2026-09-22 11:12:36", + ) + self.assertEqual( + parse_external_tool_version( + external_tool_spec("ipsw"), + "Version: 3.1.723, BuildCommit: abc123\n", + ), + "3.1.723", + ) + with self.assertRaises(ExternalToolValidationError): + parse_external_tool_version(external_tool_spec("idb"), "idb") + + def test_command_removes_tool_routing_and_secret_environment(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + spec = external_tool_spec("idb") + executable_path = root / spec.executable_name + executable_path.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + executable_path.chmod(0o700) + executable = inspect_external_tool_executable(spec, executable_path) + command = external_tool_command(spec, executable) + environment = external_tool_environment( + { + "PATH": os.environ.get("PATH", ""), + "IDB_COMPANION": "remote.example:1234", + "IDB_UDID": "sensitive-target", + }, + spec, + ) + + self.assertEqual(command.program, Path("/usr/bin/env")) + self.assertIn("IDB_COMPANION", command.prefix_arguments) + self.assertIn("IDB_UDID", command.prefix_arguments) + self.assertEqual(command.prefix_arguments[-1], str(executable.path)) + self.assertNotIn("IDB_COMPANION", environment) + self.assertNotIn("IDB_UDID", environment) + + +if __name__ == "__main__": + unittest.main() From b886dd2ea6f5104af3a70f0cb6685a5947a09411 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 05:08:19 -0700 Subject: [PATCH 13/16] Publish focused documentation site --- .github/workflows/docs.yml | 75 ++++++++++++++++++++++++++++++++ .gitignore | 3 ++ CONTRIBUTING.md | 7 +++ README.md | 12 +++++ THIRD_PARTY_NOTICES.md | 2 + docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- docs/architecture.md | 33 ++++++++++++++ docs/contributing.md | 16 +++++++ docs/index.md | 70 +++++++++++++++++++++++++++++ docs/quick-start.md | 25 +++++++++++ docs/release-verification.md | 19 ++++++++ docs/safety.md | 26 +++++++++++ docs/stylesheets/extra.css | 36 +++++++++++++++ docs/troubleshooting.md | 24 ++++++++++ mkdocs.yml | 61 ++++++++++++++++++++++++++ requirements/docs.txt | 1 + 16 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/architecture.md create mode 100644 docs/contributing.md create mode 100644 docs/index.md create mode 100644 docs/quick-start.md create mode 100644 docs/release-verification.md create mode 100644 docs/safety.md create mode 100644 docs/stylesheets/extra.css create mode 100644 docs/troubleshooting.md create mode 100644 mkdocs.yml create mode 100644 requirements/docs.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..1896069 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,75 @@ +name: Documentation + +on: + push: + branches: + - main + paths: + - ".github/workflows/docs.yml" + - "docs/**" + - "mkdocs.yml" + - "requirements/docs.txt" + - "README.md" + - "SECURITY.md" + - "CONTRIBUTING.md" + - "SOURCE_AVAILABILITY.md" + - "THIRD_PARTY_NOTICES.md" + pull_request: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + - "mkdocs.yml" + - "requirements/docs.txt" + - "README.md" + - "SECURITY.md" + - "CONTRIBUTING.md" + - "SOURCE_AVAILABILITY.md" + - "THIRD_PARTY_NOTICES.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: documentation-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build documentation site + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.13" + cache: pip + cache-dependency-path: requirements/docs.txt + - name: Install documentation dependencies + run: python -m pip install --disable-pip-version-check --requirement requirements/docs.txt + - name: Build strict documentation site + run: python -m mkdocs build --strict --clean + - uses: actions/configure-pages@v6 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + - uses: actions/upload-pages-artifact@v5 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + path: site + + deploy: + name: Publish documentation site + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 30c2892..b3a7ed4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ venv/ build/ dist/ +site/ +output/ +.playwright-cli/ ios_developer_toolkit/deployment/ packaging/deployment/ *.egg-info/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 033d5ff..bec2d05 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,13 @@ venv/bin/python -m ios_developer_toolkit.ipa_inspector --help QT_QPA_PLATFORM=offscreen venv/bin/python -m ios_developer_toolkit --toolkit-internal-smoke-test ``` +Documentation changes must also pass the strict site build: + +```bash +venv/bin/python -m pip install --requirement requirements/docs.txt +venv/bin/python -m mkdocs build --strict --clean +``` + Prefer a real, authorized integration check when the change touches device discovery, pairing, developer services, DDI handling, tunnels, backup, installation, location simulation, logging, or packet capture. State exactly which host, device family, OS version, connection path, and cleanup action were tested, without publishing a unique identifier. Changes to packaging must additionally build the native app, run its embedded CLI and GUI smoke checks, verify the expected Mach-O architecture, and pass `codesign --verify --deep --strict`. The app must contain a matching `Contents/Resources/BOM.cdx.json`, `SOURCE_AVAILABILITY.md`, and the generated `Contents/Resources/Licenses/` inventory; `scripts/verify_release_metadata.py` enforces those links. Release assets must remain separate for Apple Silicon and Intel until a verified universal build exists, and the release workflow must retain checksums plus build-provenance and SBOM attestations. diff --git a/README.md b/README.md index d4e044f..6687c26 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ ### iOS Developer Toolkit: a guided pymobiledevice3 GUI, Developer Disk Image mounter, and evidence workbench for macOS [![CI](https://github.com/hideouts-io/iOS-Developer-Toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/hideouts-io/iOS-Developer-Toolkit/actions/workflows/ci.yml) +[![Documentation](https://github.com/hideouts-io/iOS-Developer-Toolkit/actions/workflows/docs.yml/badge.svg)](https://hideouts-io.github.io/iOS-Developer-Toolkit/) [![Latest release](https://img.shields.io/github/v/release/hideouts-io/iOS-Developer-Toolkit?display_name=tag)](https://github.com/hideouts-io/iOS-Developer-Toolkit/releases/latest) ![Platform](https://img.shields.io/badge/platform-macOS-000000?logo=apple&logoColor=white) ![Devices](https://img.shields.io/badge/device-iPhone%20%7C%20iPad-0969da) @@ -23,6 +24,8 @@ The current interface organizes Apple-device work into 13 focused workspaces. It The screenshots use an illustrative device name, model, version, build, and UDID. They contain no real device capture, account identifier, backup, credential, or case evidence. +Use the focused [documentation site](https://hideouts-io.github.io/iOS-Developer-Toolkit/) for quick start, architecture, safety, troubleshooting, release verification, contribution, and physical-device testing paths. This README remains the canonical complete feature and workspace reference. + ## Contents - [Start here](#start-here) @@ -1154,6 +1157,15 @@ venv/bin/python -m ios_developer_toolkit.ipa_inspector --help The final launcher check opens the application and briefly verifies the process. It stops an existing toolkit process first, so do not run it during an active capture or backup. +Build the focused documentation site with its isolated pinned dependency: + +```bash +venv/bin/python -m pip install --requirement requirements/docs.txt +venv/bin/python -m mkdocs build --strict --clean +``` + +Pull requests validate the site without publishing it. A documentation change merged to `main` publishes through the dedicated GitHub Pages workflow. + Physical-device validation is opt-in and is not required for pull requests. Use the [physical-device test protocol](docs/PHYSICAL_DEVICE_TEST_PROTOCOL.md) to separate USB, usbmux, CoreDevice, Developer Mode, DDI, tunnel, DVT, and state-changing checks; publish only sanitized results. ### Release model diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 6f2d49b..9b2903d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -22,6 +22,8 @@ The generated package inventory is intentionally more detailed than this summary UFADE, MVT, go-ios, idb, and ipsw are optional, separately installed external providers. The toolkit does not bundle those projects. MVT remains subject to the [MVT License](https://license.mvt.re/1.1/) and its consent and interpretation boundaries. [go-ios](https://github.com/danielpaulus/go-ios), [idb](https://github.com/facebook/idb), and [ipsw](https://github.com/blacktop/ipsw) each declare the MIT License in their upstream repositories. Their adapter only validates and launches a user-selected executable; their source and binary remain outside this project and its release SBOM. Other projects named in the README as design references are not copied, imported, or linked unless the README explicitly says otherwise. +The documentation workflow uses pinned [Material for MkDocs](https://github.com/squidfunk/mkdocs-material) 9.7.7 under its MIT license. It is a site-build dependency only and is not bundled in the macOS application or application SBOM. + See [SOURCE_AVAILABILITY.md](SOURCE_AVAILABILITY.md) for the project source location, matching tagged source, and upstream source locations for bundled third-party components. These notices document the shipped dependency boundary; they are not legal advice. Anyone redistributing a modified or repackaged application remains responsible for satisfying every applicable component license, including source-availability and relinking obligations where they apply. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 353f588..91fbe08 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -124,7 +124,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack * Maintain the session-local typed-operation journal, explicit structured JSON manifests, and universal Action Palette that exposes only eligible operations. * Maintain the guided MVT backup-analysis handoff with explicit consent, no password persistence, output isolation, and no “clean device” conclusion. * Maintain optional user-configured adapters for `go-ios`, `idb`, and `ipsw`, each with executable provenance, version/build display, bounded read-only probes, and an explicit independent-target boundary. -* Publish a small documentation site split into quick start, architecture, safety, troubleshooting, release verification, and contributor paths. +* Maintain the focused documentation site split into quick start, architecture, safety, troubleshooting, release verification, contributor, physical-device testing, and product-audit paths. ### P3 — ecosystem growth and scale @@ -180,6 +180,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Added a keyboard-first Action Palette computed from current device and process eligibility. | 116 tests and a 96-action GUI smoke passed; smoke coverage verifies disconnected-state preset filtering, host-preset access, search behavior, stable control identity, and the `⌘ K` shortcut. | Guided presets are selected for review rather than executed, and eligibility is checked again at activation. | | 2026-09-22 | Added a guided external MVT handoff for consented decrypted-backup analysis. | 121 tests and a 110-action GUI smoke passed; tests cover executable provenance, secret-environment removal, backup structure/encryption checks, isolated output, explicit IOC arguments, offline defaults, version validation, and an end-to-end synthetic analysis process. | MVT stays separately installed; the toolkit accepts no password and never translates completion or absent findings into a clean-device verdict. | | 2026-09-22 | Added separately installed go-ios, idb Companion, and ipsw adapters with provenance validation. | 126 tests and a 134-action GUI smoke passed; tests cover catalog identity, discovery, executable hashing, changed-binary rejection, upstream version/build formats, secret and target-routing removal, and a synthetic validate/probe lifecycle. | These tools keep their own discovery, pairing, tunnel, target, network, licensing, and support models; only bounded inventory probes are exposed. | +| 2026-09-22 | Added a focused Material for MkDocs documentation site and pull-request/push workflow. | `mkdocs build --strict --clean` passes locally; the site routes beginners, developers, investigators, release verifiers, and contributors to canonical repository material without copying the complete README. | GitHub Pages publication occurs only after a documentation change reaches `main`; the site-build dependency is not part of the application bundle. | ## Research sources diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..db03e3e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,33 @@ +# Architecture + +## Service layers + +The toolkit keeps Apple service boundaries visible instead of reducing every failure to “device not connected.” + +| Layer | Typical role | What readiness does not prove | +|---|---|---| +| USB / Wi-Fi and usbmux | Host discovery and transport | Trust, Developer Mode, or service access | +| Lockdown and paired services | Device information, apps, backup, diagnostics, AFC, classic syslog | Root access or unrestricted files | +| RemoteXPC / RSD | Modern service discovery and transport | That every advertised service accepts a request | +| Developer Mode and DDI | Enables compatible developer-service payloads | Jailbreak, bypass, or compromise | +| CoreDevice / DVT | Apple development and Instruments-style telemetry | Complete or stable forensic coverage | + +The [README service-layer diagram](https://github.com/hideouts-io/iOS-Developer-Toolkit#how-the-service-layers-fit-together) is the canonical operational explanation. + +## Process model + +The GUI launches argument vectors directly rather than evaluating shell pipelines or substitutions. Finite operations use a shared controller with explicit timeout, terminal output draining, cancellation, and one typed result. Long-running streams use explicit Stop controls. Backup and evidence collection retain purpose-built protocols because password input and partial-artifact finalization have different safety requirements. + +The key implementation surfaces are: + +- [`runtime.py`](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/ios_developer_toolkit/runtime.py) for packaged/source command resolution; +- [`qt_process.py`](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/ios_developer_toolkit/qt_process.py) and [`interactive_process.py`](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/ios_developer_toolkit/interactive_process.py) for typed process lifecycles; +- [`command_catalog.py`](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/ios_developer_toolkit/command_catalog.py) for reviewed guided commands; +- [`operation_history.py`](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/ios_developer_toolkit/operation_history.py) for session-local operation records; +- [`collector.py`](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/ios_developer_toolkit/collector.py) for evidence coverage, manifests, and hashes. + +## External-provider boundary + +UFADE and MVT remain isolated external providers. go-ios, idb Companion, and ipsw are optional executable adapters with path, SHA-256, and version/build validation. Their dependencies, licenses, target selection, output semantics, and update cycles are not merged into the packaged application. + +See the current [product audit and roadmap](PRODUCT_AUDIT_2026-09-21.md) for the evidence behind these boundaries. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..4fa8329 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,16 @@ +# Contributing + +## Pick the right path + +- Use [Discussions](https://github.com/hideouts-io/iOS-Developer-Toolkit/discussions) for setup, compatibility, and workflow questions. +- Use a [bug report](https://github.com/hideouts-io/iOS-Developer-Toolkit/issues/new?template=bug_report.yml) for a reproducible defect. +- Use a [feature request](https://github.com/hideouts-io/iOS-Developer-Toolkit/issues/new?template=feature_request.yml) for a bounded workflow proposal. +- Use [private vulnerability reporting](https://github.com/hideouts-io/iOS-Developer-Toolkit/security/advisories/new) for security issues. + +The repository's [CONTRIBUTING.md](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/CONTRIBUTING.md) is the canonical setup, style, validation, privacy, and pull-request guide. The [Code of Conduct](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/CODE_OF_CONDUCT.md) applies to all project spaces. + +## Evidence expected in a pull request + +Describe the problem, affected layer, behavior change, tests run, device coverage, privacy impact, and known limitations. Use synthetic or sanitized screenshots and logs. Changes to process handling should exercise real short-lived child processes where practical; packaging changes should validate the frozen application and both release architectures. + +The [product audit](PRODUCT_AUDIT_2026-09-21.md) records current architecture risks, ecosystem boundaries, and prioritized work. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..8dcf942 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,70 @@ +# iOS Developer Toolkit + +
    + +## One guided macOS workbench for Apple-device services + +iOS Developer Toolkit makes authorized iPhone and iPad development, diagnostics, backup, logging, package inspection, and evidence-preservation workflows visible without hiding their prerequisites or interpretation limits. + +[Start with a device](quick-start.md){ .md-button .md-button--primary } +[Review the safety boundary](safety.md){ .md-button } + +
    + +![The Home workspace](screenshots/home.png) + +## Choose your path + +
    + +
    + +### First-time operator + +Connect one unlocked device, establish trust, and use the one-click Capability Matrix before choosing a workflow. + +[Open the quick start](quick-start.md) + +
    + +
    + +### iOS developer + +Understand the Lockdown, RemoteXPC, DDI, CoreDevice, and DVT layers, then use guided presets or native Xcode handoffs. + +[Read the architecture guide](architecture.md) + +
    + +
    + +### Investigator + +Preserve raw logs and bounded collection results, review coverage gaps, and separate tool output from analyst conclusions. + +[Review safety and privacy](safety.md) + +
    + +
    + +### Contributor or verifier + +Use the public tests, dual-architecture packaging checks, SBOMs, checksums, and GitHub attestations. + +[Verify a release](release-verification.md) + +
    + +
    + +## Current product map + +The application contains 13 workspaces spanning connection and DDI readiness, location testing, live logs, guided commands, app inventory, backup providers, IPA inspection, evidence capture, optional ecosystem tools, installed-command help, and visible scope boundaries. + +The [canonical README](https://github.com/hideouts-io/iOS-Developer-Toolkit#readme) remains the complete feature reference and screenshot walkthrough. This site separates the most common audience paths so setup, architecture, safety, troubleshooting, release verification, and contribution material are easier to find without maintaining a second copy of every command. + +!!! note "Independent community project" + + This project is not affiliated with or endorsed by Apple, the pymobiledevice3 maintainers, or the maintainers of optional external tools. diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..2c240bb --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,25 @@ +# Quick start + +## Before connecting a device + +Use macOS 13 or later, choose the release matching the Mac architecture, and work only with a device you own or are explicitly authorized to use. The [README installation section](https://github.com/hideouts-io/iOS-Developer-Toolkit#installation) is the canonical source for clone, source-launch, release-download, Gatekeeper, and removal instructions. + +!!! warning "Keep one intended device connected" + + External providers such as UFADE, MVT, go-ios, idb, and ipsw use their own target-selection rules. A device selected in the toolkit does not constrain an external program. + +## First session + +1. Connect the unlocked device directly with a data-capable cable. +2. Approve the macOS accessory prompt and the iOS **Trust** prompt if shown. +3. Select the intended physical device in the top-right picker. +4. Open **Device & DDI** and check Developer Mode only if the intended workflow needs developer services. +5. Run **Capability Matrix** before mounting, tunneling, streaming, or changing state. +6. Choose a workspace and review its prerequisite, target, exact argument vector, and safety classification. +7. Stop streams, clear simulated location, finalize evidence, and unmount temporary developer support when finished. + +The complete [first-device walkthrough](https://github.com/hideouts-io/iOS-Developer-Toolkit#first-device-walkthrough) explains each state and the expected failure indicators. For a controlled real-device validation, use the [physical-device test protocol](PHYSICAL_DEVICE_TEST_PROTOCOL.md). + +## Learn without a physical device + +Use **Demo Mode** for a visibly simulated interface walkthrough. It never exposes a fake device to operational code, and device actions remain disabled. The Action Palette (`⌘ K`) and keyboard reference (`⌘ /`) remain available for navigation. diff --git a/docs/release-verification.md b/docs/release-verification.md new file mode 100644 index 0000000..9b3ab60 --- /dev/null +++ b/docs/release-verification.md @@ -0,0 +1,19 @@ +# Release verification + +## What a published release contains + +Apple Silicon and Intel applications are built separately on native GitHub-hosted runners. Each ZIP is accompanied by a CycloneDX SBOM, a release-wide SHA-256 inventory, and GitHub build-provenance and SBOM attestations. The application is ad-hoc signed and is not Apple-notarized. + +## Verification order + +1. Download the archive matching the Mac architecture from the [latest release](https://github.com/hideouts-io/iOS-Developer-Toolkit/releases/latest). +2. Verify the archive against `SHA256SUMS.txt` before extracting it. +3. Verify GitHub build provenance for that exact archive. +4. Inspect the architecture label and embedded SBOM. +5. After extraction, inspect the ad-hoc signature and apply the documented Gatekeeper procedure only if the provenance is acceptable. + +The canonical commands and current signing caveats live in the [README release section](https://github.com/hideouts-io/iOS-Developer-Toolkit#release-model) and [security policy](https://github.com/hideouts-io/iOS-Developer-Toolkit/security/policy). Source and bundled-component boundaries are recorded in [SOURCE_AVAILABILITY.md](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/SOURCE_AVAILABILITY.md) and [THIRD_PARTY_NOTICES.md](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/THIRD_PARTY_NOTICES.md). + +!!! danger "Do not infer notarization" + + A valid checksum, ad-hoc signature, SBOM, or GitHub attestation does not make the bundle Apple-notarized. Each mechanism answers a different provenance or integrity question. diff --git a/docs/safety.md b/docs/safety.md new file mode 100644 index 0000000..691136c --- /dev/null +++ b/docs/safety.md @@ -0,0 +1,26 @@ +# Safety and privacy + +## Authorization comes first + +Use the toolkit only on devices and data you own or are explicitly authorized to develop against, administer, test, back up, or examine. A DDI, trust relationship, profile, entitlement, or available service does not establish authorization. + +The canonical [Scope and Safety workspace guide](https://github.com/hideouts-io/iOS-Developer-Toolkit#scope-and-safety) lists the product's technical limits. The [security policy](https://github.com/hideouts-io/iOS-Developer-Toolkit/security/policy) explains private vulnerability reporting and the data that must never be placed in a public issue. + +## Action classes + +| Class | Examples | Review boundary | +|---|---|---| +| Read-oriented | Discovery, status, inventory, help | Exact target and command remain visible | +| Host write | Export, capture, backup, analysis output | Destination and sensitive-output warning | +| Device change | Install, uninstall, mount, launch, location | Device-bound typed acknowledgement | +| High impact | Restore, erase, activation, restart, shutdown | Backup acknowledgement plus irreversible phrase | + +The Action Palette exposes only operations currently eligible in the visible state and rechecks eligibility at activation. + +## Evidence and interpretation + +Raw logs, packet captures, backups, app inventories, screenshots, profiles, crash reports, MVT results, and external-tool inventories can contain sensitive device, account, application, location, or network data. Store them outside a public checkout on access-controlled storage. + +Hashes detect later changes; they do not prove acquisition time, custody, authorship, completeness, or truth. Empty output is not proof of absence. A successful command is not proof that its view is complete. Analyst annotations remain separate from raw capture facts. + +For collection structure and retention guidance, use the [canonical evidence-case reference](https://github.com/hideouts-io/iOS-Developer-Toolkit#evidence-case-contents). diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..b4886be --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,36 @@ +:root { + --md-primary-fg-color: #111827; + --md-accent-fg-color: #c1121f; +} + +.md-header { + border-bottom: 3px solid #c1121f; +} + +.toolkit-hero { + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 0.8rem; + padding: 1.25rem; + background: linear-gradient(135deg, rgba(193, 18, 31, 0.1), rgba(17, 24, 39, 0.04)); +} + +.toolkit-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); + gap: 0.8rem; + margin: 1rem 0; +} + +.toolkit-card { + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 0.65rem; + padding: 0.9rem 1rem; +} + +.toolkit-card > :first-child { + margin-top: 0; +} + +.toolkit-card > :last-child { + margin-bottom: 0; +} diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..0d78cab --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,24 @@ +# Troubleshooting + +## Start with the failing layer + +| Visible symptom | First check | Next reference | +|---|---|---| +| No phone in the picker | Cable, unlock state, macOS accessory approval, Finder visibility, Trust | [Device not detected](https://github.com/hideouts-io/iOS-Developer-Toolkit#device-not-detected) | +| Paired but developer command fails | Developer Mode, DDI compatibility, tunnel, service-specific matrix row | [Capability Matrix](https://github.com/hideouts-io/iOS-Developer-Toolkit#device-capability-matrix) | +| Man Pages appears busy | Cancel the bounded request and retry from the current project environment | [Man Pages](https://github.com/hideouts-io/iOS-Developer-Toolkit#man-pages) | +| Stream has no lines | Confirm the correct stream family, prerequisites, app activity, and raw spool state | [Live Logs](https://github.com/hideouts-io/iOS-Developer-Toolkit#live-logs) | +| Backup fails | Encryption state, free space, destination freshness, unlock state | [Backup](https://github.com/hideouts-io/iOS-Developer-Toolkit#backup) | +| Optional adapter fails | Exact executable path/hash, reported version/build, upstream requirements, independent target state | [Ecosystem Tools](https://github.com/hideouts-io/iOS-Developer-Toolkit#ecosystem-tools) | + +## Use the built-in diagnostics + +1. Run **Retry Scan** for one immediate usbmux check. +2. Use **Reconnect & Retry…** for the guided 30-second physical reconnection window. +3. Run **Capability Matrix** and inspect the first non-ready prerequisite. +4. Use **Check Command Drift** when a guided command may no longer match the installed CLI. +5. Create a **Sanitized Support Bundle**, review it locally, and attach it only when appropriate. + +The toolkit does not attempt to restart SIP-protected Apple services, delete pairing records, use `sudo`, or hide a failed prerequisite behind automatic recovery. + +For support, use [GitHub Discussions](https://github.com/hideouts-io/iOS-Developer-Toolkit/discussions). Follow the [support policy](https://github.com/hideouts-io/iOS-Developer-Toolkit/blob/main/SUPPORT.md) before sharing any output. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..89a93cb --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,61 @@ +site_name: iOS Developer Toolkit +site_description: Guided macOS workbench for authorized iPhone and iPad development, diagnostics, backup, and evidence preservation +site_url: https://hideouts-io.github.io/iOS-Developer-Toolkit/ +repo_url: https://github.com/hideouts-io/iOS-Developer-Toolkit +repo_name: hideouts-io/iOS-Developer-Toolkit +edit_uri: edit/main/docs/ +docs_dir: docs +site_dir: site +strict: true + +theme: + name: material + language: en + features: + - content.code.copy + - navigation.footer + - navigation.indexes + - navigation.sections + - navigation.top + - search.highlight + - search.suggest + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: black + accent: red + toggle: + icon: material/weather-night + name: Use dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: red + toggle: + icon: material/weather-sunny + name: Use light mode + +plugins: + - search + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - tables + - toc: + permalink: true + +extra_css: + - stylesheets/extra.css + +nav: + - Overview: index.md + - Quick start: quick-start.md + - Architecture: architecture.md + - Safety and privacy: safety.md + - Troubleshooting: troubleshooting.md + - Release verification: release-verification.md + - Contributing: contributing.md + - Physical-device testing: PHYSICAL_DEVICE_TEST_PROTOCOL.md + - Product audit and roadmap: PRODUCT_AUDIT_2026-09-21.md diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 0000000..d3504c7 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1 @@ +mkdocs-material==9.7.7 From 4ebbc9a4f7a0443687bea1ad9e04c5aaa8f68b12 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 05:18:29 -0700 Subject: [PATCH 14/16] Export sanitized compatibility reports --- README.md | 10 +- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- ios_developer_toolkit/app.py | 144 +++++++++++++ ios_developer_toolkit/device_compatibility.py | 197 ++++++++++++++++++ ios_developer_toolkit/support_bundle.py | 8 +- tests/test_capability_matrix.py | 93 +++++++++ 6 files changed, 447 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6687c26..2f354e3 100644 --- a/README.md +++ b/README.md @@ -96,12 +96,12 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - **Ecosystem Tools** validates user-selected go-ios, idb Companion, and ipsw executables by path, SHA-256, and version/build identity, then enables one bounded read-only inventory probe per adapter; - the desktop UI starts independently of the MobileBackup2 transport, and device discovery consumes output both while the child process runs and after it exits, so a fast successful `usbmux list` result is not lost before the picker is updated; - **Demo Mode** shows a prominently labeled simulated iPhone for walkthroughs and screenshots, while deliberately withholding a selected physical-device target and disabling device operations; -- the manual **Capability Matrix** reports host, trust, Developer Mode, DDI, tunnel, DVT, CoreDevice, and related readiness as separate bounded results, then compares completed local probes across real devices without retaining raw UDIDs; +- the manual **Capability Matrix** reports host, trust, Developer Mode, DDI, tunnel, DVT, CoreDevice, and related readiness as separate bounded results, then compares completed local probes across real devices and previews sanitized JSON or Markdown exports without disclosing stored device fingerprints; - **DVT network activity** and **CoreDevice applications** are handled as long-running streams with explicit Stop controls instead of misleading finite snapshots; - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, external MVT analysis, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one workbench; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 126 tests, embedded CLI checks, a 134-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 129 tests, embedded CLI checks, a 136-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. The README contains 22 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. @@ -121,7 +121,7 @@ The README contains 22 sanitized screenshots. The six views below provide a quic | Guided Command Drift | Checks the installed help surface for all 49 presets before device work and reports changed routes, options, failures, timeouts, or cancellation. | Read-only; does not run a preset or contact a device. | | Action Safety | Classifies every guided and advanced command as read-only, host-write, device-change, or high-impact. | Device changes require a typed device-bound acknowledgement; high-impact actions additionally require backup acknowledgement and `IRREVERSIBLE`. | | Reconnect & Retry | Opens a bounded, guided 30-second device-detection window. | Does not restart SIP-protected Apple services. | -| Real-Device Compatibility | Compares completed Capability Matrix observations across locally tested devices, builds, and connection types. | Stores a one-way device fingerprint, not raw UDIDs or names. | +| Real-Device Compatibility | Compares completed Capability Matrix observations across locally tested devices, builds, and connection types, then previews sanitized JSON or Markdown reports. | Local history stores a one-way fingerprint; exports omit names, raw identifiers, stored fingerprints, and local paths. | | Guided Cases | Records authorized purpose and scope before a bounded evidence collection. | Creates local intake metadata only; collection remains explicit. | | Investigative Live Logs | Adds references, annotated findings, reviewed findings, raw hashing, and evidence-bundle export to pop-out streams. | Keeps raw output distinct from analyst annotations and does not upload captures. | | Keyboard-first access | Adds named controls, standard navigation, and application-wide workspace shortcuts. | Shortcuts never bypass action confirmation. | @@ -510,6 +510,8 @@ Every row uses one of six explicit states: **Ready**, **Needs attention**, **Una The **Real-Device Compatibility** tab retains a local, append-only observation only after the worker completes a Capability Matrix run against a connected device. It compares the latest observed result for each locally tested device across model, iOS version, build, connection type, and individual capabilities. The history stores a one-way device fingerprint rather than the raw UDID or device name, and it never predicts compatibility for untested hardware or builds. It lives at `~/Library/Application Support/iOS Developer Toolkit/Compatibility/real-device-observations.jsonl`. +**Export Sanitized JSON…** and **Export Sanitized Markdown…** build an exact local preview before saving an owner-only file. The exported report includes the toolkit, macOS, architecture, Python, PySide6, and `pymobiledevice3` versions plus device model, iOS version/build, connection type, probe time, states, and sanitized evidence and remediation. It omits device names, raw identifiers, and stored fingerprints, and redacts common local paths, email addresses, IPv4 addresses, and MAC addresses. The application never uploads the report; model and build metadata can still be identifying in a small fleet, so review the preview before sharing it. + The matrix does not mount a DDI, enable Developer Mode, start a tunnel daemon, change Safari settings, or unlock the device. A service being reachable at refresh time is not proof that every command in that family will succeed, and an empty Web Inspector tab list is different from a failed Web Inspector request. ### Location Lab @@ -1170,7 +1172,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 126 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 134-button offscreen GUI smoke test, verifies live help and a synthetic external-adapter lifecycle from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 129 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 136-button offscreen GUI smoke test, verifies live help and a synthetic external-adapter lifecycle from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 91fbe08..54b9242 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -128,7 +128,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P3 — ecosystem growth and scale -* Opt-in anonymized compatibility contribution workflow with a local preview and explicit export confirmation. +* Maintain the opt-in sanitized compatibility export with an exact local preview, owner-only JSON and Markdown files, host/toolchain context, tested device family and build metadata, and no automatic upload. * Team/workspace import-export that remains local by default. * Notarized Developer ID distribution when an eligible signing identity exists. * Optional device-lab integration through external services, never a mandatory cloud account. @@ -181,6 +181,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Added a guided external MVT handoff for consented decrypted-backup analysis. | 121 tests and a 110-action GUI smoke passed; tests cover executable provenance, secret-environment removal, backup structure/encryption checks, isolated output, explicit IOC arguments, offline defaults, version validation, and an end-to-end synthetic analysis process. | MVT stays separately installed; the toolkit accepts no password and never translates completion or absent findings into a clean-device verdict. | | 2026-09-22 | Added separately installed go-ios, idb Companion, and ipsw adapters with provenance validation. | 126 tests and a 134-action GUI smoke passed; tests cover catalog identity, discovery, executable hashing, changed-binary rejection, upstream version/build formats, secret and target-routing removal, and a synthetic validate/probe lifecycle. | These tools keep their own discovery, pairing, tunnel, target, network, licensing, and support models; only bounded inventory probes are exposed. | | 2026-09-22 | Added a focused Material for MkDocs documentation site and pull-request/push workflow. | `mkdocs build --strict --clean` passes locally; the site routes beginners, developers, investigators, release verifiers, and contributors to canonical repository material without copying the complete README. | GitHub Pages publication occurs only after a documentation change reaches `main`; the site-build dependency is not part of the application bundle. | +| 2026-09-22 | Added previewed, sanitized JSON and Markdown export for real-device compatibility observations. | The 129-test suite and 136-action GUI smoke passed; focused tests cover removal of device identity and stored fingerprints, common path/email redaction, owner-only files, overwrite refusal, empty-history rejection, and both report formats. | Exports remain manual and local; model/build/connection metadata can still identify a small fleet, so the exact payload is previewed before saving and never uploaded. | ## Research sources diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index 433f322..7fe61ac 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -106,13 +106,20 @@ from ios_developer_toolkit.collection_process import CollectionProcessController from ios_developer_toolkit.collection_protocol import CollectionEvent from ios_developer_toolkit.device_compatibility import ( + CompatibilityReport, DeviceCompatibilityError, DeviceCompatibilityObservation, append_observation, compatibility_history_path, + create_compatibility_report, create_observation, + current_report_environment, latest_observations, load_observations, + render_compatibility_json, + render_compatibility_markdown, + write_compatibility_json_report, + write_compatibility_markdown_report, ) from ios_developer_toolkit.demo_mode import demo_connection_banner, demo_device from ios_developer_toolkit.external_tools import ( @@ -1251,6 +1258,20 @@ def _eligible_action_palette_entries(self) -> tuple[ActionPaletteEntry, ...]: ("manpage", "documentation", "syntax"), self.refresh_manpage_button.isEnabled(), ), + ( + "action:export-compatibility-json", + "Export Sanitized Compatibility JSON", + "Export the latest locally observed real-device capability evidence without stable device identity.", + ("compatibility", "matrix", "json", "report", "sanitized"), + self._compatibility_export_is_available(), + ), + ( + "action:export-compatibility-markdown", + "Export Sanitized Compatibility Markdown", + "Export a readable real-device capability report without stable device identity.", + ("compatibility", "matrix", "markdown", "report", "sanitized"), + self._compatibility_export_is_available(), + ), ) entries.extend( action_palette_entry(identifier, title, "Eligible read action", summary, keywords) @@ -1318,6 +1339,8 @@ def _execute_action_palette_entry(self, identifier: str) -> None: "action:backup-encryption-status": self.check_backup_encryption, "action:command-drift": self.start_command_drift_check, "action:refresh-live-help": self.refresh_selected_manpage, + "action:export-compatibility-json": self.export_compatibility_json, + "action:export-compatibility-markdown": self.export_compatibility_markdown, } action = actions.get(identifier) if action is None: @@ -1771,6 +1794,14 @@ def _build_capability_matrix_page(self) -> QWidget: copy_history_button.setObjectName("copyCompatibilityMatrixButton") copy_history_button.clicked.connect(self.copy_compatibility_matrix) compatibility_controls.addWidget(copy_history_button) + export_json_button = QPushButton("Export Sanitized JSON…") + export_json_button.setObjectName("exportCompatibilityJsonButton") + export_json_button.clicked.connect(self.export_compatibility_json) + compatibility_controls.addWidget(export_json_button) + export_markdown_button = QPushButton("Export Sanitized Markdown…") + export_markdown_button.setObjectName("exportCompatibilityMarkdownButton") + export_markdown_button.clicked.connect(self.export_compatibility_markdown) + compatibility_controls.addWidget(export_markdown_button) compatibility_controls.addStretch() compatibility_layout.addLayout(compatibility_controls) self.compatibility_history_status = QLabel() @@ -3840,6 +3871,119 @@ def copy_compatibility_matrix(self) -> None: QApplication.clipboard().setText("\n".join(lines).rstrip() + "\n") self.compatibility_history_status.setText("Copied local real-device compatibility observations to the clipboard.") + def _compatibility_export_is_available(self) -> bool: + return self._compatibility_history_error is None and bool( + latest_observations(self._compatibility_observations) + ) + + def _compatibility_report(self) -> CompatibilityReport: + return create_compatibility_report( + datetime.now(timezone.utc).isoformat(), + current_report_environment(APP_VERSION, is_frozen_runtime()), + self._compatibility_observations, + ) + + def _review_compatibility_export(self, title: str, content: str) -> bool: + dialog = QDialog(self) + dialog.setObjectName("compatibilityExportPreviewDialog") + dialog.setWindowTitle(title) + dialog.resize(900, 650) + layout = QVBoxLayout(dialog) + explanation = QLabel( + "Review the exact sanitized content before saving. Device names, raw identifiers, stored fingerprints, " + "and local paths are excluded or redacted. Device model, iOS version/build, connection type, " + "host/toolchain versions, and sanitized capability evidence remain. The app never uploads this report." + ) + explanation.setWordWrap(True) + layout.addWidget(explanation) + preview = QPlainTextEdit() + preview.setObjectName("compatibilityExportPreview") + preview.setReadOnly(True) + preview.setPlainText(content) + layout.addWidget(preview, 1) + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel) + buttons.setObjectName("compatibilityExportPreviewButtons") + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addWidget(buttons) + return dialog.exec() == QDialog.DialogCode.Accepted + + def export_compatibility_json(self) -> None: + if not self._compatibility_export_is_available(): + QMessageBox.information( + self, + "No Real-Device Observations", + "Complete a Capability Matrix run against a connected device before exporting compatibility evidence.", + ) + return + try: + report = self._compatibility_report() + except DeviceCompatibilityError as error: + QMessageBox.critical(self, "Could Not Prepare Compatibility Report", str(error)) + return + if not self._review_compatibility_export( + "Review Sanitized Compatibility JSON", + render_compatibility_json(report), + ): + return + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%SZ") + suggested = Path.home() / f"iOSDeveloperToolkit-compatibility-{timestamp}.json" + selected, _ = QFileDialog.getSaveFileName( + self, + "Save Sanitized Compatibility JSON", + str(suggested), + "JSON (*.json)", + ) + if not selected: + return + destination = Path(selected) + if destination.suffix.casefold() != ".json": + destination = destination.with_suffix(".json") + try: + path = write_compatibility_json_report(destination, report) + except DeviceCompatibilityError as error: + QMessageBox.critical(self, "Could Not Export Compatibility Report", str(error)) + return + self.compatibility_history_status.setText(f"Created sanitized compatibility JSON: {path}") + + def export_compatibility_markdown(self) -> None: + if not self._compatibility_export_is_available(): + QMessageBox.information( + self, + "No Real-Device Observations", + "Complete a Capability Matrix run against a connected device before exporting compatibility evidence.", + ) + return + try: + report = self._compatibility_report() + except DeviceCompatibilityError as error: + QMessageBox.critical(self, "Could Not Prepare Compatibility Report", str(error)) + return + if not self._review_compatibility_export( + "Review Sanitized Compatibility Markdown", + render_compatibility_markdown(report), + ): + return + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%SZ") + suggested = Path.home() / f"iOSDeveloperToolkit-compatibility-{timestamp}.md" + selected, _ = QFileDialog.getSaveFileName( + self, + "Save Sanitized Compatibility Markdown", + str(suggested), + "Markdown (*.md)", + ) + if not selected: + return + destination = Path(selected) + if destination.suffix.casefold() != ".md": + destination = destination.with_suffix(".md") + try: + path = write_compatibility_markdown_report(destination, report) + except DeviceCompatibilityError as error: + QMessageBox.critical(self, "Could Not Export Compatibility Report", str(error)) + return + self.compatibility_history_status.setText(f"Created sanitized compatibility Markdown: {path}") + def _capability_selection_changed(self) -> None: selected_rows = self.capability_table.selectionModel().selectedRows() if len(selected_rows) != 1: diff --git a/ios_developer_toolkit/device_compatibility.py b/ios_developer_toolkit/device_compatibility.py index 3ff2c65..71977a0 100644 --- a/ios_developer_toolkit/device_compatibility.py +++ b/ios_developer_toolkit/device_compatibility.py @@ -1,14 +1,18 @@ from __future__ import annotations import hashlib +import html import json import os +import platform from dataclasses import asdict, dataclass from pathlib import Path +from sys import version as python_runtime_version from typing import Mapping, Sequence from ios_developer_toolkit.capability_matrix import CapabilityMatrixError, CapabilityResult, parse_capability_result from ios_developer_toolkit.models import IOSDevice +from ios_developer_toolkit.support_bundle import installed_package_version, sanitize_support_text class DeviceCompatibilityError(ValueError): @@ -28,6 +32,28 @@ class DeviceCompatibilityObservation: results: tuple[CapabilityResult, ...] +@dataclass(frozen=True) +class CompatibilityReportEnvironment: + """Host and toolchain metadata that explains one exported compatibility report.""" + + toolkit_version: str + macos_version: str + architecture: str + python_version: str + runtime: str + pymobiledevice3_version: str + pyside6_version: str + + +@dataclass(frozen=True) +class CompatibilityReport: + """A shareable report that deliberately omits stable device identity.""" + + generated_at: str + environment: CompatibilityReportEnvironment + observations: tuple[DeviceCompatibilityObservation, ...] + + def compatibility_history_path(home: Path) -> Path: return ( home.expanduser().resolve() @@ -174,3 +200,174 @@ def latest_observations( if existing is None or observation.recorded_at > existing.recorded_at: latest_by_device[observation.device_fingerprint] = observation return tuple(sorted(latest_by_device.values(), key=lambda item: item.recorded_at)) + + +def current_report_environment(toolkit_version: str, frozen_runtime: bool) -> CompatibilityReportEnvironment: + if not toolkit_version.strip(): + raise DeviceCompatibilityError("Toolkit version is required for a compatibility report") + return CompatibilityReportEnvironment( + toolkit_version=toolkit_version, + macos_version=platform.mac_ver()[0] or "unavailable", + architecture=platform.machine() or "unavailable", + python_version=platform.python_version() or python_runtime_version.split()[0], + runtime="frozen-app" if frozen_runtime else "source-python", + pymobiledevice3_version=installed_package_version("pymobiledevice3"), + pyside6_version=installed_package_version("PySide6"), + ) + + +def create_compatibility_report( + generated_at: str, + environment: CompatibilityReportEnvironment, + observations: Sequence[DeviceCompatibilityObservation], +) -> CompatibilityReport: + if not generated_at.strip(): + raise DeviceCompatibilityError("Compatibility report generation time is required") + environment_values = asdict(environment) + missing_environment_fields = tuple( + key for key, value in environment_values.items() if not isinstance(value, str) or not value.strip() + ) + if missing_environment_fields: + raise DeviceCompatibilityError( + f"Compatibility report environment fields must be non-empty: {missing_environment_fields}" + ) + latest = latest_observations(observations) + if not latest: + raise DeviceCompatibilityError("Cannot export a compatibility report without completed device observations") + return CompatibilityReport(generated_at, environment, latest) + + +def compatibility_report_mapping(report: CompatibilityReport) -> dict[str, object]: + devices: list[dict[str, object]] = [] + for index, observation in enumerate(report.observations, start=1): + devices.append( + { + "report_device": f"device-{index}", + "observed_at": observation.recorded_at, + "product_type": observation.product_type, + "product_version": observation.product_version, + "build_version": observation.build_version, + "connection_type": observation.connection_type, + "capabilities": [ + { + "identifier": result.identifier, + "layer": result.layer, + "title": result.title, + "state": result.state, + "summary": sanitize_support_text(result.summary, ()), + "evidence": sanitize_support_text(result.evidence, ()), + "remediation": sanitize_support_text(result.remediation, ()), + } + for result in observation.results + ], + } + ) + return { + "schema_version": 1, + "generated_at": report.generated_at, + "environment": asdict(report.environment), + "privacy": { + "raw_device_identifiers_included": False, + "device_names_included": False, + "device_fingerprints_included": False, + "local_paths_redacted": True, + "warning": ( + "Device model, iOS version and build, connection type, host/toolchain versions, and sanitized " + "capability evidence remain in this report. Review it before sharing." + ), + }, + "devices": devices, + } + + +def render_compatibility_json(report: CompatibilityReport) -> str: + return json.dumps(compatibility_report_mapping(report), indent=2, sort_keys=True) + "\n" + + +def _markdown_cell(value: str) -> str: + sanitized = sanitize_support_text(value, ()) + return html.escape(sanitized, quote=False).replace("|", "\\|").replace("\n", "
    ") + + +def render_compatibility_markdown(report: CompatibilityReport) -> str: + environment = report.environment + lines = [ + "# iOS Developer Toolkit compatibility report", + "", + f"Generated: `{report.generated_at}`", + "", + "> This sanitized export omits device names, raw identifiers, and stored device fingerprints. It retains device " + "model, iOS version/build, connection type, host/toolchain versions, and sanitized capability evidence. Review " + "it before sharing.", + "", + "## Host and toolchain", + "", + "| Item | Value |", + "|---|---|", + f"| Toolkit | {_markdown_cell(environment.toolkit_version)} |", + f"| macOS | {_markdown_cell(environment.macos_version)} |", + f"| Architecture | {_markdown_cell(environment.architecture)} |", + f"| Runtime | {_markdown_cell(environment.runtime)} |", + f"| Python | {_markdown_cell(environment.python_version)} |", + f"| pymobiledevice3 | {_markdown_cell(environment.pymobiledevice3_version)} |", + f"| PySide6 | {_markdown_cell(environment.pyside6_version)} |", + "", + ] + for index, observation in enumerate(report.observations, start=1): + lines.extend( + ( + f"## Observed device {index}", + "", + "| Item | Value |", + "|---|---|", + f"| Observed at | {_markdown_cell(observation.recorded_at)} |", + f"| Product type | {_markdown_cell(observation.product_type)} |", + f"| iOS | {_markdown_cell(observation.product_version)} |", + f"| Build | {_markdown_cell(observation.build_version)} |", + f"| Connection | {_markdown_cell(observation.connection_type)} |", + "", + "| State | Layer | Capability | Summary | Evidence | Next step |", + "|---|---|---|---|---|---|", + ) + ) + for result in observation.results: + lines.append( + f"| {_markdown_cell(result.state)} | {_markdown_cell(result.layer)} | " + f"{_markdown_cell(result.title)} | {_markdown_cell(result.summary)} | " + f"{_markdown_cell(result.evidence)} | {_markdown_cell(result.remediation)} |" + ) + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +def _write_private_report(destination: Path, expected_suffix: str, content: str) -> Path: + path = destination.expanduser().resolve() + if path.suffix.casefold() != expected_suffix: + raise DeviceCompatibilityError( + f"Compatibility report destination must end in {expected_suffix}: {path}" + ) + if not path.parent.is_dir(): + raise DeviceCompatibilityError(f"Compatibility report parent directory does not exist: {path.parent}") + try: + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + except FileExistsError as error: + raise DeviceCompatibilityError(f"Refusing to overwrite existing compatibility report: {path}") from error + except OSError as error: + raise DeviceCompatibilityError(f"Could not create compatibility report at {path}: {error}") from error + try: + with os.fdopen(descriptor, "wb") as output: + output.write(content.encode("utf-8")) + output.flush() + os.fsync(output.fileno()) + except OSError as error: + path.unlink(missing_ok=True) + raise DeviceCompatibilityError(f"Could not write compatibility report at {path}: {error}") from error + return path + + +def write_compatibility_json_report(destination: Path, report: CompatibilityReport) -> Path: + return _write_private_report(destination, ".json", render_compatibility_json(report)) + + +def write_compatibility_markdown_report(destination: Path, report: CompatibilityReport) -> Path: + return _write_private_report(destination, ".md", render_compatibility_markdown(report)) diff --git a/ios_developer_toolkit/support_bundle.py b/ios_developer_toolkit/support_bundle.py index cfe0638..087eb54 100644 --- a/ios_developer_toolkit/support_bundle.py +++ b/ios_developer_toolkit/support_bundle.py @@ -88,8 +88,8 @@ def _support_entries(context: SupportBundleContext) -> tuple[tuple[str, str], .. "python_implementation": platform.python_implementation(), "python_version": platform.python_version(), "runtime": "frozen-app" if context.frozen_runtime else "source-python", - "pymobiledevice3_version": _installed_package_version("pymobiledevice3"), - "pyside6_version": _installed_package_version("PySide6"), + "pymobiledevice3_version": installed_package_version("pymobiledevice3"), + "pyside6_version": installed_package_version("PySide6"), } context_document: dict[str, JsonDocumentValue] = { "workspace": context.workspace, @@ -145,7 +145,9 @@ def _json_document(value: dict[str, JsonDocumentValue]) -> str: return json.dumps(value, indent=2, sort_keys=True) + "\n" -def _installed_package_version(package: str) -> str: +def installed_package_version(package: str) -> str: + if not package.strip(): + raise SupportBundleError("Package name is required when reading installed version metadata") try: return version(package) except PackageNotFoundError: diff --git a/tests/test_capability_matrix.py b/tests/test_capability_matrix.py index 652d47d..17d8f23 100644 --- a/tests/test_capability_matrix.py +++ b/tests/test_capability_matrix.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os import shutil import tempfile import unittest @@ -27,12 +28,18 @@ ) from ios_developer_toolkit.command_catalog import command_presets from ios_developer_toolkit.device_compatibility import ( + CompatibilityReportEnvironment, DeviceCompatibilityError, append_observation, + compatibility_report_mapping, compatibility_history_path, + create_compatibility_report, create_observation, latest_observations, load_observations, + render_compatibility_markdown, + write_compatibility_json_report, + write_compatibility_markdown_report, ) from ios_developer_toolkit.models import IOSDevice @@ -161,6 +168,92 @@ def test_real_device_observation_rejects_duplicate_capabilities(self) -> None: with self.assertRaises(DeviceCompatibilityError): create_observation("2026-09-14T10:00:00+00:00", device, (untested_capability_results()[0],) * 2) + def test_sanitized_compatibility_report_omits_stable_identity_and_private_paths(self) -> None: + device = IOSDevice("PRIVATE-UDID", "Private iPhone", "iPhone14,5", "26.3.1", "23D123", "USB") + result = CapabilityResult( + "developer-image", + "Developer", + "Developer image", + "ready", + "Ready for PRIVATE-UDID", + "Mounted from /Users/julian/Private/DDI for analyst@example.com", + "No action required", + ) + observation = create_observation("2026-09-14T12:00:00+00:00", device, (result,)) + environment = CompatibilityReportEnvironment( + "0.3.4", + "15.6.1", + "arm64", + "3.13.7", + "source-python", + "11.15.1", + "6.9.3", + ) + report = create_compatibility_report( + "2026-09-22T12:00:00+00:00", + environment, + (observation,), + ) + + payload = json.dumps(compatibility_report_mapping(report), sort_keys=True) + markdown = render_compatibility_markdown(report) + + self.assertNotIn("PRIVATE-UDID", payload) + self.assertNotIn("Private iPhone", payload) + self.assertNotIn(observation.device_fingerprint, payload) + self.assertNotIn("/Users/julian", payload) + self.assertNotIn("analyst@example.com", payload) + self.assertIn("", payload) + self.assertIn("", payload) + self.assertIn("iPhone14,5", markdown) + self.assertIn("pymobiledevice3", markdown) + + def test_compatibility_reports_are_owner_only_and_refuse_overwrite(self) -> None: + temporary_directory = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, temporary_directory) + device = IOSDevice("DEVICE", "One", "iPad14,3", "26.3.1", "23D123", "USB") + observation = create_observation( + "2026-09-14T12:00:00+00:00", + device, + untested_capability_results(), + ) + environment = CompatibilityReportEnvironment( + "0.3.4", + "15.6.1", + "arm64", + "3.13.7", + "frozen-app", + "11.15.1", + "6.9.3", + ) + report = create_compatibility_report( + "2026-09-22T12:00:00+00:00", + environment, + (observation,), + ) + json_path = write_compatibility_json_report(temporary_directory / "compatibility.json", report) + markdown_path = write_compatibility_markdown_report(temporary_directory / "compatibility.md", report) + + self.assertEqual(os.stat(json_path).st_mode & 0o777, 0o600) + self.assertEqual(os.stat(markdown_path).st_mode & 0o777, 0o600) + self.assertEqual(json.loads(json_path.read_text(encoding="utf-8"))["schema_version"], 1) + self.assertIn("## Observed device 1", markdown_path.read_text(encoding="utf-8")) + with self.assertRaises(DeviceCompatibilityError): + write_compatibility_json_report(json_path, report) + + def test_compatibility_report_requires_completed_observations(self) -> None: + environment = CompatibilityReportEnvironment( + "0.3.4", + "15.6.1", + "arm64", + "3.13.7", + "source-python", + "11.15.1", + "6.9.3", + ) + with self.assertRaises(DeviceCompatibilityError): + create_compatibility_report("2026-09-22T12:00:00+00:00", environment, ()) + @unittest.skipIf(shutil.which("xcrun") is None, "xcrun is unavailable") def test_xcode_tool_probe_uses_the_executable_command_wrapper(self) -> None: result = _probe_xcode_tools() From 961b8afa475f83e6e7e4ccf738ac9f08eab9d0eb Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 05:32:43 -0700 Subject: [PATCH 15/16] Add local workspace profiles --- README.md | 14 +- docs/PRODUCT_AUDIT_2026-09-21.md | 3 +- docs/index.md | 2 +- ios_developer_toolkit/app.py | 301 +++++++++++++++++ ios_developer_toolkit/entrypoint.py | 10 + ios_developer_toolkit/workspace_profile.py | 363 +++++++++++++++++++++ tests/test_workspace_profile.py | 104 ++++++ 7 files changed, 792 insertions(+), 5 deletions(-) create mode 100644 ios_developer_toolkit/workspace_profile.py create mode 100644 tests/test_workspace_profile.py diff --git a/README.md b/README.md index 2f354e3..b6c13a3 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ The application executes the project-pinned binary directly. Guided values becom ## Current release additions and visual tour -Release `v0.3.4` combines the complete 12-workspace interface with the latest connection, streaming, packaging, and repository-readiness work: +Release `v0.3.4` combines the complete 13-workspace interface with the latest connection, streaming, packaging, and repository-readiness work: - **Guided Command Drift** checks the live `pymobiledevice3 --help` surface for all 49 presets before a device command is run, highlighting missing routes, changed options, failed checks, and cancellations without contacting a device; - **Action Safety** makes state boundaries explicit: local-output actions require review, device changes require a typed device-bound `RUN` phrase, and high-impact actions additionally require a current-backup acknowledgement and an `IRREVERSIBLE` phrase; @@ -92,6 +92,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - **Selected command readiness** maps each guided Command Center action to the exact connection, trust, Developer Mode, DDI, tunnel, CoreDevice, DVT, or Web Inspector checks it needs, with a one-click route to the bounded read-only matrix; - **Action Palette** (`⌘ K`) searches all workspaces, guided presets, utilities, and currently eligible read actions while withholding device-only operations until a physical target is selected; - **Session Activity** correlates completed typed operations with workspace, target, transport, exact argument vector, timing, terminal status, prerequisite snapshot, output paths, and output hashes without automatically persisting raw command output; +- **Workspace Profiles** preview and import/export reviewed control defaults for team reuse without including device identity, credentials, paths, coordinates, case text, command parameters, or output; - **MVT Analysis** validates a user-installed `mvt-ios` executable and runs a consented decrypted-backup analysis with isolated output, opt-in indicators, network access off by default, no password input, and no clean-device verdict; - **Ecosystem Tools** validates user-selected go-ios, idb Companion, and ipsw executables by path, SHA-256, and version/build identity, then enables one bounded read-only inventory probe per adapter; - the desktop UI starts independently of the MobileBackup2 transport, and device discovery consumes output both while the child process runs and after it exits, so a fast successful `usbmux list` result is not lost before the picker is updated; @@ -101,7 +102,7 @@ Release `v0.3.4` combines the complete 12-workspace interface with the latest co - Unified Logs, classic syslog, and DVT OSLog use independent pop-out windows with raw spooling, pause, filtering, save, and explicit close behavior; - Location Lab supports validated coordinates, saved places, offline map selection, generated routes, GPX playback, event evidence, and explicit location clearing; - app inventory, local IPA inspection, eligible installation, encrypted MobileBackup2 workflows, isolated UFADE launch, external MVT analysis, PCAP, screenshots, crashes, and hashed evidence cases are integrated into one workbench; -- native Apple Silicon and Intel release ZIPs are built separately and verified with 129 tests, embedded CLI checks, a 136-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; +- native Apple Silicon and Intel release ZIPs are built separately and verified with 133 tests, embedded CLI checks, a 138-button GUI smoke test, full-bundle architecture and deployment-floor inspection, strict code-signature validation, and one SHA-256 manifest; - public contribution paths now include structured issues, Discussions, pull requests, CI, CodeQL, dependency review, Dependabot, private vulnerability reporting, and protected `main`. The README contains 22 sanitized screenshots. The six views below provide a quick tour; each workspace section later in the README contains the relevant full-size image and operational walkthrough. @@ -129,6 +130,7 @@ The README contains 22 sanitized screenshots. The six views below provide a quic | Demo Mode | Shows a local simulated iPhone for an honest product walkthrough or screenshot. | The banner identifies the simulation and no device service, command, mount, capture, backup, or location operation can run. | | Eligible Action Palette | Searches workspaces, utilities, guided presets, and read actions that are valid for the current device and process state. | Selecting a preset opens it for review; it never runs automatically or bypasses confirmation. | | Session Activity | Correlates completed typed operations and previews an exportable structured JSON manifest. | Session-only by default; raw output is omitted, and explicit exports can still contain identifiers and local paths. | +| Workspace Profiles | Shares reviewed DDI, guided-command, app, backup, evidence, and location-control defaults as validated JSON. | Exact preview; owner-only, no-overwrite export; import changes controls only and never runs a command. | | Guided MVT handoff | Validates and records external MVT provenance, then analyzes one authorized decrypted backup into a new result path. | No password input; inherited password/IOC variables are removed, network is off by default, and no result is translated into a clean-device claim. | ## What the workbench covers @@ -173,6 +175,12 @@ Each record distinguishes the workspace and target from the transport, exact arg Use **Copy Selected Manifest** or **Save Selected Manifest…** only when you intend to preserve a record. A saved JSON file is created with owner-only permissions and is never overwritten. Because exact arguments and targets can include a UDID, device details, IPA or GPX paths, case locations, and other sensitive values, review the manifest before sharing it. Session Activity is separate from the sanitized support bundle, which continues to exclude command arguments and device identity. +### Local workspace profiles + +Use **Export Workspace…** and **Import Workspace…** below the workspace list to share reviewed workflow defaults without copying operational data. A profile can set the default workspace, DDI source, guided-command category and preset, app-inventory and IPA-install options, backup policy, evidence coverage and duration, and non-coordinate Location Lab timing and route-builder settings. + +Export shows the exact JSON before creating an owner-only file and refuses to overwrite an existing file. Import accepts only the bounded versioned schema, previews every proposed setting, refuses to apply while an operation is active, and rechecks the imported values immediately before changing the controls. It does not persist automatically, run a command, choose a device, fill a path, import an acknowledgement, or start a device action. The schema excludes device identity, credential fields, local paths, coordinates, command parameters, case text, and captured output. Profile name and description are user-supplied text; common path, account, device, and network identifier patterns are rejected, but the exact preview still must be reviewed before sharing. + ### Sanitized support bundle **Create Support Bundle…** in the window header creates a local ZIP for a bug report or support request. It is opt-in and never uploads anything. The ZIP contains toolkit and dependency versions, macOS/Python metadata, selected workspace, device count without identity, aggregate Capability Matrix states, sanitized status summaries, the sanitized Command Drift report, and a SHA-256 manifest. @@ -1172,7 +1180,7 @@ Physical-device validation is opt-in and is not required for pull requests. Use ### Release model -The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 129 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 136-button offscreen GUI smoke test, verifies live help and a synthetic external-adapter lifecycle from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. +The release workflow builds natively on separate Apple Silicon and Intel GitHub-hosted macOS runners. Each job creates a self-contained PySide6/Nuitka `.app`, runs all 133 tests, verifies the embedded pymobiledevice3 command, checks the internal worker route, runs the 138-button offscreen GUI smoke test, verifies live help and a synthetic external-adapter lifecycle from inside the app, verifies the native launcher architecture, and checks the architecture and macOS deployment floor of every bundled Mach-O file. It also embeds third-party notices and a CycloneDX SBOM with the serial number required for GitHub attestation, applies an ad-hoc signature, and uploads an architecture-labeled ZIP and SBOM. The release job publishes both architectures with one SHA-256 inventory and creates GitHub build-provenance and SBOM attestations for each ZIP. The builder requires `MACOSX_DEPLOYMENT_TARGET=13.0`. It rejects any bundled executable, library, extension, or framework slice that requires a newer macOS version or omits the native release architecture. A component may support an older minimum because the application still advertises macOS 13 as its supported floor. PySide6 is pinned to the newest validated line whose actual Shiboken load commands satisfy that floor; wheel filenames alone are not treated as compatibility evidence. Local release builds should use a Python toolchain capable of producing macOS 13-compatible binaries; GitHub release CI supplies the target explicitly. diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 54b9242..73b3601 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -129,7 +129,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack ### P3 — ecosystem growth and scale * Maintain the opt-in sanitized compatibility export with an exact local preview, owner-only JSON and Markdown files, host/toolchain context, tested device family and build metadata, and no automatic upload. -* Team/workspace import-export that remains local by default. +* Maintain local team/workspace profile import-export with a strict versioned schema, exact preview, owner-only export, active-operation guard, and no targets, paths, credentials, coordinates, parameters, case text, or output. * Notarized Developer ID distribution when an eligible signing identity exists. * Optional device-lab integration through external services, never a mandatory cloud account. @@ -182,6 +182,7 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack | 2026-09-22 | Added separately installed go-ios, idb Companion, and ipsw adapters with provenance validation. | 126 tests and a 134-action GUI smoke passed; tests cover catalog identity, discovery, executable hashing, changed-binary rejection, upstream version/build formats, secret and target-routing removal, and a synthetic validate/probe lifecycle. | These tools keep their own discovery, pairing, tunnel, target, network, licensing, and support models; only bounded inventory probes are exposed. | | 2026-09-22 | Added a focused Material for MkDocs documentation site and pull-request/push workflow. | `mkdocs build --strict --clean` passes locally; the site routes beginners, developers, investigators, release verifiers, and contributors to canonical repository material without copying the complete README. | GitHub Pages publication occurs only after a documentation change reaches `main`; the site-build dependency is not part of the application bundle. | | 2026-09-22 | Added previewed, sanitized JSON and Markdown export for real-device compatibility observations. | The 129-test suite and 136-action GUI smoke passed; focused tests cover removal of device identity and stored fingerprints, common path/email redaction, owner-only files, overwrite refusal, empty-history rejection, and both report formats. | Exports remain manual and local; model/build/connection metadata can still identify a small fleet, so the exact payload is previewed before saving and never uploaded. | +| 2026-09-22 | Added local team/workspace profile import and export for reviewed non-sensitive control defaults. | The 133-test suite and 138-action GUI smoke passed; tests cover strict parsing, known workspaces and presets, bounds, forward-compatible extra fields, owner-only files, overwrite refusal, size limits, round trips, and synthetic GUI application. | Profiles never contain targets, paths, credentials, coordinates, command parameters, case text, or output; import changes controls only and is blocked while operations are active. | ## Research sources diff --git a/docs/index.md b/docs/index.md index 8dcf942..993a5cb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -61,7 +61,7 @@ Use the public tests, dual-architecture packaging checks, SBOMs, checksums, and ## Current product map -The application contains 13 workspaces spanning connection and DDI readiness, location testing, live logs, guided commands, app inventory, backup providers, IPA inspection, evidence capture, optional ecosystem tools, installed-command help, and visible scope boundaries. +The application contains 13 workspaces spanning connection and DDI readiness, location testing, live logs, guided commands, app inventory, backup providers, IPA inspection, evidence capture, optional ecosystem tools, installed-command help, and visible scope boundaries. Local workspace profiles can move reviewed control defaults between team members without carrying targets, paths, credentials, coordinates, case text, parameters, or output. The [canonical README](https://github.com/hideouts-io/iOS-Developer-Toolkit#readme) remains the complete feature reference and screenshot walkthrough. This site separates the most common audience paths so setup, architecture, safety, troubleshooting, release verification, and contribution material are easier to find without maintaining a second copy of every command. diff --git a/ios_developer_toolkit/app.py b/ios_developer_toolkit/app.py index 7fe61ac..abfb732 100644 --- a/ios_developer_toolkit/app.py +++ b/ios_developer_toolkit/app.py @@ -261,6 +261,19 @@ macos_setup_commands, ) from ios_developer_toolkit.validation import output_indicates_failure +from ios_developer_toolkit.workspace_profile import ( + AppWorkflowPreferences, + BackupWorkflowPreferences, + EvidenceWorkflowPreferences, + LocationWorkflowPreferences, + WorkspaceProfile, + WorkspaceProfileError, + load_workspace_profile, + render_workspace_profile_json, + render_workspace_profile_preview, + validate_workspace_profile, + write_workspace_profile, +) from ios_developer_toolkit.xcode_handoff import ( XcodeHandoffError, coredevice_details_handoff, @@ -866,6 +879,20 @@ def _build_ui(self) -> None: ) self.session_activity_button.clicked.connect(self.show_session_activity) sidebar_layout.addWidget(self.session_activity_button) + self.export_workspace_profile_button = QPushButton("Export Workspace…") + self.export_workspace_profile_button.setObjectName("exportWorkspaceProfileButton") + self.export_workspace_profile_button.setToolTip( + "Export reviewed control defaults without device identity, paths, credentials, coordinates, or output" + ) + self.export_workspace_profile_button.clicked.connect(self.export_workspace_profile) + sidebar_layout.addWidget(self.export_workspace_profile_button) + self.import_workspace_profile_button = QPushButton("Import Workspace…") + self.import_workspace_profile_button.setObjectName("importWorkspaceProfileButton") + self.import_workspace_profile_button.setToolTip( + "Preview and apply a local workspace profile without running any command" + ) + self.import_workspace_profile_button.clicked.connect(self.import_workspace_profile) + sidebar_layout.addWidget(self.import_workspace_profile_button) version_note = QLabel(f"Toolkit {APP_VERSION}\npymobiledevice3 11.15.1") version_note.setObjectName("sidebarVersion") version_note.setWordWrap(True) @@ -927,6 +954,14 @@ def _configure_accessibility(self) -> None: self.session_activity_button.setAccessibleDescription( "Review completed typed operations and explicitly export a selected structured manifest." ) + self.export_workspace_profile_button.setAccessibleName("Export workspace profile") + self.export_workspace_profile_button.setAccessibleDescription( + "Preview and save non-sensitive workflow control defaults without running a command." + ) + self.import_workspace_profile_button.setAccessibleName("Import workspace profile") + self.import_workspace_profile_button.setAccessibleDescription( + "Preview and apply validated workflow control defaults without running a command." + ) self.action_palette_button.setAccessibleName("Action palette") self.action_palette_button.setAccessibleDescription( "Search workspaces, guided commands, and actions eligible in the current app state. Shortcut: Command K." @@ -1182,6 +1217,20 @@ def _eligible_action_palette_entries(self) -> tuple[ActionPaletteEntry, ...]: "Review keyboard-first navigation without running a device action.", ("accessibility", "keyboard", "hotkeys"), ), + action_palette_entry( + "utility:export-workspace-profile", + "Export Workspace Profile", + "Utility", + "Preview and save non-sensitive workflow control defaults for local or team reuse.", + ("team", "workspace", "profile", "configuration", "export"), + ), + action_palette_entry( + "utility:import-workspace-profile", + "Import Workspace Profile", + "Utility", + "Preview and apply validated workflow control defaults without running a command.", + ("team", "workspace", "profile", "configuration", "import"), + ), ) ) if self.refresh_devices_button.isEnabled() and not self._demo_mode: @@ -1329,6 +1378,8 @@ def _execute_action_palette_entry(self, identifier: str) -> None: actions: Mapping[str, Callable[[], None]] = { "utility:session-activity": self.show_session_activity, "utility:keyboard-shortcuts": self.show_keyboard_shortcuts, + "utility:export-workspace-profile": self.export_workspace_profile, + "utility:import-workspace-profile": self.import_workspace_profile, "action:retry-device-scan": self._scanner_scan, "action:developer-mode-status": self.check_developer_mode, "action:list-developer-images": self.list_mounted_images, @@ -1382,6 +1433,256 @@ def show_session_activity(self) -> None: dialog = OperationHistoryDialog(self._operation_records, self) dialog.exec() + def _request_workspace_profile_metadata(self) -> tuple[str, str] | None: + dialog = QDialog(self) + dialog.setObjectName("workspaceProfileMetadataDialog") + dialog.setWindowTitle("Describe Workspace Profile") + layout = QVBoxLayout(dialog) + explanation = QLabel( + "The profile contains reviewed control defaults only. Device identity, paths, credentials, coordinates, " + "case text, command parameters, and output are excluded by schema." + ) + explanation.setWordWrap(True) + layout.addWidget(explanation) + form = QFormLayout() + name_field = QLineEdit("Team workflow") + name_field.setObjectName("workspaceProfileName") + form.addRow("Name", name_field) + description_field = QLineEdit() + description_field.setObjectName("workspaceProfileDescription") + description_field.setPlaceholderText("Purpose or expected use; do not enter sensitive data") + form.addRow("Description", description_field) + layout.addLayout(form) + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel) + buttons.setObjectName("workspaceProfileMetadataButtons") + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addWidget(buttons) + name_field.selectAll() + name_field.setFocus(Qt.FocusReason.OtherFocusReason) + if dialog.exec() != QDialog.DialogCode.Accepted: + return None + return name_field.text(), description_field.text() + + def _workspace_profile_from_controls(self, name: str, description: str) -> WorkspaceProfile: + current_item = self.navigation_list.currentItem() + if current_item is None: + raise WorkspaceProfileError("Cannot export a profile without a selected workspace") + preset = self._current_preset + if preset is None: + raise WorkspaceProfileError("Cannot export a profile without a selected guided command preset") + speed_preset = self.location_route_speed_preset.currentData() + if not isinstance(speed_preset, int): + raise WorkspaceProfileError("Cannot export a profile without a valid location speed preset") + return validate_workspace_profile( + WorkspaceProfile( + created_with_version=APP_VERSION, + name=name, + description=description, + default_workspace=current_item.text(), + ddi_source="local-xcode" if self.local_radio.isChecked() else "personalized", + command_category=self.command_category_combo.currentText(), + command_preset=preset.identifier, + app_workflow=AppWorkflowPreferences( + self.calculate_app_sizes_checkbox.isChecked(), + self.developer_package_checkbox.isChecked(), + ), + backup_workflow=BackupWorkflowPreferences( + self.full_backup_checkbox.isChecked(), + self.require_encryption_checkbox.isChecked(), + ), + evidence_workflow=EvidenceWorkflowPreferences( + self.capture_duration.value(), + self.include_syslog.isChecked(), + self.include_oslog.isChecked(), + self.include_pcap.isChecked(), + self.include_screenshot.isChecked(), + self.include_crash_pull.isChecked(), + ), + location_workflow=LocationWorkflowPreferences( + self.location_timing_randomness.value(), + self.location_disable_sleep.isChecked(), + speed_preset, + self.location_route_speed.value(), + self.location_route_interval.value(), + self.location_route_traversals.value(), + ), + ) + ) + + def _review_workspace_profile( + self, + title: str, + explanation_text: str, + content: str, + accept_label: str, + ) -> bool: + dialog = QDialog(self) + dialog.setObjectName("workspaceProfilePreviewDialog") + dialog.setWindowTitle(title) + dialog.resize(820, 650) + layout = QVBoxLayout(dialog) + explanation = QLabel(explanation_text) + explanation.setWordWrap(True) + layout.addWidget(explanation) + preview = QPlainTextEdit() + preview.setObjectName("workspaceProfilePreview") + preview.setReadOnly(True) + preview.setPlainText(content) + layout.addWidget(preview, 1) + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel) + buttons.setObjectName("workspaceProfilePreviewButtons") + buttons.addButton(accept_label, QDialogButtonBox.ButtonRole.AcceptRole) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addWidget(buttons) + return dialog.exec() == QDialog.DialogCode.Accepted + + def export_workspace_profile(self) -> None: + metadata = self._request_workspace_profile_metadata() + if metadata is None: + return + try: + profile = self._workspace_profile_from_controls(*metadata) + except WorkspaceProfileError as error: + QMessageBox.critical(self, "Could Not Prepare Workspace Profile", str(error)) + return + if not self._review_workspace_profile( + "Review Workspace Profile Export", + "Review the exact JSON before saving. The application never uploads the file.", + render_workspace_profile_json(profile), + "Save Profile", + ): + return + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%SZ") + suggested = Path.home() / f"iOSDeveloperToolkit-workspace-{timestamp}.json" + selected, _ = QFileDialog.getSaveFileName( + self, + "Save Workspace Profile", + str(suggested), + "JSON (*.json)", + ) + if not selected: + return + destination = Path(selected) + if destination.suffix.casefold() != ".json": + destination = destination.with_suffix(".json") + try: + path = write_workspace_profile(destination, profile) + except WorkspaceProfileError as error: + QMessageBox.critical(self, "Could Not Export Workspace Profile", str(error)) + return + QMessageBox.information( + self, + "Workspace Profile Created", + f"Created owner-only local profile:\n{path}\n\nReview it before sharing.", + ) + + def _workspace_profile_import_is_available(self) -> bool: + finite_controllers = ( + self._action_controller, + self._ipa_inspection_controller, + self._sideload_controller, + self._apps_controller, + self._external_tool_controller, + self._manpage_controller, + self._command_drift_controller, + ) + stream_controllers = ( + self._collection_controller, + self._backup_controller, + self._mvt_controller, + self._console_controller, + ) + return ( + self._capability_process is None + and self._location_process is None + and all(not controller.is_running() for controller in finite_controllers) + and all(not controller.is_running() for controller in stream_controllers) + ) + + def import_workspace_profile(self) -> None: + if not self._workspace_profile_import_is_available(): + QMessageBox.information( + self, + "Workspace Profile Import Unavailable", + "Stop or wait for active operations before changing workflow controls.", + ) + return + selected, _ = QFileDialog.getOpenFileName( + self, + "Open Workspace Profile", + str(Path.home()), + "JSON (*.json)", + ) + if not selected: + return + try: + profile = load_workspace_profile(Path(selected)) + except WorkspaceProfileError as error: + QMessageBox.critical(self, "Invalid Workspace Profile", str(error)) + return + if not self._review_workspace_profile( + "Review Workspace Profile Import", + "Review every control change. Applying this profile never runs a command or starts a device operation.", + render_workspace_profile_preview(profile), + "Apply Profile", + ): + return + try: + self._apply_workspace_profile(profile) + except WorkspaceProfileError as error: + QMessageBox.critical(self, "Could Not Apply Workspace Profile", str(error)) + return + QMessageBox.information( + self, + "Workspace Profile Applied", + f"Applied {profile.name!r}. No command or device operation was started.", + ) + + def _apply_workspace_profile(self, profile: WorkspaceProfile) -> None: + validated = validate_workspace_profile(profile) + if not self._workspace_profile_import_is_available(): + raise WorkspaceProfileError("An operation started while the workspace profile was being reviewed") + self.personalized_radio.setChecked(validated.ddi_source == "personalized") + self.local_radio.setChecked(validated.ddi_source == "local-xcode") + self.command_category_combo.setCurrentText(validated.command_category) + self.command_search_field.clear() + matching_rows = tuple( + row + for row in range(self.command_preset_list.count()) + if self.command_preset_list.item(row).data(Qt.ItemDataRole.UserRole) == validated.command_preset + ) + if len(matching_rows) != 1: + raise WorkspaceProfileError( + f"Validated preset is not visible in its configured category: {validated.command_preset!r}" + ) + self.command_preset_list.setCurrentRow(matching_rows[0]) + self.calculate_app_sizes_checkbox.setChecked(validated.app_workflow.calculate_app_sizes) + self.developer_package_checkbox.setChecked(validated.app_workflow.install_as_developer_package) + self.full_backup_checkbox.setChecked(validated.backup_workflow.force_full_backup) + self.require_encryption_checkbox.setChecked(validated.backup_workflow.require_encryption) + evidence = validated.evidence_workflow + self.capture_duration.setValue(evidence.capture_duration_seconds) + self.include_syslog.setChecked(evidence.include_syslog) + self.include_oslog.setChecked(evidence.include_oslog) + self.include_pcap.setChecked(evidence.include_pcap) + self.include_screenshot.setChecked(evidence.include_screenshot) + self.include_crash_pull.setChecked(evidence.include_crash_pull) + location = validated.location_workflow + self.location_timing_randomness.setValue(location.timing_randomness_ms) + self.location_disable_sleep.setChecked(location.ignore_timing_delays) + speed_index = self.location_route_speed_preset.findData(location.route_speed_preset_kmh) + if speed_index < 0: + raise WorkspaceProfileError( + f"Validated location speed preset is unavailable: {location.route_speed_preset_kmh}" + ) + self.location_route_speed_preset.setCurrentIndex(speed_index) + self.location_route_speed.setValue(location.route_speed_kmh) + self.location_route_interval.setValue(location.route_interval_seconds) + self.location_route_traversals.setValue(location.route_traversals) + self.navigate_to_page(validated.default_workspace) + def _host_operation_context( self, title: str, diff --git a/ios_developer_toolkit/entrypoint.py b/ios_developer_toolkit/entrypoint.py index f1dfeb2..997c2e3 100644 --- a/ios_developer_toolkit/entrypoint.py +++ b/ios_developer_toolkit/entrypoint.py @@ -350,6 +350,16 @@ def run_smoke_test(arguments: Sequence[str]) -> int: support_bundle_button = window.findChild(QPushButton, "createSupportBundleButton") if support_bundle_button is None: raise RuntimeError("GUI support-bundle action is missing") + for button_name in ("exportWorkspaceProfileButton", "importWorkspaceProfileButton"): + if window.findChild(QPushButton, button_name) is None: + raise RuntimeError(f"GUI workspace-profile action is missing: {button_name}") + workspace_profile = window._workspace_profile_from_controls( + "Smoke profile", + "Synthetic non-sensitive control defaults", + ) + window._apply_workspace_profile(workspace_profile) + if window.navigation_list.currentItem() is None: + raise RuntimeError("GUI workspace-profile application lost the selected workspace") demo_mode_button = window.findChild(QPushButton, "demoModeButton") if demo_mode_button is None: raise RuntimeError("GUI demo-mode action is missing") diff --git a/ios_developer_toolkit/workspace_profile.py b/ios_developer_toolkit/workspace_profile.py new file mode 100644 index 0000000..3ea79e1 --- /dev/null +++ b/ios_developer_toolkit/workspace_profile.py @@ -0,0 +1,363 @@ +from __future__ import annotations + +import json +import os +from dataclasses import asdict, dataclass, replace +from pathlib import Path +from typing import Mapping + +from ios_developer_toolkit.command_catalog import command_presets, preset_categories +from ios_developer_toolkit.support_bundle import sanitize_support_text + + +MAX_WORKSPACE_PROFILE_BYTES = 1_048_576 +WORKSPACE_NAMES = ( + "Home", + "Device & DDI", + "Capability Matrix", + "Location Lab", + "Live Logs", + "Command Center", + "Installed Apps", + "Backup", + "Sideload IPA", + "Evidence Capture", + "Ecosystem Tools", + "Man Pages", + "Scope & Safety", +) +DDI_SOURCES = ("personalized", "local-xcode") + + +class WorkspaceProfileError(ValueError): + """Raised when a local, shareable workspace profile is invalid.""" + + +@dataclass(frozen=True) +class AppWorkflowPreferences: + calculate_app_sizes: bool + install_as_developer_package: bool + + +@dataclass(frozen=True) +class BackupWorkflowPreferences: + force_full_backup: bool + require_encryption: bool + + +@dataclass(frozen=True) +class EvidenceWorkflowPreferences: + capture_duration_seconds: int + include_syslog: bool + include_oslog: bool + include_pcap: bool + include_screenshot: bool + include_crash_pull: bool + + +@dataclass(frozen=True) +class LocationWorkflowPreferences: + timing_randomness_ms: int + ignore_timing_delays: bool + route_speed_preset_kmh: int + route_speed_kmh: int + route_interval_seconds: int + route_traversals: int + + +@dataclass(frozen=True) +class WorkspaceProfile: + created_with_version: str + name: str + description: str + default_workspace: str + ddi_source: str + command_category: str + command_preset: str + app_workflow: AppWorkflowPreferences + backup_workflow: BackupWorkflowPreferences + evidence_workflow: EvidenceWorkflowPreferences + location_workflow: LocationWorkflowPreferences + + +def _validated_text(value: str, label: str, maximum_length: int, allow_empty: bool) -> str: + if not isinstance(value, str): + raise WorkspaceProfileError(f"Workspace profile {label} must be a string") + normalized = value.strip() + if not normalized and not allow_empty: + raise WorkspaceProfileError(f"Workspace profile {label} is required") + if len(normalized) > maximum_length: + raise WorkspaceProfileError( + f"Workspace profile {label} exceeds {maximum_length} characters: {len(normalized)}" + ) + if any(not character.isprintable() for character in normalized): + raise WorkspaceProfileError(f"Workspace profile {label} contains control characters") + if sanitize_support_text(normalized, ()) != normalized: + raise WorkspaceProfileError( + f"Workspace profile {label} appears to contain a local path, account, device, or network identifier" + ) + return normalized + + +def validate_workspace_profile(profile: WorkspaceProfile) -> WorkspaceProfile: + created_with_version = _validated_text(profile.created_with_version, "toolkit version", 40, False) + name = _validated_text(profile.name, "name", 100, False) + description = _validated_text(profile.description, "description", 500, True) + if profile.default_workspace not in WORKSPACE_NAMES: + raise WorkspaceProfileError(f"Unknown default workspace: {profile.default_workspace!r}") + if profile.ddi_source not in DDI_SOURCES: + raise WorkspaceProfileError(f"Unknown DDI source preference: {profile.ddi_source!r}") + categories = ("All categories", *preset_categories()) + if profile.command_category not in categories: + raise WorkspaceProfileError(f"Unknown command category: {profile.command_category!r}") + presets = {preset.identifier: preset for preset in command_presets()} + preset = presets.get(profile.command_preset) + if preset is None: + raise WorkspaceProfileError(f"Unknown guided command preset: {profile.command_preset!r}") + if profile.command_category != "All categories" and preset.category != profile.command_category: + raise WorkspaceProfileError( + f"Guided preset {profile.command_preset!r} is not in category {profile.command_category!r}" + ) + boolean_values = { + "calculate app sizes": profile.app_workflow.calculate_app_sizes, + "install as developer package": profile.app_workflow.install_as_developer_package, + "force full backup": profile.backup_workflow.force_full_backup, + "require encryption": profile.backup_workflow.require_encryption, + "include syslog": profile.evidence_workflow.include_syslog, + "include oslog": profile.evidence_workflow.include_oslog, + "include pcap": profile.evidence_workflow.include_pcap, + "include screenshot": profile.evidence_workflow.include_screenshot, + "include crash pull": profile.evidence_workflow.include_crash_pull, + "ignore timing delays": profile.location_workflow.ignore_timing_delays, + } + invalid_boolean_fields = tuple( + label for label, value in boolean_values.items() if not isinstance(value, bool) + ) + if invalid_boolean_fields: + raise WorkspaceProfileError( + f"Workspace profile boolean fields are invalid: {invalid_boolean_fields}" + ) + _bounded_integer(profile.evidence_workflow.capture_duration_seconds, "capture duration", 10, 3600) + _bounded_integer(profile.location_workflow.timing_randomness_ms, "timing randomness", 0, 60000) + allowed_speed_presets = (5, 10, 20, 40, 100) + if profile.location_workflow.route_speed_preset_kmh not in allowed_speed_presets: + raise WorkspaceProfileError( + f"Route speed preset must be one of {allowed_speed_presets}: " + f"{profile.location_workflow.route_speed_preset_kmh}" + ) + _bounded_integer(profile.location_workflow.route_speed_kmh, "route speed", 1, 300) + _bounded_integer(profile.location_workflow.route_interval_seconds, "route interval", 1, 60) + _bounded_integer(profile.location_workflow.route_traversals, "route traversals", 1, 20) + return replace( + profile, + created_with_version=created_with_version, + name=name, + description=description, + ) + + +def _bounded_integer(value: int, label: str, minimum: int, maximum: int) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value < minimum or value > maximum: + raise WorkspaceProfileError( + f"Workspace profile {label} must be between {minimum} and {maximum}: {value!r}" + ) + return value + + +def workspace_profile_mapping(profile: WorkspaceProfile) -> dict[str, object]: + validated = validate_workspace_profile(profile) + return { + "schema_version": 1, + "created_with_version": validated.created_with_version, + "name": validated.name, + "description": validated.description, + "default_workspace": validated.default_workspace, + "settings": { + "ddi_source": validated.ddi_source, + "command": { + "category": validated.command_category, + "preset": validated.command_preset, + }, + "app_workflow": asdict(validated.app_workflow), + "backup_workflow": asdict(validated.backup_workflow), + "evidence_workflow": asdict(validated.evidence_workflow), + "location_workflow": asdict(validated.location_workflow), + }, + "privacy": { + "schema_excludes": [ + "device identity and targets", + "credentials and authorization acknowledgements", + "local paths and coordinates", + "command parameters", + "case text and capture output", + ], + "user_supplied_text_fields": ["name", "description"], + "warning": "Review the user-supplied name and description before sharing.", + }, + } + + +def render_workspace_profile_json(profile: WorkspaceProfile) -> str: + return json.dumps(workspace_profile_mapping(profile), indent=2, sort_keys=True) + "\n" + + +def render_workspace_profile_preview(profile: WorkspaceProfile) -> str: + validated = validate_workspace_profile(profile) + evidence = validated.evidence_workflow + location = validated.location_workflow + return ( + f"Profile: {validated.name}\n" + f"Description: {validated.description or '(none)'}\n" + f"Created with toolkit: {validated.created_with_version}\n" + f"Default workspace: {validated.default_workspace}\n" + f"DDI source: {validated.ddi_source}\n" + f"Guided command category: {validated.command_category}\n" + f"Guided command preset: {validated.command_preset}\n" + "\n" + "App workflow\n" + f" Calculate app sizes: {validated.app_workflow.calculate_app_sizes}\n" + f" Install as developer package: {validated.app_workflow.install_as_developer_package}\n" + "\n" + "Backup workflow\n" + f" Force full backup: {validated.backup_workflow.force_full_backup}\n" + f" Require encryption: {validated.backup_workflow.require_encryption}\n" + "\n" + "Evidence workflow\n" + f" Capture duration: {evidence.capture_duration_seconds} seconds\n" + f" Classic syslog: {evidence.include_syslog}\n" + f" DVT OSLog: {evidence.include_oslog}\n" + f" PCAP: {evidence.include_pcap}\n" + f" Screenshot: {evidence.include_screenshot}\n" + f" Crash pull: {evidence.include_crash_pull}\n" + "\n" + "Location workflow\n" + f" Timing randomness: {location.timing_randomness_ms} ms\n" + f" Ignore timing delays: {location.ignore_timing_delays}\n" + f" Route speed preset: {location.route_speed_preset_kmh} km/h\n" + f" Route speed: {location.route_speed_kmh} km/h\n" + f" Point interval: {location.route_interval_seconds} seconds\n" + f" Traversals: {location.route_traversals}\n" + "\n" + "Excluded by schema: device identity, credentials, paths, coordinates, command parameters, case text, and output.\n" + "Importing changes visible controls only. It never runs a command or starts a device operation.\n" + ) + + +def _required_mapping(record: Mapping[str, object], key: str) -> Mapping[str, object]: + value = record.get(key) + if not isinstance(value, dict): + raise WorkspaceProfileError(f"Workspace profile field {key!r} must be a JSON object") + return value + + +def _required_string(record: Mapping[str, object], key: str) -> str: + value = record.get(key) + if not isinstance(value, str): + raise WorkspaceProfileError(f"Workspace profile field {key!r} must be a string") + return value + + +def _required_boolean(record: Mapping[str, object], key: str) -> bool: + value = record.get(key) + if not isinstance(value, bool): + raise WorkspaceProfileError(f"Workspace profile field {key!r} must be a boolean") + return value + + +def _required_integer(record: Mapping[str, object], key: str) -> int: + value = record.get(key) + if not isinstance(value, int) or isinstance(value, bool): + raise WorkspaceProfileError(f"Workspace profile field {key!r} must be an integer") + return value + + +def parse_workspace_profile(record: Mapping[str, object]) -> WorkspaceProfile: + schema_version = record.get("schema_version") + if not isinstance(schema_version, int) or isinstance(schema_version, bool) or schema_version != 1: + raise WorkspaceProfileError("Workspace profile has an unsupported schema version") + settings = _required_mapping(record, "settings") + command = _required_mapping(settings, "command") + app_workflow = _required_mapping(settings, "app_workflow") + backup_workflow = _required_mapping(settings, "backup_workflow") + evidence_workflow = _required_mapping(settings, "evidence_workflow") + location_workflow = _required_mapping(settings, "location_workflow") + profile = WorkspaceProfile( + created_with_version=_required_string(record, "created_with_version"), + name=_required_string(record, "name"), + description=_required_string(record, "description"), + default_workspace=_required_string(record, "default_workspace"), + ddi_source=_required_string(settings, "ddi_source"), + command_category=_required_string(command, "category"), + command_preset=_required_string(command, "preset"), + app_workflow=AppWorkflowPreferences( + calculate_app_sizes=_required_boolean(app_workflow, "calculate_app_sizes"), + install_as_developer_package=_required_boolean(app_workflow, "install_as_developer_package"), + ), + backup_workflow=BackupWorkflowPreferences( + force_full_backup=_required_boolean(backup_workflow, "force_full_backup"), + require_encryption=_required_boolean(backup_workflow, "require_encryption"), + ), + evidence_workflow=EvidenceWorkflowPreferences( + capture_duration_seconds=_required_integer(evidence_workflow, "capture_duration_seconds"), + include_syslog=_required_boolean(evidence_workflow, "include_syslog"), + include_oslog=_required_boolean(evidence_workflow, "include_oslog"), + include_pcap=_required_boolean(evidence_workflow, "include_pcap"), + include_screenshot=_required_boolean(evidence_workflow, "include_screenshot"), + include_crash_pull=_required_boolean(evidence_workflow, "include_crash_pull"), + ), + location_workflow=LocationWorkflowPreferences( + timing_randomness_ms=_required_integer(location_workflow, "timing_randomness_ms"), + ignore_timing_delays=_required_boolean(location_workflow, "ignore_timing_delays"), + route_speed_preset_kmh=_required_integer(location_workflow, "route_speed_preset_kmh"), + route_speed_kmh=_required_integer(location_workflow, "route_speed_kmh"), + route_interval_seconds=_required_integer(location_workflow, "route_interval_seconds"), + route_traversals=_required_integer(location_workflow, "route_traversals"), + ), + ) + return validate_workspace_profile(profile) + + +def load_workspace_profile(source: Path) -> WorkspaceProfile: + path = source.expanduser().resolve() + if path.suffix.casefold() != ".json": + raise WorkspaceProfileError(f"Workspace profile must have a .json extension: {path}") + if not path.is_file(): + raise WorkspaceProfileError(f"Workspace profile is not a readable file: {path}") + try: + size = path.stat().st_size + if size > MAX_WORKSPACE_PROFILE_BYTES: + raise WorkspaceProfileError( + f"Workspace profile exceeds {MAX_WORKSPACE_PROFILE_BYTES} bytes: {size}" + ) + payload = json.loads(path.read_text(encoding="utf-8")) + except OSError as error: + raise WorkspaceProfileError(f"Could not read workspace profile at {path}: {error}") from error + except json.JSONDecodeError as error: + raise WorkspaceProfileError(f"Workspace profile is not valid JSON: {error}") from error + if not isinstance(payload, dict): + raise WorkspaceProfileError("Workspace profile root must be a JSON object") + return parse_workspace_profile(payload) + + +def write_workspace_profile(destination: Path, profile: WorkspaceProfile) -> Path: + path = destination.expanduser().resolve() + if path.suffix.casefold() != ".json": + raise WorkspaceProfileError(f"Workspace profile destination must end in .json: {path}") + if not path.parent.is_dir(): + raise WorkspaceProfileError(f"Workspace profile parent directory does not exist: {path.parent}") + content = render_workspace_profile_json(profile).encode("utf-8") + try: + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + except FileExistsError as error: + raise WorkspaceProfileError(f"Refusing to overwrite existing workspace profile: {path}") from error + except OSError as error: + raise WorkspaceProfileError(f"Could not create workspace profile at {path}: {error}") from error + try: + with os.fdopen(descriptor, "wb") as output: + output.write(content) + output.flush() + os.fsync(output.fileno()) + except OSError as error: + path.unlink(missing_ok=True) + raise WorkspaceProfileError(f"Could not write workspace profile at {path}: {error}") from error + return path diff --git a/tests/test_workspace_profile.py b/tests/test_workspace_profile.py new file mode 100644 index 0000000..f550f06 --- /dev/null +++ b/tests/test_workspace_profile.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import json +import os +import shutil +import tempfile +import unittest +from dataclasses import replace +from pathlib import Path + +from ios_developer_toolkit.command_catalog import command_presets +from ios_developer_toolkit.workspace_profile import ( + AppWorkflowPreferences, + BackupWorkflowPreferences, + EvidenceWorkflowPreferences, + LocationWorkflowPreferences, + WorkspaceProfile, + WorkspaceProfileError, + load_workspace_profile, + parse_workspace_profile, + render_workspace_profile_json, + render_workspace_profile_preview, + workspace_profile_mapping, + write_workspace_profile, +) + + +def example_profile() -> WorkspaceProfile: + preset = command_presets()[0] + return WorkspaceProfile( + created_with_version="0.3.4", + name="Release validation", + description="Shared control defaults without targets or paths", + default_workspace="Capability Matrix", + ddi_source="personalized", + command_category=preset.category, + command_preset=preset.identifier, + app_workflow=AppWorkflowPreferences(True, False), + backup_workflow=BackupWorkflowPreferences(True, True), + evidence_workflow=EvidenceWorkflowPreferences(300, True, True, True, False, True), + location_workflow=LocationWorkflowPreferences(100, False, 5, 7, 2, 3), + ) + + +class WorkspaceProfileTests(unittest.TestCase): + def test_round_trips_only_reviewed_non_sensitive_control_defaults(self) -> None: + profile = example_profile() + + payload = workspace_profile_mapping(profile) + parsed = parse_workspace_profile(json.loads(render_workspace_profile_json(profile))) + preview = render_workspace_profile_preview(profile) + + self.assertEqual(parsed, profile) + privacy = payload["privacy"] + self.assertIsInstance(privacy, dict) + self.assertIn("device identity and targets", privacy["schema_excludes"]) + serialized = json.dumps(payload, sort_keys=True) + self.assertNotIn("/Users/", serialized) + self.assertNotIn("PRIVATE-UDID", serialized) + self.assertNotIn("password", serialized.casefold()) + self.assertIn("Importing changes visible controls only", preview) + + def test_parser_requires_known_workspace_preset_and_bounded_values(self) -> None: + profile = example_profile() + with self.assertRaises(WorkspaceProfileError): + workspace_profile_mapping(replace(profile, default_workspace="Unknown")) + with self.assertRaises(WorkspaceProfileError): + workspace_profile_mapping(replace(profile, command_preset="unknown-preset")) + with self.assertRaises(WorkspaceProfileError): + workspace_profile_mapping(replace(profile, description="Stored at /Users/private/team")) + invalid_location = replace(profile.location_workflow, route_traversals=21) + with self.assertRaises(WorkspaceProfileError): + workspace_profile_mapping(replace(profile, location_workflow=invalid_location)) + + def test_parser_ignores_unrelated_extra_fields(self) -> None: + payload = workspace_profile_mapping(example_profile()) + payload["future_top_level"] = "ignored" + settings = payload["settings"] + self.assertIsInstance(settings, dict) + settings["future_setting"] = {"ignored": True} + + parsed = parse_workspace_profile(payload) + + self.assertEqual(parsed, example_profile()) + + def test_private_file_round_trip_refuses_overwrite_and_oversize_input(self) -> None: + temporary_directory = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, temporary_directory) + destination = temporary_directory / "team-profile.json" + + written = write_workspace_profile(destination, example_profile()) + + self.assertEqual(os.stat(written).st_mode & 0o777, 0o600) + self.assertEqual(load_workspace_profile(written), example_profile()) + with self.assertRaises(WorkspaceProfileError): + write_workspace_profile(destination, example_profile()) + oversized = temporary_directory / "oversized.json" + oversized.write_bytes(b" " * 1_048_577) + with self.assertRaises(WorkspaceProfileError): + load_workspace_profile(oversized) + + +if __name__ == "__main__": + unittest.main() From e703c8e53c38cdb4ba08f7f17294b6c0132483a9 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Tue, 22 Sep 2026 05:34:05 -0700 Subject: [PATCH 16/16] Record remaining roadmap boundaries --- docs/PRODUCT_AUDIT_2026-09-21.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/PRODUCT_AUDIT_2026-09-21.md b/docs/PRODUCT_AUDIT_2026-09-21.md index 73b3601..d4f7c9f 100644 --- a/docs/PRODUCT_AUDIT_2026-09-21.md +++ b/docs/PRODUCT_AUDIT_2026-09-21.md @@ -130,8 +130,8 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack * Maintain the opt-in sanitized compatibility export with an exact local preview, owner-only JSON and Markdown files, host/toolchain context, tested device family and build metadata, and no automatic upload. * Maintain local team/workspace profile import-export with a strict versioned schema, exact preview, owner-only export, active-operation guard, and no targets, paths, credentials, coordinates, parameters, case text, or output. -* Notarized Developer ID distribution when an eligible signing identity exists. -* Optional device-lab integration through external services, never a mandatory cloud account. +* **Blocked externally:** notarized Developer ID distribution requires an eligible Apple Developer signing identity, which is not available for this project. The existing release remains explicitly ad-hoc signed and unnotarized. +* **Deferred by design:** no device-lab service or account is in project scope. Add a provider-specific, optional adapter only after a concrete service, authentication model, data boundary, target-selection contract, and test environment are selected; do not add a speculative cloud abstraction. ### Do not build @@ -142,17 +142,11 @@ Upstream contribution candidates are concrete: report the fast-exit scanner pack * A cloud telemetry/sync system for device identifiers, logs, captures, backups, or case records. * A second bundled iOS protocol stack merely for feature-count parity. -## Single best next thing to build +## Audit implementation status -**Reliable startup and lossless device discovery.** This is the right first build because the device picker is a dependency for nearly every existing workspace, there is direct evidence of a released UI/CLI disagreement, and the current eager import makes a non-backup dependency capable of blocking the app before the user can receive diagnostics. It improves both personas: beginners see a usable application and accurate connection state; experts get predictable process results that can later underpin every operation. +The original single best next build—reliable startup and lossless device discovery—is complete. Backup protocol parsing is isolated from desktop startup, terminal discovery output is drained before evaluation, deterministic fast-exit tests exist, the connection diagnostic exposes failure layers without raw identity, and the project pins the validated `pymobiledevice3` 11.15.1 runtime. -## Implementation plan - -1. Extract backup request/event schema validation into a dependency-free `backup_protocol` module. The desktop UI and tests import that module; only the backup worker imports the MobileBackup2 transport implementation. -2. Make `DeviceScanner` consume any remaining stdout/stderr synchronously in its completion handler before evaluating exit status or parsing JSON. -3. Add tests for the backup protocol and a real, short-lived QProcess whose valid JSON is available only after it has exited. -4. Update the README’s troubleshooting and architecture material to explain the connection behavior and the no-sudo boundary. -5. Validate `pymobiledevice3` 11.15.1 in the project environment, then run the full 94-test suite, 90-action headless GUI smoke, CLI discovery, and every command-catalog live-help route. Review the diff before handoff. +The repository-side P0, P1, P2, compatibility-export, and local workspace-profile work is implemented on the audit branch and recorded below. The remaining P3 items are intentionally not represented as unfinished local code: notarization is blocked by the absent signing identity, and device-lab integration is deferred until a specific optional provider and data contract exist. CI and native frozen-artifact checks remain the acceptance authority for each pushed revision. ## Continuous improvement log
    ShortcutAction
    ⌘ RRetry device scan
    ⌘ KOpen the eligible Action Palette
    ⌘ LFocus workspace navigation
    ⌘ FFocus search in Command Center, Man Pages, Installed Apps, or Location Lab
    ⌘ ⌥ ← / ⌘ ⌥ →Previous / next workspace
    ⌘ FFocus search in Command Center, Man Pages, Installed Apps, or Location Lab
    ⌘ ⌥ ← / ⌘ ⌥ →Previous / next workspace
    ⌘ 1–0Home through Evidence Capture
    ⌘ ⇧ EEcosystem Tools
    ⌘ ⇧ MMan Pages
    ⌘ ⇧ SScope & Safety
    ⌘ /Open this reference