From 2e11fb4e463bd6c88bcd7feacc38544c20c928d7 Mon Sep 17 00:00:00 2001 From: Gale W Date: Tue, 16 Jun 2026 12:39:26 -0400 Subject: [PATCH 1/7] validation: clarify helper install validation workflow --- Docs/Architecture/RealAppTestingPlan.md | 4 +- README.md | 8 +++- ROADMAP.md | 4 +- .../validate-installed-automation-helper.sh | 45 +++++++++++++++++-- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/Docs/Architecture/RealAppTestingPlan.md b/Docs/Architecture/RealAppTestingPlan.md index cf6260f..a93c557 100644 --- a/Docs/Architecture/RealAppTestingPlan.md +++ b/Docs/Architecture/RealAppTestingPlan.md @@ -171,9 +171,9 @@ Sandbox and helper direction: Installed-app validation note: -`scripts/validate-installed-automation-helper.sh` is the local installed-app probe. It builds Sirious, copies the app to a stable temporary path, verifies the app and helper signatures, confirms the LaunchAgent plist exists, runs the helper direct CLI, and asks the installed app for `SMAppService.agent(plistName:)` status. With `--register`, it can also request registration, check the XPC `--status` path when the agent is enabled, and unregister afterward. +`scripts/validate-installed-automation-helper.sh` is the local installed-app probe. It builds Sirious, copies the app to a stable temporary path, verifies the app and helper signatures, confirms the LaunchAgent plist exists, runs the helper direct CLI, and asks the installed app for `SMAppService.agent(plistName:)` status. Running the same command with `--keep-installed` keeps the validation copy so later runs update it in place. Running it with `--uninstall` removes the retained validation copy. With `--register`, it can also request registration, check the XPC `--status` path when the agent is enabled, and unregister afterward. -Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from both the repo-local `.build` copy and a `~/Applications/SiriousInstalledAppValidation/Sirious.app` copy. The next helper slice should follow Apple's package-style sample more closely by installing the controller app under an Application Support location through a package or equivalent install step, then rerun the same status/register/XPC checks from that installed path. +Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from both the repo-local `.build` copy and a `~/Applications/SiriousInstalledAppValidation/Sirious.app` copy. The copied-app probe treats this as a successful bundle-shape validation unless registration or `--require-service-found` is requested. The next helper slice should follow Apple's package-style sample more closely by installing the controller app under an Application Support location through a package or equivalent install step, then rerun the same status/register/XPC checks from that installed path. ## Initial Scenario Matrix diff --git a/README.md b/README.md index 981f448..85f95a4 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,13 @@ Run the installed-app helper validation probe with: scripts/validate-installed-automation-helper.sh --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" --keep-installed ``` -The script builds Sirious, installs a temporary app copy, verifies the app and helper signatures, checks the bundled LaunchAgent plist, runs the helper directly, and asks the installed app for `SMAppService.agent(plistName:)` status. Add `--register` only when that status is no longer `notFound`; registration can add a Login Items entry and may require local macOS approval. +The script builds Sirious, installs or updates a validation app copy, verifies the app and helper signatures, checks the bundled LaunchAgent plist, runs the helper directly, and asks the installed app for `SMAppService.agent(plistName:)` status. A copied validation app can still report `notFound`; that means the bundle-shape checks passed, but Service Management registration remains blocked until the package-style install probe changes the status to `notRegistered`, `requiresApproval`, or `enabled`. Add `--register` only when that status is no longer `notFound`; registration can add a Login Items entry and may require local macOS approval. + +Remove a retained validation copy with: + +```sh +scripts/validate-installed-automation-helper.sh --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" --uninstall +``` Install the local SwiftFormat pre-commit hook: diff --git a/ROADMAP.md b/ROADMAP.md index 1bd4be4..c437c4a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -90,7 +90,8 @@ In Progress - [ ] Decide whether a safer or richer now-playing/media-control surface can support exact play, exact pause, stop, and app-specific media behavior. - [ ] Add app-specific default text field focus strategies for predictable compose or search targets. - [x] Add a bundled `SiriousAutomationHelper` LaunchAgent with embedded Info.plist metadata, a launchd `MachServices` entry, and an XPC command channel for Accessibility-owned text insertion. -- [ ] Validate `SiriousAutomationHelper` registration from an installed app bundle outside DerivedData, then document the local install/update/uninstall workflow for helper testing. +- [x] Document the copied-app install/update/uninstall workflow for local `SiriousAutomationHelper` validation. +- [ ] Validate `SiriousAutomationHelper` registration from a package-style installed app outside DerivedData. ### Exit Criteria @@ -222,3 +223,4 @@ Planned - Added the local-only real-app scenario model for gated setup, expectations, cleanup, and artifact reporting. - Added a bundled automation helper LaunchAgent with embedded helper Info.plist metadata, a `MachServices` plist entry, and an XPC command channel for helper-owned Accessibility commands. - Added an installed-app helper validation script and captured that copied app installs still report `.notFound` from Service Management despite valid bundle shape and signatures. +- Documented the local automation-helper validation install/update/uninstall loop while keeping package-style Service Management registration as the next open probe. diff --git a/scripts/validate-installed-automation-helper.sh b/scripts/validate-installed-automation-helper.sh index d450c82..7e9dbd6 100755 --- a/scripts/validate-installed-automation-helper.sh +++ b/scripts/validate-installed-automation-helper.sh @@ -8,8 +8,10 @@ CONFIGURATION=Debug DERIVED_DATA_PATH="$REPO_ROOT/.build/InstalledAppValidation/DerivedData" INSTALL_APP_PATH="$REPO_ROOT/.build/InstalledAppValidation/Sirious.app" RUN_REGISTRATION=0 +REQUIRE_SERVICE_FOUND=0 KEEP_INSTALLED=0 SKIP_BUILD=0 +UNINSTALL_ONLY=0 usage() { cat <<'USAGE' @@ -23,14 +25,21 @@ Options: --derived-data PATH DerivedData path for the validation build. --install-app PATH Destination .app path for the test install. --register Register, XPC-check, and unregister the LaunchAgent. + --require-service-found Fail when Service Management reports notFound. --skip-build Reuse the app already built under the DerivedData path. --keep-installed Leave the copied .app in place after validation. + --uninstall Remove the validation .app at --install-app and exit. -h, --help Show this help. -The default install path stays inside the repository's .build directory. For a -closer local install test, pass a stable user location such as: +The default install path stays inside the repository's .build directory. Passing +--keep-installed keeps the validation copy so the next run updates it in place. +For a closer local install test, pass a stable user location such as: --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" + +Remove a retained validation copy with: + + --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" --uninstall USAGE } @@ -64,6 +73,10 @@ while [ "$#" -gt 0 ]; do RUN_REGISTRATION=1 shift ;; + --require-service-found) + REQUIRE_SERVICE_FOUND=1 + shift + ;; --skip-build) SKIP_BUILD=1 shift @@ -72,6 +85,10 @@ while [ "$#" -gt 0 ]; do KEEP_INSTALLED=1 shift ;; + --uninstall) + UNINSTALL_ONLY=1 + shift + ;; -h|--help) usage exit 0 @@ -87,12 +104,26 @@ INSTALLED_HELPER_PATH="$INSTALL_APP_PATH/Contents/Resources/SiriousAutomationHel INSTALLED_AGENT_PLIST="$INSTALL_APP_PATH/Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist" INSTALLED_APP_EXECUTABLE="$INSTALL_APP_PATH/Contents/MacOS/Sirious" +remove_installed_app() { + if [ -e "$INSTALL_APP_PATH" ]; then + log "Removing validation app copy at $INSTALL_APP_PATH." + rm -rf "$INSTALL_APP_PATH" + else + log "No validation app copy exists at $INSTALL_APP_PATH." + fi +} + cleanup() { if [ "$KEEP_INSTALLED" -eq 0 ]; then - rm -rf "$INSTALL_APP_PATH" + remove_installed_app fi } +if [ "$UNINSTALL_ONLY" -eq 1 ]; then + remove_installed_app + exit 0 +fi + trap cleanup EXIT cd "$REPO_ROOT" @@ -136,7 +167,13 @@ printf '%s\n' "$STATUS_OUTPUT" case "$STATUS_OUTPUT" in *notFound*) - fail "ServiceManagement still reports the automation helper as notFound from the installed app at $INSTALL_APP_PATH." + if [ "$RUN_REGISTRATION" -eq 1 ] || [ "$REQUIRE_SERVICE_FOUND" -eq 1 ]; then + fail "ServiceManagement reports the automation helper as notFound from the installed app at $INSTALL_APP_PATH, so registration and XPC validation cannot continue. This usually means macOS does not recognize the copied validation app as an installed Service Management container; run the next package-style install probe before treating the helper plist or executable layout as broken." + fi + + log "ServiceManagement reports the automation helper as notFound from the installed app at $INSTALL_APP_PATH." + log "Bundle-shape validation passed, but registration remains blocked until the package-style install probe moves this status to notRegistered, requiresApproval, or enabled." + exit 0 ;; esac From 947a3f2abc0e70bf9cb78d021fca0ce9b6ad6c0b Mon Sep 17 00:00:00 2001 From: Gale W Date: Tue, 16 Jun 2026 19:07:53 -0400 Subject: [PATCH 2/7] xpc: harden automation helper peer validation --- .../App/AutomationHelperCommandRunner.swift | 17 +++++++++--- Sources/App/AutomationHelperDiagnostics.swift | 17 +++++++++--- Sources/AutomationHelper/main.swift | 5 ++-- .../AutomationHelperXPC.swift | 25 +++++++++++++++++ .../AutomationHelperCommandRunnerTests.swift | 27 +++++++++++++++++++ 5 files changed, 81 insertions(+), 10 deletions(-) diff --git a/Sources/App/AutomationHelperCommandRunner.swift b/Sources/App/AutomationHelperCommandRunner.swift index 89e3588..b3c1cd7 100644 --- a/Sources/App/AutomationHelperCommandRunner.swift +++ b/Sources/App/AutomationHelperCommandRunner.swift @@ -115,6 +115,7 @@ struct LaunchAgentAutomationHelperCommandRunner: AutomationHelperCommandRunning options: [] ) connection.remoteObjectInterface = NSXPCInterface(with: AutomationHelperXPCProtocol.self) + connection.setCodeSigningRequirement(AutomationHelperXPC.helperCodeSigningRequirement) connection.invalidationHandler = { completion.finishWithConnectionError( "Sirious lost its XPC connection to the automation helper before the helper returned a response." @@ -125,11 +126,13 @@ struct LaunchAgentAutomationHelperCommandRunner: AutomationHelperCommandRunning "Sirious had its XPC connection to the automation helper interrupted before the helper returned a response." ) } - connection.resume() let proxy = connection.remoteObjectProxyWithErrorHandler { error in - completion.finishWithConnectionError( - "Sirious could not connect to the automation helper XPC service named \(AutomationHelperXPC.machServiceName). macOS reported: \(error.localizedDescription)" + completion.finishWithConnectionErrorMessage( + AutomationHelperXPC.connectionErrorMessage( + for: error, + commandArguments: command.arguments + ) ) connection.invalidate() } @@ -142,6 +145,8 @@ struct LaunchAgentAutomationHelperCommandRunner: AutomationHelperCommandRunning return } + connection.activate() + helper.runCommand(command.arguments) { reply in completion.finish(with: reply) connection.invalidate() @@ -169,10 +174,14 @@ private final class AutomationHelperXPCCommandCompletion: @unchecked Sendable { } func finishWithConnectionError(_ message: String) { + finishWithConnectionErrorMessage("\(message) Command: \(command.arguments.joined(separator: " ")).") + } + + func finishWithConnectionErrorMessage(_ message: String) { finish(AutomationHelperCommandResult( terminationStatus: 126, standardOutput: "", - standardError: "\(message) Command: \(command.arguments.joined(separator: " "))." + standardError: message )) } diff --git a/Sources/App/AutomationHelperDiagnostics.swift b/Sources/App/AutomationHelperDiagnostics.swift index e2e65fe..9a764d5 100644 --- a/Sources/App/AutomationHelperDiagnostics.swift +++ b/Sources/App/AutomationHelperDiagnostics.swift @@ -94,6 +94,7 @@ private enum AutomationHelperXPCDiagnostics { ) connection.remoteObjectInterface = NSXPCInterface(with: AutomationHelperXPCProtocol.self) + connection.setCodeSigningRequirement(AutomationHelperXPC.helperCodeSigningRequirement) connection.invalidationHandler = { completion.finishWithConnectionError( "Sirious lost its XPC connection to the automation helper before the helper returned a response." @@ -104,11 +105,13 @@ private enum AutomationHelperXPCDiagnostics { "Sirious had its XPC connection to the automation helper interrupted before the helper returned a response." ) } - connection.resume() let proxy = connection.remoteObjectProxyWithErrorHandler { error in - completion.finishWithConnectionError( - "Sirious could not connect to the automation helper XPC service named \(AutomationHelperXPC.machServiceName). macOS reported: \(error.localizedDescription)" + completion.finishWithConnectionErrorMessage( + AutomationHelperXPC.connectionErrorMessage( + for: error, + commandArguments: arguments + ) ) connection.invalidate() } @@ -122,6 +125,8 @@ private enum AutomationHelperXPCDiagnostics { ) } + connection.activate() + helper.runCommand(arguments) { reply in completion.finish(with: reply) connection.invalidate() @@ -146,10 +151,14 @@ private final class AutomationHelperXPCDiagnosticCompletion: @unchecked Sendable } func finishWithConnectionError(_ message: String) { + finishWithConnectionErrorMessage("\(message) Command: \(arguments.joined(separator: " ")).") + } + + func finishWithConnectionErrorMessage(_ message: String) { finish(AutomationHelperCommandResult( terminationStatus: 126, standardOutput: "", - standardError: "\(message) Command: \(arguments.joined(separator: " "))." + standardError: message )) } diff --git a/Sources/AutomationHelper/main.swift b/Sources/AutomationHelper/main.swift index 1a91edb..e243a25 100644 --- a/Sources/AutomationHelper/main.swift +++ b/Sources/AutomationHelper/main.swift @@ -27,9 +27,10 @@ private final class AutomationHelperXPCListenerDelegate: NSObject, NSXPCListener private let service = AutomationHelperXPCService() func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool { + connection.setCodeSigningRequirement(AutomationHelperXPC.appCodeSigningRequirement) connection.exportedInterface = NSXPCInterface(with: AutomationHelperXPCProtocol.self) connection.exportedObject = service - connection.resume() + connection.activate() return true } @@ -53,7 +54,7 @@ private func runXPCService() { let delegate = AutomationHelperXPCListenerDelegate() listener.delegate = delegate - listener.resume() + listener.activate() RunLoop.main.run() } diff --git a/Sources/AutomationHelperSupport/AutomationHelperXPC.swift b/Sources/AutomationHelperSupport/AutomationHelperXPC.swift index 06444fe..ab6da73 100644 --- a/Sources/AutomationHelperSupport/AutomationHelperXPC.swift +++ b/Sources/AutomationHelperSupport/AutomationHelperXPC.swift @@ -1,11 +1,36 @@ import Foundation enum AutomationHelperXPC { + static let signingTeamIdentifier = "BC73766F69" + static let appBundleIdentifier = "com.galewilliams.Sirious" + static let helperBundleIdentifier = "com.galewilliams.Sirious.AutomationHelper" static let launchAgentPlistName = "com.galewilliams.Sirious.AutomationHelper.plist" static let machServiceName = "com.galewilliams.Sirious.AutomationHelper" static let terminationStatusKey = "terminationStatus" static let standardOutputKey = "standardOutput" static let standardErrorKey = "standardError" + static let appCodeSigningRequirement = codeSigningRequirement(bundleIdentifier: appBundleIdentifier) + static let helperCodeSigningRequirement = codeSigningRequirement(bundleIdentifier: helperBundleIdentifier) + + static func connectionErrorMessage( + for error: any Error, + commandArguments: [String] + ) -> String { + let error = error as NSError + let command = commandArguments.joined(separator: " ") + + if error.domain == NSCocoaErrorDomain, + error.code == NSXPCConnectionCodeSigningRequirementFailure + { + return "Sirious rejected the automation helper XPC peer because its code signature did not satisfy the required signing identity. Required helper identifier: \(helperBundleIdentifier). Required team identifier: \(signingTeamIdentifier). macOS reported: \(error.localizedDescription). Command: \(command)." + } + + return "Sirious could not connect to the automation helper XPC service named \(machServiceName). macOS reported: \(error.localizedDescription). Command: \(command)." + } + + private static func codeSigningRequirement(bundleIdentifier: String) -> String { + #"anchor apple generic and certificate leaf[subject.OU] = "\#(signingTeamIdentifier)" and identifier "\#(bundleIdentifier)""# + } } @objc diff --git a/Tests/SiriousTests/AutomationHelperCommandRunnerTests.swift b/Tests/SiriousTests/AutomationHelperCommandRunnerTests.swift index f6a7479..acba4a1 100644 --- a/Tests/SiriousTests/AutomationHelperCommandRunnerTests.swift +++ b/Tests/SiriousTests/AutomationHelperCommandRunnerTests.swift @@ -4,6 +4,33 @@ import Testing @MainActor struct AutomationHelperCommandRunnerTests { + @Test("automation helper XPC signing requirements describe app and helper peers") + func automationHelperXPCSigningRequirementsDescribeAppAndHelperPeers() { + #expect(AutomationHelperXPC.signingTeamIdentifier == "BC73766F69") + #expect(AutomationHelperXPC.appBundleIdentifier == "com.galewilliams.Sirious") + #expect(AutomationHelperXPC.helperBundleIdentifier == "com.galewilliams.Sirious.AutomationHelper") + #expect(AutomationHelperXPC.appCodeSigningRequirement.contains(#"certificate leaf[subject.OU] = "BC73766F69""#)) + #expect(AutomationHelperXPC.appCodeSigningRequirement.contains(#"identifier "com.galewilliams.Sirious""#)) + #expect(AutomationHelperXPC.helperCodeSigningRequirement.contains(#"certificate leaf[subject.OU] = "BC73766F69""#)) + #expect(AutomationHelperXPC.helperCodeSigningRequirement.contains(#"identifier "com.galewilliams.Sirious.AutomationHelper""#)) + } + + @Test("automation helper XPC signing failures get a specific diagnostic") + func automationHelperXPCSigningFailuresGetSpecificDiagnostic() { + let message = AutomationHelperXPC.connectionErrorMessage( + for: NSError( + domain: NSCocoaErrorDomain, + code: NSXPCConnectionCodeSigningRequirementFailure + ), + commandArguments: AutomationHelperCommand.status.arguments + ) + + #expect(message.contains("code signature did not satisfy") == true) + #expect(message.contains("com.galewilliams.Sirious.AutomationHelper") == true) + #expect(message.contains("BC73766F69") == true) + #expect(message.contains("--status") == true) + } + @Test("automation helper command arguments match helper CLI") func automationHelperCommandArgumentsMatchHelperCLI() { #expect(AutomationHelperCommand.status.arguments == ["--status"]) From 0d792da865bcc2a3a93231b4237b9b5db0f3f484 Mon Sep 17 00:00:00 2001 From: Gale W Date: Wed, 17 Jun 2026 01:45:12 -0400 Subject: [PATCH 3/7] helper: add package-style validation probe --- Docs/Architecture/RealAppTestingPlan.md | 4 +- README.md | 10 +- ROADMAP.md | 8 +- .../validate-installed-automation-helper.sh | 126 +++++++++++++++++- 4 files changed, 136 insertions(+), 12 deletions(-) diff --git a/Docs/Architecture/RealAppTestingPlan.md b/Docs/Architecture/RealAppTestingPlan.md index a93c557..bfea391 100644 --- a/Docs/Architecture/RealAppTestingPlan.md +++ b/Docs/Architecture/RealAppTestingPlan.md @@ -171,9 +171,9 @@ Sandbox and helper direction: Installed-app validation note: -`scripts/validate-installed-automation-helper.sh` is the local installed-app probe. It builds Sirious, copies the app to a stable temporary path, verifies the app and helper signatures, confirms the LaunchAgent plist exists, runs the helper direct CLI, and asks the installed app for `SMAppService.agent(plistName:)` status. Running the same command with `--keep-installed` keeps the validation copy so later runs update it in place. Running it with `--uninstall` removes the retained validation copy. With `--register`, it can also request registration, check the XPC `--status` path when the agent is enabled, and unregister afterward. +`scripts/validate-installed-automation-helper.sh` is the local installed-app probe. It builds Sirious, installs the app into a validation location, verifies the app and helper signatures, confirms the LaunchAgent plist exists, runs the helper direct CLI, and asks the installed app for `SMAppService.agent(plistName:)` status. By default it copies the app directly. With `--package-style`, it stages the app under `Library/Application Support/SiriousInstalledAppValidation`, builds a local macOS Installer package with bundle relocation disabled, installs that package into `CurrentUserHomeDirectory`, and then runs the same validation checks. Running either path with `--keep-installed` keeps the validation app so later runs update it in place. Running it with `--uninstall` removes the retained validation copy. With `--register`, it can also request registration, check the XPC `--status` path when the agent is enabled, and unregister afterward. -Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from both the repo-local `.build` copy and a `~/Applications/SiriousInstalledAppValidation/Sirious.app` copy. The copied-app probe treats this as a successful bundle-shape validation unless registration or `--require-service-found` is requested. The next helper slice should follow Apple's package-style sample more closely by installing the controller app under an Application Support location through a package or equivalent install step, then rerun the same status/register/XPC checks from that installed path. +Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from the repo-local `.build` copy, a `~/Applications/SiriousInstalledAppValidation/Sirious.app` copy, and a package-installed `~/Library/Application Support/SiriousInstalledAppValidation/Sirious.app` copy. The copied-app and package-style probes treat this as successful bundle-shape validation unless registration or `--require-service-found` is requested. The next helper slice should investigate the remaining app identity or installation condition that Service Management needs before it recognizes the bundled LaunchAgent. ## Initial Scenario Matrix diff --git a/README.md b/README.md index 85f95a4..ce5dea3 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,15 @@ Run the installed-app helper validation probe with: scripts/validate-installed-automation-helper.sh --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" --keep-installed ``` -The script builds Sirious, installs or updates a validation app copy, verifies the app and helper signatures, checks the bundled LaunchAgent plist, runs the helper directly, and asks the installed app for `SMAppService.agent(plistName:)` status. A copied validation app can still report `notFound`; that means the bundle-shape checks passed, but Service Management registration remains blocked until the package-style install probe changes the status to `notRegistered`, `requiresApproval`, or `enabled`. Add `--register` only when that status is no longer `notFound`; registration can add a Login Items entry and may require local macOS approval. +The script builds Sirious, installs or updates a validation app copy, verifies the app and helper signatures, checks the bundled LaunchAgent plist, runs the helper directly, and asks the installed app for `SMAppService.agent(plistName:)` status. A copied validation app can still report `notFound`; that means the bundle-shape checks passed, but Service Management registration remains blocked. + +Run the package-style helper validation probe with: + +```sh +scripts/validate-installed-automation-helper.sh --package-style --keep-installed +``` + +The package-style probe installs the validation app through a local macOS Installer package into the current user's Application Support directory before checking the same helper shape and Service Management status. If either probe still reports `notFound`, do not add `--register`; registration can add a Login Items entry and may require local macOS approval. Add `--register` only when status moves to `notRegistered`, `requiresApproval`, or `enabled`. Remove a retained validation copy with: diff --git a/ROADMAP.md b/ROADMAP.md index c437c4a..70b2f73 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -91,7 +91,8 @@ In Progress - [ ] Add app-specific default text field focus strategies for predictable compose or search targets. - [x] Add a bundled `SiriousAutomationHelper` LaunchAgent with embedded Info.plist metadata, a launchd `MachServices` entry, and an XPC command channel for Accessibility-owned text insertion. - [x] Document the copied-app install/update/uninstall workflow for local `SiriousAutomationHelper` validation. -- [ ] Validate `SiriousAutomationHelper` registration from a package-style installed app outside DerivedData. +- [x] Validate `SiriousAutomationHelper` status from a package-style installed app outside DerivedData. +- [ ] Determine what remaining app identity or installation condition keeps `SMAppService.agent(plistName:)` at `.notFound` after copied-app and package-style validation both pass bundle-shape checks. ### Exit Criteria @@ -185,7 +186,8 @@ Planned - [ ] Add supervised routed-audio scenarios that play generated command audio through a virtual microphone into Sirious. - [ ] Add Computer Use setup, observation, and recovery notes for scenarios where Accessibility or app automation leaves a real gap. - [x] Add an installed-app helper validation slice that copies Sirious into a stable local app location before checking bundle shape, signatures, direct helper status, and `SMAppService.agent(plistName:)` status. -- [ ] Add a package-style installed-helper probe to determine whether Apple's Application Support install shape moves Service Management out of `.notFound` before registration, XPC connection, and helper Accessibility prompting. +- [x] Add a package-style installed-helper probe to determine whether Apple's Application Support install shape moves Service Management out of `.notFound` before registration, XPC connection, and helper Accessibility prompting. +- [ ] Investigate why Service Management still reports `.notFound` after package-style Application Support installation verifies the app signature, helper signature, LaunchAgent plist, and direct helper status. - [ ] Decide which scenarios belong in a local `.xctestplan`, which should be manifest-gated, and which should remain manual supervised checks. ### Exit Criteria @@ -223,4 +225,4 @@ Planned - Added the local-only real-app scenario model for gated setup, expectations, cleanup, and artifact reporting. - Added a bundled automation helper LaunchAgent with embedded helper Info.plist metadata, a `MachServices` plist entry, and an XPC command channel for helper-owned Accessibility commands. - Added an installed-app helper validation script and captured that copied app installs still report `.notFound` from Service Management despite valid bundle shape and signatures. -- Documented the local automation-helper validation install/update/uninstall loop while keeping package-style Service Management registration as the next open probe. +- Added a package-style validation path through a local Installer package and captured that user Application Support package installs still report `.notFound` from Service Management despite valid bundle shape and signatures. diff --git a/scripts/validate-installed-automation-helper.sh b/scripts/validate-installed-automation-helper.sh index 7e9dbd6..7bbdeed 100755 --- a/scripts/validate-installed-automation-helper.sh +++ b/scripts/validate-installed-automation-helper.sh @@ -7,6 +7,13 @@ REPO_ROOT=$(CDPATH= cd -- "$SELF_DIR/.." && pwd) CONFIGURATION=Debug DERIVED_DATA_PATH="$REPO_ROOT/.build/InstalledAppValidation/DerivedData" INSTALL_APP_PATH="$REPO_ROOT/.build/InstalledAppValidation/Sirious.app" +INSTALL_APP_PATH_WAS_SET=0 +PACKAGE_PATH="$REPO_ROOT/.build/InstalledAppValidation/SiriousInstalledAppValidation.pkg" +PACKAGE_ROOT="$REPO_ROOT/.build/InstalledAppValidation/PackageRoot" +PACKAGE_COMPONENT_PLIST="$REPO_ROOT/.build/InstalledAppValidation/SiriousInstalledAppValidation-components.plist" +PACKAGE_IDENTIFIER="com.galewilliams.Sirious.InstalledAppValidation" +PACKAGE_VERSION=1 +PACKAGE_STYLE=0 RUN_REGISTRATION=0 REQUIRE_SERVICE_FOUND=0 KEEP_INSTALLED=0 @@ -24,6 +31,11 @@ Options: --configuration NAME Xcode configuration to build. Default: Debug. --derived-data PATH DerivedData path for the validation build. --install-app PATH Destination .app path for the test install. + --package-style Install the validation app through a local .pkg first. + --package-path PATH Local package output path. Default: repo .build path. + --package-root PATH Local package staging root. Default: repo .build path. + --package-components PATH + Local component plist path. Default: repo .build path. --register Register, XPC-check, and unregister the LaunchAgent. --require-service-found Fail when Service Management reports notFound. --skip-build Reuse the app already built under the DerivedData path. @@ -37,6 +49,11 @@ For a closer local install test, pass a stable user location such as: --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" +For a package-style probe, install the validation app through macOS Installer +into a user Application Support location before checking Service Management: + + --package-style --keep-installed + Remove a retained validation copy with: --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" --uninstall @@ -67,6 +84,26 @@ while [ "$#" -gt 0 ]; do --install-app) [ "$#" -ge 2 ] || fail "--install-app requires a value." INSTALL_APP_PATH=$2 + INSTALL_APP_PATH_WAS_SET=1 + shift 2 + ;; + --package-style) + PACKAGE_STYLE=1 + shift + ;; + --package-path) + [ "$#" -ge 2 ] || fail "--package-path requires a value." + PACKAGE_PATH=$2 + shift 2 + ;; + --package-root) + [ "$#" -ge 2 ] || fail "--package-root requires a value." + PACKAGE_ROOT=$2 + shift 2 + ;; + --package-components) + [ "$#" -ge 2 ] || fail "--package-components requires a value." + PACKAGE_COMPONENT_PLIST=$2 shift 2 ;; --register) @@ -99,6 +136,22 @@ while [ "$#" -gt 0 ]; do esac done +if [ "$PACKAGE_STYLE" -eq 1 ] && [ "$INSTALL_APP_PATH_WAS_SET" -eq 0 ]; then + INSTALL_APP_PATH="$HOME/Library/Application Support/SiriousInstalledAppValidation/Sirious.app" +fi + +PACKAGE_PAYLOAD_APP_PATH= +if [ "$PACKAGE_STYLE" -eq 1 ]; then + case "$INSTALL_APP_PATH" in + "$HOME"/*) + PACKAGE_PAYLOAD_APP_PATH=${INSTALL_APP_PATH#"$HOME"/} + ;; + *) + fail "--package-style requires --install-app to be under $HOME so installer -target CurrentUserHomeDirectory writes only to the current user's domain." + ;; + esac +fi + BUILT_APP_PATH="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION/Sirious.app" INSTALLED_HELPER_PATH="$INSTALL_APP_PATH/Contents/Resources/SiriousAutomationHelper" INSTALLED_AGENT_PLIST="$INSTALL_APP_PATH/Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist" @@ -119,6 +172,58 @@ cleanup() { fi } +install_validation_copy() { + log "Installing validation copy at $INSTALL_APP_PATH." + mkdir -p "$(dirname -- "$INSTALL_APP_PATH")" + rm -rf "$INSTALL_APP_PATH" + cp -R "$BUILT_APP_PATH" "$INSTALL_APP_PATH" +} + +install_validation_package() { + log "Staging validation package root at $PACKAGE_ROOT." + rm -rf "$PACKAGE_ROOT" + mkdir -p "$(dirname -- "$PACKAGE_ROOT/$PACKAGE_PAYLOAD_APP_PATH")" + cp -R "$BUILT_APP_PATH" "$PACKAGE_ROOT/$PACKAGE_PAYLOAD_APP_PATH" + + log "Writing validation package component plist at $PACKAGE_COMPONENT_PLIST." + mkdir -p "$(dirname -- "$PACKAGE_COMPONENT_PLIST")" + cat > "$PACKAGE_COMPONENT_PLIST" < + + + + + RootRelativeBundlePath + $PACKAGE_PAYLOAD_APP_PATH + BundleIsRelocatable + + BundleIsVersionChecked + + BundleHasStrictIdentifier + + BundleOverwriteAction + upgrade + + + +PLIST + + log "Building validation package at $PACKAGE_PATH." + mkdir -p "$(dirname -- "$PACKAGE_PATH")" + rm -f "$PACKAGE_PATH" + pkgbuild \ + --root "$PACKAGE_ROOT" \ + --component-plist "$PACKAGE_COMPONENT_PLIST" \ + --install-location "/" \ + --identifier "$PACKAGE_IDENTIFIER" \ + --version "$PACKAGE_VERSION" \ + "$PACKAGE_PATH" + + log "Installing validation package into $HOME." + rm -rf "$INSTALL_APP_PATH" + installer -pkg "$PACKAGE_PATH" -target CurrentUserHomeDirectory +} + if [ "$UNINSTALL_ONLY" -eq 1 ]; then remove_installed_app exit 0 @@ -140,10 +245,11 @@ fi [ -d "$BUILT_APP_PATH" ] || fail "Expected built app at $BUILT_APP_PATH, but it does not exist." -log "Installing validation copy at $INSTALL_APP_PATH." -mkdir -p "$(dirname -- "$INSTALL_APP_PATH")" -rm -rf "$INSTALL_APP_PATH" -cp -R "$BUILT_APP_PATH" "$INSTALL_APP_PATH" +if [ "$PACKAGE_STYLE" -eq 1 ]; then + install_validation_package +else + install_validation_copy +fi [ -x "$INSTALLED_APP_EXECUTABLE" ] || fail "Installed app executable is missing or not executable at $INSTALLED_APP_EXECUTABLE." [ -x "$INSTALLED_HELPER_PATH" ] || fail "Installed helper executable is missing or not executable at $INSTALLED_HELPER_PATH." @@ -168,11 +274,19 @@ printf '%s\n' "$STATUS_OUTPUT" case "$STATUS_OUTPUT" in *notFound*) if [ "$RUN_REGISTRATION" -eq 1 ] || [ "$REQUIRE_SERVICE_FOUND" -eq 1 ]; then - fail "ServiceManagement reports the automation helper as notFound from the installed app at $INSTALL_APP_PATH, so registration and XPC validation cannot continue. This usually means macOS does not recognize the copied validation app as an installed Service Management container; run the next package-style install probe before treating the helper plist or executable layout as broken." + if [ "$PACKAGE_STYLE" -eq 1 ]; then + fail "ServiceManagement reports the automation helper as notFound from the package-installed app at $INSTALL_APP_PATH, so registration and XPC validation cannot continue. The package-style install verified the helper bundle shape, but macOS still does not recognize this app as a Service Management automation helper container." + fi + + fail "ServiceManagement reports the automation helper as notFound from the installed app at $INSTALL_APP_PATH, so registration and XPC validation cannot continue. This usually means macOS does not recognize the copied validation app as an installed Service Management container; use --package-style to compare against the Installer-based Application Support probe before treating the helper plist or executable layout as broken." fi log "ServiceManagement reports the automation helper as notFound from the installed app at $INSTALL_APP_PATH." - log "Bundle-shape validation passed, but registration remains blocked until the package-style install probe moves this status to notRegistered, requiresApproval, or enabled." + if [ "$PACKAGE_STYLE" -eq 1 ]; then + log "Package-style install validation passed, but Service Management still does not recognize this app bundle as an automation helper container." + else + log "Bundle-shape validation passed, but registration remains blocked while Service Management reports notFound. Use --package-style to compare against the Installer-based Application Support probe before treating the helper plist or executable layout as broken." + fi exit 0 ;; esac From 69f12fc03ebc44fdc4b302f5bb01162fe797871b Mon Sep 17 00:00:00 2001 From: Gale W Date: Wed, 17 Jun 2026 14:56:12 -0400 Subject: [PATCH 4/7] helper: move automation helper into HelperTools --- Docs/Architecture/RealAppTestingPlan.md | 4 ++-- ROADMAP.md | 4 ++-- Sirious.xcodeproj/project.pbxproj | 4 ++-- .../Support/com.galewilliams.Sirious.AutomationHelper.plist | 2 +- Tests/SiriousTests/AutomationHelperAgentStateTests.swift | 2 +- project.yml | 3 ++- scripts/validate-installed-automation-helper.sh | 2 +- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Docs/Architecture/RealAppTestingPlan.md b/Docs/Architecture/RealAppTestingPlan.md index bfea391..3deb539 100644 --- a/Docs/Architecture/RealAppTestingPlan.md +++ b/Docs/Architecture/RealAppTestingPlan.md @@ -165,7 +165,7 @@ Sandbox and helper direction: - Keep the main app sandboxed for ordinary app behavior. Put assistive automation behind a separate helper boundary so the permissioned process is narrow and operator-visible. - Prefer a bundled LaunchAgent registered with `SMAppService.agent(plistName:)` before considering a LaunchDaemon, because these automation flows run in the logged-in user session and need user-facing Accessibility context. The first checked-in helper is `SiriousAutomationHelper`, a hardened-runtime command-line tool copied into the app bundle with its LaunchAgent plist at `Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist`. - `SiriousAutomationHelper` now embeds generated Info.plist metadata into the executable with `CREATE_INFOPLIST_SECTION_IN_BINARY` and `GENERATE_INFOPLIST_FILE`, keeping the helper identity visible to codesigning and Service Management. -- The helper LaunchAgent plist declares `BundleProgram` as `Contents/Resources/SiriousAutomationHelper` and advertises `MachServices` for `com.galewilliams.Sirious.AutomationHelper`. +- The helper LaunchAgent plist declares `BundleProgram` as `Contents/Library/HelperTools/SiriousAutomationHelper` and advertises `MachServices` for `com.galewilliams.Sirious.AutomationHelper`. - Use the XPC command channel as the normal app-to-helper path. The app sends command arguments to the helper's Mach service, and the helper returns termination status plus output/error strings over a small shared protocol. The helper still accepts direct CLI commands such as `--status` for diagnostics. - Gale's local Apple signing team identifier is `BC73766F69`. Some certificate common names include `AMRC3N39SQ`, but the certificate subject's organizational unit is the Team ID Xcode uses for `DEVELOPMENT_TEAM`. Keep `DEVELOPMENT_TEAM` aligned with `BC73766F69` in `project.yml` so macOS TCC can associate prompts with a stable development identity during real-app testing. @@ -173,7 +173,7 @@ Installed-app validation note: `scripts/validate-installed-automation-helper.sh` is the local installed-app probe. It builds Sirious, installs the app into a validation location, verifies the app and helper signatures, confirms the LaunchAgent plist exists, runs the helper direct CLI, and asks the installed app for `SMAppService.agent(plistName:)` status. By default it copies the app directly. With `--package-style`, it stages the app under `Library/Application Support/SiriousInstalledAppValidation`, builds a local macOS Installer package with bundle relocation disabled, installs that package into `CurrentUserHomeDirectory`, and then runs the same validation checks. Running either path with `--keep-installed` keeps the validation app so later runs update it in place. Running it with `--uninstall` removes the retained validation copy. With `--register`, it can also request registration, check the XPC `--status` path when the agent is enabled, and unregister afterward. -Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from the repo-local `.build` copy, a `~/Applications/SiriousInstalledAppValidation/Sirious.app` copy, and a package-installed `~/Library/Application Support/SiriousInstalledAppValidation/Sirious.app` copy. The copied-app and package-style probes treat this as successful bundle-shape validation unless registration or `--require-service-found` is requested. The next helper slice should investigate the remaining app identity or installation condition that Service Management needs before it recognizes the bundled LaunchAgent. +Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, the helper executable is present at `Contents/Library/HelperTools`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from the repo-local `.build` copy and a package-installed `~/Library/Application Support/SiriousInstalledAppValidation/Sirious.app` copy. The copied-app and package-style probes treat this as successful bundle-shape validation unless registration or `--require-service-found` is requested. The next helper slice should investigate the remaining app identity, entitlement, or installation condition that Service Management needs before it recognizes the bundled LaunchAgent. ## Initial Scenario Matrix diff --git a/ROADMAP.md b/ROADMAP.md index 70b2f73..8c40f57 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -92,7 +92,7 @@ In Progress - [x] Add a bundled `SiriousAutomationHelper` LaunchAgent with embedded Info.plist metadata, a launchd `MachServices` entry, and an XPC command channel for Accessibility-owned text insertion. - [x] Document the copied-app install/update/uninstall workflow for local `SiriousAutomationHelper` validation. - [x] Validate `SiriousAutomationHelper` status from a package-style installed app outside DerivedData. -- [ ] Determine what remaining app identity or installation condition keeps `SMAppService.agent(plistName:)` at `.notFound` after copied-app and package-style validation both pass bundle-shape checks. +- [ ] Determine what remaining app identity, entitlement, or installation condition keeps `SMAppService.agent(plistName:)` at `.notFound` after HelperTools copied-app and package-style validation both pass bundle-shape checks. ### Exit Criteria @@ -187,7 +187,7 @@ Planned - [ ] Add Computer Use setup, observation, and recovery notes for scenarios where Accessibility or app automation leaves a real gap. - [x] Add an installed-app helper validation slice that copies Sirious into a stable local app location before checking bundle shape, signatures, direct helper status, and `SMAppService.agent(plistName:)` status. - [x] Add a package-style installed-helper probe to determine whether Apple's Application Support install shape moves Service Management out of `.notFound` before registration, XPC connection, and helper Accessibility prompting. -- [ ] Investigate why Service Management still reports `.notFound` after package-style Application Support installation verifies the app signature, helper signature, LaunchAgent plist, and direct helper status. +- [ ] Investigate why Service Management still reports `.notFound` after package-style Application Support installation verifies the app signature, HelperTools helper signature, LaunchAgent plist, and direct helper status. - [ ] Decide which scenarios belong in a local `.xctestplan`, which should be manifest-gated, and which should remain manual supervised checks. ### Exit Criteria diff --git a/Sirious.xcodeproj/project.pbxproj b/Sirious.xcodeproj/project.pbxproj index 38b7f69..6b2ad64 100644 --- a/Sirious.xcodeproj/project.pbxproj +++ b/Sirious.xcodeproj/project.pbxproj @@ -170,8 +170,8 @@ 9AB0A85719BD262239B4AA20 /* Embed Dependencies */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 7; + dstPath = Contents/Library/HelperTools; + dstSubfolderSpec = 1; files = ( 8315DA30FD65517F9BC0D94C /* SiriousAutomationHelper in Embed Dependencies */, ); diff --git a/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist b/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist index 64af66c..d311652 100644 --- a/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist +++ b/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist @@ -7,7 +7,7 @@ com.galewilliams.Sirious BundleProgram - Contents/Resources/SiriousAutomationHelper + Contents/Library/HelperTools/SiriousAutomationHelper Label com.galewilliams.Sirious.AutomationHelper MachServices diff --git a/Tests/SiriousTests/AutomationHelperAgentStateTests.swift b/Tests/SiriousTests/AutomationHelperAgentStateTests.swift index 9070a2f..4eb9dd0 100644 --- a/Tests/SiriousTests/AutomationHelperAgentStateTests.swift +++ b/Tests/SiriousTests/AutomationHelperAgentStateTests.swift @@ -18,7 +18,7 @@ struct AutomationHelperAgentStateTests { let plist = try #require(PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]) #expect(plist["Label"] as? String == "com.galewilliams.Sirious.AutomationHelper") - #expect(plist["BundleProgram"] as? String == "Contents/Resources/SiriousAutomationHelper") + #expect(plist["BundleProgram"] as? String == "Contents/Library/HelperTools/SiriousAutomationHelper") #expect(plist["AssociatedBundleIdentifiers"] as? [String] == ["com.galewilliams.Sirious"]) #expect(plist["MachServices"] as? [String: Bool] == [ "com.galewilliams.Sirious.AutomationHelper": true, diff --git a/project.yml b/project.yml index 9ee2cad..44ea410 100644 --- a/project.yml +++ b/project.yml @@ -44,7 +44,8 @@ targets: - target: SiriousAutomationHelper embed: true copy: - destination: resources + destination: wrapper + subpath: Contents/Library/HelperTools info: path: Sources/Support/Info.plist properties: diff --git a/scripts/validate-installed-automation-helper.sh b/scripts/validate-installed-automation-helper.sh index 7bbdeed..cdad472 100755 --- a/scripts/validate-installed-automation-helper.sh +++ b/scripts/validate-installed-automation-helper.sh @@ -153,7 +153,7 @@ if [ "$PACKAGE_STYLE" -eq 1 ]; then fi BUILT_APP_PATH="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION/Sirious.app" -INSTALLED_HELPER_PATH="$INSTALL_APP_PATH/Contents/Resources/SiriousAutomationHelper" +INSTALLED_HELPER_PATH="$INSTALL_APP_PATH/Contents/Library/HelperTools/SiriousAutomationHelper" INSTALLED_AGENT_PLIST="$INSTALL_APP_PATH/Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist" INSTALLED_APP_EXECUTABLE="$INSTALL_APP_PATH/Contents/MacOS/Sirious" From f277777bc8e33d1704c8d0b940380b605ee8f80b Mon Sep 17 00:00:00 2001 From: Gale W Date: Wed, 17 Jun 2026 15:15:20 -0400 Subject: [PATCH 5/7] helper: align launchd agent plist shape --- Docs/Architecture/RealAppTestingPlan.md | 4 ++-- ROADMAP.md | 3 +++ .../Support/com.galewilliams.Sirious.AutomationHelper.plist | 6 ++++++ Tests/SiriousTests/AutomationHelperAgentStateTests.swift | 2 ++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Docs/Architecture/RealAppTestingPlan.md b/Docs/Architecture/RealAppTestingPlan.md index 3deb539..e5a0b9b 100644 --- a/Docs/Architecture/RealAppTestingPlan.md +++ b/Docs/Architecture/RealAppTestingPlan.md @@ -165,7 +165,7 @@ Sandbox and helper direction: - Keep the main app sandboxed for ordinary app behavior. Put assistive automation behind a separate helper boundary so the permissioned process is narrow and operator-visible. - Prefer a bundled LaunchAgent registered with `SMAppService.agent(plistName:)` before considering a LaunchDaemon, because these automation flows run in the logged-in user session and need user-facing Accessibility context. The first checked-in helper is `SiriousAutomationHelper`, a hardened-runtime command-line tool copied into the app bundle with its LaunchAgent plist at `Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist`. - `SiriousAutomationHelper` now embeds generated Info.plist metadata into the executable with `CREATE_INFOPLIST_SECTION_IN_BINARY` and `GENERATE_INFOPLIST_FILE`, keeping the helper identity visible to codesigning and Service Management. -- The helper LaunchAgent plist declares `BundleProgram` as `Contents/Library/HelperTools/SiriousAutomationHelper` and advertises `MachServices` for `com.galewilliams.Sirious.AutomationHelper`. +- The helper LaunchAgent plist declares `BundleProgram` as `Contents/Library/HelperTools/SiriousAutomationHelper`, targets the logged-in GUI session with `LimitLoadToSessionType` set to `Aqua`, keeps an explicit `ProgramArguments` entry for launchd-style argv shape, and advertises `MachServices` for `com.galewilliams.Sirious.AutomationHelper`. - Use the XPC command channel as the normal app-to-helper path. The app sends command arguments to the helper's Mach service, and the helper returns termination status plus output/error strings over a small shared protocol. The helper still accepts direct CLI commands such as `--status` for diagnostics. - Gale's local Apple signing team identifier is `BC73766F69`. Some certificate common names include `AMRC3N39SQ`, but the certificate subject's organizational unit is the Team ID Xcode uses for `DEVELOPMENT_TEAM`. Keep `DEVELOPMENT_TEAM` aligned with `BC73766F69` in `project.yml` so macOS TCC can associate prompts with a stable development identity during real-app testing. @@ -173,7 +173,7 @@ Installed-app validation note: `scripts/validate-installed-automation-helper.sh` is the local installed-app probe. It builds Sirious, installs the app into a validation location, verifies the app and helper signatures, confirms the LaunchAgent plist exists, runs the helper direct CLI, and asks the installed app for `SMAppService.agent(plistName:)` status. By default it copies the app directly. With `--package-style`, it stages the app under `Library/Application Support/SiriousInstalledAppValidation`, builds a local macOS Installer package with bundle relocation disabled, installs that package into `CurrentUserHomeDirectory`, and then runs the same validation checks. Running either path with `--keep-installed` keeps the validation app so later runs update it in place. Running it with `--uninstall` removes the retained validation copy. With `--register`, it can also request registration, check the XPC `--status` path when the agent is enabled, and unregister afterward. -Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, the helper executable is present at `Contents/Library/HelperTools`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from the repo-local `.build` copy and a package-installed `~/Library/Application Support/SiriousInstalledAppValidation/Sirious.app` copy. The copied-app and package-style probes treat this as successful bundle-shape validation unless registration or `--require-service-found` is requested. The next helper slice should investigate the remaining app identity, entitlement, or installation condition that Service Management needs before it recognizes the bundled LaunchAgent. +Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, the helper executable is present at `Contents/Library/HelperTools`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from the repo-local `.build` copy and a package-installed `~/Library/Application Support/SiriousInstalledAppValidation/Sirious.app` copy. Adding the launchd bridge keys `LimitLoadToSessionType=Aqua` and `ProgramArguments=["SiriousAutomationHelper"]` did not move either installed-app probe out of `.notFound`. The copied-app and package-style probes treat this as successful bundle-shape validation unless registration or `--require-service-found` is requested. The next helper slice should investigate whether this helper shape needs a different Service Management registration surface, a temporary sandbox Mach lookup entitlement for post-registration XPC communication, or a classic external LaunchAgent install path outside the app bundle. ## Initial Scenario Matrix diff --git a/ROADMAP.md b/ROADMAP.md index 8c40f57..d69c62e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -92,6 +92,7 @@ In Progress - [x] Add a bundled `SiriousAutomationHelper` LaunchAgent with embedded Info.plist metadata, a launchd `MachServices` entry, and an XPC command channel for Accessibility-owned text insertion. - [x] Document the copied-app install/update/uninstall workflow for local `SiriousAutomationHelper` validation. - [x] Validate `SiriousAutomationHelper` status from a package-style installed app outside DerivedData. +- [x] Align the helper LaunchAgent plist with GUI-session launchd guidance by adding `LimitLoadToSessionType=Aqua` and explicit `ProgramArguments`, and capture that Service Management still reports `.notFound`. - [ ] Determine what remaining app identity, entitlement, or installation condition keeps `SMAppService.agent(plistName:)` at `.notFound` after HelperTools copied-app and package-style validation both pass bundle-shape checks. ### Exit Criteria @@ -187,6 +188,7 @@ Planned - [ ] Add Computer Use setup, observation, and recovery notes for scenarios where Accessibility or app automation leaves a real gap. - [x] Add an installed-app helper validation slice that copies Sirious into a stable local app location before checking bundle shape, signatures, direct helper status, and `SMAppService.agent(plistName:)` status. - [x] Add a package-style installed-helper probe to determine whether Apple's Application Support install shape moves Service Management out of `.notFound` before registration, XPC connection, and helper Accessibility prompting. +- [x] Add launchd GUI-agent compatibility keys to the bundled helper plist and verify that copied-app and package-style installed probes still report `.notFound`. - [ ] Investigate why Service Management still reports `.notFound` after package-style Application Support installation verifies the app signature, HelperTools helper signature, LaunchAgent plist, and direct helper status. - [ ] Decide which scenarios belong in a local `.xctestplan`, which should be manifest-gated, and which should remain manual supervised checks. @@ -226,3 +228,4 @@ Planned - Added a bundled automation helper LaunchAgent with embedded helper Info.plist metadata, a `MachServices` plist entry, and an XPC command channel for helper-owned Accessibility commands. - Added an installed-app helper validation script and captured that copied app installs still report `.notFound` from Service Management despite valid bundle shape and signatures. - Added a package-style validation path through a local Installer package and captured that user Application Support package installs still report `.notFound` from Service Management despite valid bundle shape and signatures. +- Added launchd GUI-agent compatibility keys to the bundled helper plist and captured that the copied-app and package-style installed probes still report `.notFound`. diff --git a/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist b/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist index d311652..8aa47d5 100644 --- a/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist +++ b/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist @@ -10,10 +10,16 @@ Contents/Library/HelperTools/SiriousAutomationHelper Label com.galewilliams.Sirious.AutomationHelper + LimitLoadToSessionType + Aqua MachServices com.galewilliams.Sirious.AutomationHelper + ProgramArguments + + SiriousAutomationHelper + diff --git a/Tests/SiriousTests/AutomationHelperAgentStateTests.swift b/Tests/SiriousTests/AutomationHelperAgentStateTests.swift index 4eb9dd0..1ea16da 100644 --- a/Tests/SiriousTests/AutomationHelperAgentStateTests.swift +++ b/Tests/SiriousTests/AutomationHelperAgentStateTests.swift @@ -19,6 +19,8 @@ struct AutomationHelperAgentStateTests { #expect(plist["Label"] as? String == "com.galewilliams.Sirious.AutomationHelper") #expect(plist["BundleProgram"] as? String == "Contents/Library/HelperTools/SiriousAutomationHelper") + #expect(plist["LimitLoadToSessionType"] as? String == "Aqua") + #expect(plist["ProgramArguments"] as? [String] == ["SiriousAutomationHelper"]) #expect(plist["AssociatedBundleIdentifiers"] as? [String] == ["com.galewilliams.Sirious"]) #expect(plist["MachServices"] as? [String: Bool] == [ "com.galewilliams.Sirious.AutomationHelper": true, From 3c10d4b02b73fc2b1fd78aa867c114924324a775 Mon Sep 17 00:00:00 2001 From: Gale W Date: Wed, 17 Jun 2026 15:59:59 -0400 Subject: [PATCH 6/7] helper: add external launch agent bridge --- Docs/Architecture/RealAppTestingPlan.md | 7 +- ROADMAP.md | 9 +- Sirious.xcodeproj/project.pbxproj | 133 ++++++++- Sources/App/AutomationHelperDiagnostics.swift | 57 ++++ Sources/InheritedSandboxHelper/main.swift | 4 + Sources/Support/Sirious.entitlements | 4 + ...SiriousInheritedSandboxHelper.entitlements | 10 + .../AutomationHelperDiagnosticsTests.swift | 1 + project.yml | 20 ++ scripts/manage-external-automation-helper.sh | 275 ++++++++++++++++++ .../validate-installed-automation-helper.sh | 25 ++ 11 files changed, 538 insertions(+), 7 deletions(-) create mode 100644 Sources/InheritedSandboxHelper/main.swift create mode 100644 Sources/Support/SiriousInheritedSandboxHelper.entitlements create mode 100755 scripts/manage-external-automation-helper.sh diff --git a/Docs/Architecture/RealAppTestingPlan.md b/Docs/Architecture/RealAppTestingPlan.md index e5a0b9b..17d24f5 100644 --- a/Docs/Architecture/RealAppTestingPlan.md +++ b/Docs/Architecture/RealAppTestingPlan.md @@ -163,17 +163,20 @@ Sandbox and helper direction: - Apple identifies assistive Accessibility API use as restricted under App Sandbox, so real app text insertion and window automation should not be forced through the sandboxed main app. - Apple recommends diagnosing sandbox issues from the concrete sandbox violation log before adding capabilities or temporary exceptions. - Keep the main app sandboxed for ordinary app behavior. Put assistive automation behind a separate helper boundary so the permissioned process is narrow and operator-visible. +- `SiriousInheritedSandboxHelper` is the article-shaped embedded command-line helper probe from Apple's sandboxed helper recipe. XcodeGen builds it as a sandboxed tool with `com.apple.security.inherit`, embeds it in `Contents/MacOS`, and validates it through the app's `--inherited-helper-status` diagnostic. This proves the recipe maps cleanly into the project, but it also confirms that this helper category inherits the app sandbox and is not the unsandboxed Accessibility helper path. - Prefer a bundled LaunchAgent registered with `SMAppService.agent(plistName:)` before considering a LaunchDaemon, because these automation flows run in the logged-in user session and need user-facing Accessibility context. The first checked-in helper is `SiriousAutomationHelper`, a hardened-runtime command-line tool copied into the app bundle with its LaunchAgent plist at `Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist`. - `SiriousAutomationHelper` now embeds generated Info.plist metadata into the executable with `CREATE_INFOPLIST_SECTION_IN_BINARY` and `GENERATE_INFOPLIST_FILE`, keeping the helper identity visible to codesigning and Service Management. - The helper LaunchAgent plist declares `BundleProgram` as `Contents/Library/HelperTools/SiriousAutomationHelper`, targets the logged-in GUI session with `LimitLoadToSessionType` set to `Aqua`, keeps an explicit `ProgramArguments` entry for launchd-style argv shape, and advertises `MachServices` for `com.galewilliams.Sirious.AutomationHelper`. -- Use the XPC command channel as the normal app-to-helper path. The app sends command arguments to the helper's Mach service, and the helper returns termination status plus output/error strings over a small shared protocol. The helper still accepts direct CLI commands such as `--status` for diagnostics. +- Use the XPC command channel as the normal app-to-helper path. The app sends command arguments to the helper's Mach service, and the helper returns termination status plus output/error strings over a small shared protocol. The helper still accepts direct CLI commands such as `--status` for diagnostics. External user LaunchAgent validation installs `SiriousAutomationHelper` under `~/Library/Application Support/Sirious/AutomationHelper`, writes `~/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist`, bootstraps it into the `gui/$UID` domain, and requires the app's narrow `com.apple.security.temporary-exception.mach-lookup.global-name` entitlement for `com.galewilliams.Sirious.AutomationHelper`. - Gale's local Apple signing team identifier is `BC73766F69`. Some certificate common names include `AMRC3N39SQ`, but the certificate subject's organizational unit is the Team ID Xcode uses for `DEVELOPMENT_TEAM`. Keep `DEVELOPMENT_TEAM` aligned with `BC73766F69` in `project.yml` so macOS TCC can associate prompts with a stable development identity during real-app testing. Installed-app validation note: `scripts/validate-installed-automation-helper.sh` is the local installed-app probe. It builds Sirious, installs the app into a validation location, verifies the app and helper signatures, confirms the LaunchAgent plist exists, runs the helper direct CLI, and asks the installed app for `SMAppService.agent(plistName:)` status. By default it copies the app directly. With `--package-style`, it stages the app under `Library/Application Support/SiriousInstalledAppValidation`, builds a local macOS Installer package with bundle relocation disabled, installs that package into `CurrentUserHomeDirectory`, and then runs the same validation checks. Running either path with `--keep-installed` keeps the validation app so later runs update it in place. Running it with `--uninstall` removes the retained validation copy. With `--register`, it can also request registration, check the XPC `--status` path when the agent is enabled, and unregister afterward. -Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, the helper executable is present at `Contents/Library/HelperTools`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from the repo-local `.build` copy and a package-installed `~/Library/Application Support/SiriousInstalledAppValidation/Sirious.app` copy. Adding the launchd bridge keys `LimitLoadToSessionType=Aqua` and `ProgramArguments=["SiriousAutomationHelper"]` did not move either installed-app probe out of `.notFound`. The copied-app and package-style probes treat this as successful bundle-shape validation unless registration or `--require-service-found` is requested. The next helper slice should investigate whether this helper shape needs a different Service Management registration surface, a temporary sandbox Mach lookup entitlement for post-registration XPC communication, or a classic external LaunchAgent install path outside the app bundle. +Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, the helper executable is present at `Contents/Library/HelperTools`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from the repo-local `.build` copy and a package-installed `~/Library/Application Support/SiriousInstalledAppValidation/Sirious.app` copy. Adding the launchd bridge keys `LimitLoadToSessionType=Aqua` and `ProgramArguments=["SiriousAutomationHelper"]` did not move either installed-app probe out of `.notFound`. The copied-app and package-style probes treat this as successful bundle-shape validation unless registration or `--require-service-found` is requested. + +External LaunchAgent result: `scripts/manage-external-automation-helper.sh install --check-xpc` successfully builds the non-sandboxed helper, installs it into Gale's user Application Support directory, bootstraps the user LaunchAgent, and verifies the sandboxed app can reach the helper over `NSXPCConnection`. Before adding the Mach lookup temporary exception, the concrete sandbox log was `deny(1) mach-lookup com.galewilliams.Sirious.AutomationHelper`; after adding the entitlement, the app diagnostic returned `SiriousAutomationHelper is available.` While the external same-label LaunchAgent is installed, `SMAppService.agent(plistName:)` status from a validation app may report `enabled`; treat that as the external agent being active, not proof that the bundled LaunchAgent path is recognized. ## Initial Scenario Matrix diff --git a/ROADMAP.md b/ROADMAP.md index d69c62e..2b1b647 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -93,7 +93,9 @@ In Progress - [x] Document the copied-app install/update/uninstall workflow for local `SiriousAutomationHelper` validation. - [x] Validate `SiriousAutomationHelper` status from a package-style installed app outside DerivedData. - [x] Align the helper LaunchAgent plist with GUI-session launchd guidance by adding `LimitLoadToSessionType=Aqua` and explicit `ProgramArguments`, and capture that Service Management still reports `.notFound`. -- [ ] Determine what remaining app identity, entitlement, or installation condition keeps `SMAppService.agent(plistName:)` at `.notFound` after HelperTools copied-app and package-style validation both pass bundle-shape checks. +- [x] Add an article-shaped inherited sandbox helper probe and verify it works when spawned by the app, confirming that recipe is sandbox-inheriting rather than the unsandboxed Accessibility path. +- [x] Add an external user LaunchAgent install path for the non-sandboxed automation helper and verify the sandboxed app can reach it over XPC with a narrow Mach lookup exception. +- [ ] Decide whether the external user LaunchAgent helper becomes the local power-user lane, and what distribution/update/install UI should own it. ### Exit Criteria @@ -189,7 +191,8 @@ Planned - [x] Add an installed-app helper validation slice that copies Sirious into a stable local app location before checking bundle shape, signatures, direct helper status, and `SMAppService.agent(plistName:)` status. - [x] Add a package-style installed-helper probe to determine whether Apple's Application Support install shape moves Service Management out of `.notFound` before registration, XPC connection, and helper Accessibility prompting. - [x] Add launchd GUI-agent compatibility keys to the bundled helper plist and verify that copied-app and package-style installed probes still report `.notFound`. -- [ ] Investigate why Service Management still reports `.notFound` after package-style Application Support installation verifies the app signature, HelperTools helper signature, LaunchAgent plist, and direct helper status. +- [x] Investigate why Service Management still reports `.notFound` after package-style Application Support installation verifies the app signature, HelperTools helper signature, LaunchAgent plist, and direct helper status; external LaunchAgent install works, while the bundled ServiceManagement path remains blocked. +- [ ] Design the external helper install, update, uninstall, and version check surface for local distribution. - [ ] Decide which scenarios belong in a local `.xctestplan`, which should be manifest-gated, and which should remain manual supervised checks. ### Exit Criteria @@ -229,3 +232,5 @@ Planned - Added an installed-app helper validation script and captured that copied app installs still report `.notFound` from Service Management despite valid bundle shape and signatures. - Added a package-style validation path through a local Installer package and captured that user Application Support package installs still report `.notFound` from Service Management despite valid bundle shape and signatures. - Added launchd GUI-agent compatibility keys to the bundled helper plist and captured that the copied-app and package-style installed probes still report `.notFound`. +- Added a sandbox-inherited embedded helper probe for Apple's command-line helper recipe and captured that it works only when spawned by the sandboxed app. +- Added an external user LaunchAgent install path for the non-sandboxed automation helper and captured successful sandboxed-app XPC after adding the narrow Mach lookup exception. diff --git a/Sirious.xcodeproj/project.pbxproj b/Sirious.xcodeproj/project.pbxproj index 6b2ad64..7352b00 100644 --- a/Sirious.xcodeproj/project.pbxproj +++ b/Sirious.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 186B2F4188A61F1837AB0663 /* WorkspaceStateProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EC8789017EFD204AFA940AF /* WorkspaceStateProviding.swift */; }; 188C565FF6D5248D8D1347DC /* InstalledApplicationCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA94681BB3770374ED64F0E /* InstalledApplicationCandidate.swift */; }; 1A4930B07DFE71B984524BBB /* RoutingMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90148173640BEF7E4606F92 /* RoutingMode.swift */; }; + 1F465E4A5B1E5952BFEBB418 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E975D0C71AE5E656E3CEE01 /* main.swift */; }; 1F910B2AD1BE2155E2B93A88 /* AutomationHelperDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D38CDDDF3EA5F3EA1F7DDF /* AutomationHelperDiagnostics.swift */; }; 2160056700C4D8EBDC9C045B /* TextCommandExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB4F9D0CEB18279D1874CB15 /* TextCommandExecutor.swift */; }; 2707C2AA585F5AFEF3044D17 /* FirstStageContextResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B21CE25C80990889763EA0 /* FirstStageContextResolverTests.swift */; }; @@ -136,6 +137,7 @@ E998F68EABAF6FCB741E694F /* DictionaryCommandExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C2F02651E753A83ACD4080 /* DictionaryCommandExecutor.swift */; }; EE79534A405C54AC8658DA0F /* PatternCommandRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 601B712F4A4F30B8E987A12C /* PatternCommandRouter.swift */; }; F1A6D1E463CEEFB167170751 /* AppWindowID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076D6E1313CFACB95E4771C6 /* AppWindowID.swift */; }; + F27AE01F5AC2EDDB7F7EB473 /* SiriousInheritedSandboxHelper in Embed Dependencies */ = {isa = PBXBuildFile; fileRef = 1EB04A3B28FD4AEBE3F5AD0A /* SiriousInheritedSandboxHelper */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F327516745842436F36F535E /* CommandToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 790864528DEDE51E0C382016 /* CommandToken.swift */; }; F3E1F8592991E11B1AD792E1 /* AutomationHelperXPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E9BFD6ED449FBB8CA4C292 /* AutomationHelperXPC.swift */; }; F4FDD4C1E5EEB975DEB7E995 /* DictionaryCommandPatterns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3413676491CAB35A1A297A97 /* DictionaryCommandPatterns.swift */; }; @@ -150,6 +152,13 @@ remoteGlobalIDString = C4B92D384B30AFDB8362F542; remoteInfo = Sirious; }; + 463D8B9E123D013E00DD6E76 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D5BD09856B751B5B7C214BF3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C1870AF24C3CC07DC228DCBD; + remoteInfo = SiriousInheritedSandboxHelper; + }; CC464BC10263DEBA5CF7605D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D5BD09856B751B5B7C214BF3 /* Project object */; @@ -170,10 +179,10 @@ 9AB0A85719BD262239B4AA20 /* Embed Dependencies */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; - dstPath = Contents/Library/HelperTools; - dstSubfolderSpec = 1; + dstPath = ""; + dstSubfolderSpec = 6; files = ( - 8315DA30FD65517F9BC0D94C /* SiriousAutomationHelper in Embed Dependencies */, + F27AE01F5AC2EDDB7F7EB473 /* SiriousInheritedSandboxHelper in Embed Dependencies */, ); name = "Embed Dependencies"; runOnlyForDeploymentPostprocessing = 0; @@ -188,6 +197,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FCC8378FE7C033B2654428D7 /* Embed Dependencies */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Contents/Library/HelperTools; + dstSubfolderSpec = 1; + files = ( + 8315DA30FD65517F9BC0D94C /* SiriousAutomationHelper in Embed Dependencies */, + ); + name = "Embed Dependencies"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -211,6 +231,7 @@ 1BDE7311B1C7FF3759B8546A /* RoutingModeProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingModeProviding.swift; sourceTree = ""; }; 1DA0360B18CA6166310C8A3F /* TranscriptSpanStabilizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptSpanStabilizer.swift; sourceTree = ""; }; 1E32B7AF7E35616EB4565C29 /* CommandTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTarget.swift; sourceTree = ""; }; + 1EB04A3B28FD4AEBE3F5AD0A /* SiriousInheritedSandboxHelper */ = {isa = PBXFileReference; includeInIndex = 0; path = SiriousInheritedSandboxHelper; sourceTree = BUILT_PRODUCTS_DIR; }; 1EC8789017EFD204AFA940AF /* WorkspaceStateProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStateProviding.swift; sourceTree = ""; }; 212AEC0F6B5C299D4049BBE7 /* CommandExecutionDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandExecutionDispatcher.swift; sourceTree = ""; }; 23FFEC24EC3EC6F4F85C52B3 /* HomeDirectoryAccessState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeDirectoryAccessState.swift; sourceTree = ""; }; @@ -220,6 +241,7 @@ 2875F35C0FE138C8A47182C5 /* MediaCommandExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCommandExecutor.swift; sourceTree = ""; }; 287D0A94B24438C530B0C39B /* FirstStageNormalizationAndFallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstStageNormalizationAndFallbackTests.swift; sourceTree = ""; }; 291692174DB8FA310F6D8272 /* SystemContextSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemContextSnapshot.swift; sourceTree = ""; }; + 2E975D0C71AE5E656E3CEE01 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 333E9B9AC52B4D4FC419117F /* TextEntrySessionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntrySessionState.swift; sourceTree = ""; }; 3413676491CAB35A1A297A97 /* DictionaryCommandPatterns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryCommandPatterns.swift; sourceTree = ""; }; 34551AF675C0A18E447AEB8D /* StreamingReadiness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingReadiness.swift; sourceTree = ""; }; @@ -404,6 +426,7 @@ children = ( 19F29D22576FD82326A5026A /* Sirious.app */, E334B2CD8EF5D6FEED14723F /* SiriousAutomationHelper */, + 1EB04A3B28FD4AEBE3F5AD0A /* SiriousInheritedSandboxHelper */, 0EECB5309F1BD26568BD1989 /* SiriousTests.xctest */, A858746B64A185F645A070A6 /* SiriousUITests.xctest */, ); @@ -582,6 +605,14 @@ path = Sources/Routing; sourceTree = ""; }; + 9776EA1A7365760DF7699245 /* Inherited Sandbox Helper */ = { + isa = PBXGroup; + children = ( + B79F033D8D165A071A632543 /* InheritedSandboxHelper */, + ); + name = "Inherited Sandbox Helper"; + sourceTree = ""; + }; A3C993BA8795898FAF3E18ED /* AutomationHelperSupport */ = { isa = PBXGroup; children = ( @@ -598,6 +629,7 @@ 59179663817A3498DD73062C /* Automation Helper */, 63CBC2A3630DFF0F54AB716C /* Automation Helper Support */, 702E9622F33424786D055805 /* Config */, + 9776EA1A7365760DF7699245 /* Inherited Sandbox Helper */, 17A2C7DDB9F78ECCCAD57A6D /* Permissions */, 2E67BE6B59361C660BFD951D /* Routing */, 44C406102AD487A9287F3469 /* Support */, @@ -620,6 +652,15 @@ path = SystemCommands; sourceTree = ""; }; + B79F033D8D165A071A632543 /* InheritedSandboxHelper */ = { + isa = PBXGroup; + children = ( + 2E975D0C71AE5E656E3CEE01 /* main.swift */, + ); + name = InheritedSandboxHelper; + path = Sources/InheritedSandboxHelper; + sourceTree = ""; + }; C0EA7D68C676023F5E5F8F8A /* Context */ = { isa = PBXGroup; children = ( @@ -775,6 +816,23 @@ productReference = A858746B64A185F645A070A6 /* SiriousUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + C1870AF24C3CC07DC228DCBD /* SiriousInheritedSandboxHelper */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC0510022A9643BF26EDFD60 /* Build configuration list for PBXNativeTarget "SiriousInheritedSandboxHelper" */; + buildPhases = ( + 4EAA2069B19702623AF1873A /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SiriousInheritedSandboxHelper; + packageProductDependencies = ( + ); + productName = SiriousInheritedSandboxHelper; + productReference = 1EB04A3B28FD4AEBE3F5AD0A /* SiriousInheritedSandboxHelper */; + productType = "com.apple.product-type.tool"; + }; C4B92D384B30AFDB8362F542 /* Sirious */ = { isa = PBXNativeTarget; buildConfigurationList = 4B8ED482AC45B69555A6BABE /* Build configuration list for PBXNativeTarget "Sirious" */; @@ -782,10 +840,12 @@ 0BC582E550C40FCC01398165 /* Sources */, B52489F9FC33223DA0376E80 /* CopyFiles */, 9AB0A85719BD262239B4AA20 /* Embed Dependencies */, + FCC8378FE7C033B2654428D7 /* Embed Dependencies */, ); buildRules = ( ); dependencies = ( + CE73D4E6B253C219EA926C83 /* PBXTargetDependency */, D8BD4A8F1A26C769EFD80F2F /* PBXTargetDependency */, ); name = Sirious; @@ -817,6 +877,10 @@ ProvisioningStyle = Automatic; TestTargetID = C4B92D384B30AFDB8362F542; }; + C1870AF24C3CC07DC228DCBD = { + DevelopmentTeam = BC73766F69; + ProvisioningStyle = Automatic; + }; C4B92D384B30AFDB8362F542 = { DevelopmentTeam = BC73766F69; ProvisioningStyle = Automatic; @@ -839,6 +903,7 @@ targets = ( C4B92D384B30AFDB8362F542 /* Sirious */, 14C9F9EF3028FC8EB6B17CB9 /* SiriousAutomationHelper */, + C1870AF24C3CC07DC228DCBD /* SiriousInheritedSandboxHelper */, 817F011D0ED9512A5DABBD66 /* SiriousTests */, 90ACFC5E40EA89E225D48F63 /* SiriousUITests */, ); @@ -949,6 +1014,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4EAA2069B19702623AF1873A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1F465E4A5B1E5952BFEBB418 /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9657309F5845F2A303B1BCE8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1018,6 +1091,11 @@ target = C4B92D384B30AFDB8362F542 /* Sirious */; targetProxy = E9A6C793706A76B6F4651125 /* PBXContainerItemProxy */; }; + CE73D4E6B253C219EA926C83 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C1870AF24C3CC07DC228DCBD /* SiriousInheritedSandboxHelper */; + targetProxy = 463D8B9E123D013E00DD6E76 /* PBXContainerItemProxy */; + }; D8BD4A8F1A26C769EFD80F2F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 14C9F9EF3028FC8EB6B17CB9 /* SiriousAutomationHelper */; @@ -1026,6 +1104,26 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 2AE769341E79A7C6985E5600 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = Sources/Support/SiriousInheritedSandboxHelper.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + COMBINE_HIDPI_IMAGES = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + OTHER_CODE_SIGN_FLAGS = "$(inherited) -i $(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_BUNDLE_IDENTIFIER = com.galewilliams.Sirious.InheritedSandboxHelper; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Debug; + }; 38672C1BDE826267F438205E /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 0AFDC88F1FE4D3CC5C16FBA1 /* Sirious.xcconfig */; @@ -1087,6 +1185,26 @@ }; name = Release; }; + 4DDB54371054B379DC4B351F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = Sources/Support/SiriousInheritedSandboxHelper.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + COMBINE_HIDPI_IMAGES = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + OTHER_CODE_SIGN_FLAGS = "$(inherited) -i $(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_BUNDLE_IDENTIFIER = com.galewilliams.Sirious.InheritedSandboxHelper; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; 52561ED234866D654C5CC6C7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1344,6 +1462,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + DC0510022A9643BF26EDFD60 /* Build configuration list for PBXNativeTarget "SiriousInheritedSandboxHelper" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2AE769341E79A7C6985E5600 /* Debug */, + 4DDB54371054B379DC4B351F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; E2EC6FA405869CD972E8D14A /* Build configuration list for PBXProject "Sirious" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Sources/App/AutomationHelperDiagnostics.swift b/Sources/App/AutomationHelperDiagnostics.swift index 9a764d5..27b8b5b 100644 --- a/Sources/App/AutomationHelperDiagnostics.swift +++ b/Sources/App/AutomationHelperDiagnostics.swift @@ -6,6 +6,7 @@ enum AutomationHelperDiagnosticCommand: String, CaseIterable { case register = "--automation-helper-register" case unregister = "--automation-helper-unregister" case xpcStatus = "--automation-helper-xpc-status" + case inheritedHelperStatus = "--inherited-helper-status" static var usage: String { allCases.map(\.rawValue).joined(separator: ", ") @@ -68,6 +69,9 @@ enum AutomationHelperDiagnostics { } return .failure(result.trimmedMessage) + + case .inheritedHelperStatus: + return InheritedSandboxHelperDiagnostics.run() } } } @@ -85,6 +89,59 @@ struct AutomationHelperDiagnosticResult: Equatable { } } +private enum InheritedSandboxHelperDiagnostics { + static func run() -> AutomationHelperDiagnosticResult { + guard let helperURL = Bundle.main.executableURL? + .deletingLastPathComponent() + .appendingPathComponent("SiriousInheritedSandboxHelper") + else { + return .failure("Sirious could not resolve its bundled inherited sandbox helper path from the app executable URL.") + } + + guard FileManager.default.isExecutableFile(atPath: helperURL.path) else { + return .failure("Sirious could not run the inherited sandbox helper because it is missing or not executable at \(helperURL.path).") + } + + let process = Process() + let outputPipe = Pipe() + let errorPipe = Pipe() + + process.executableURL = helperURL + process.standardOutput = outputPipe + process.standardError = errorPipe + + do { + try process.run() + } catch { + return .failure("Sirious could not launch the inherited sandbox helper at \(helperURL.path). macOS reported: \(error.localizedDescription).") + } + + process.waitUntilExit() + + let output = String( + data: outputPipe.fileHandleForReading.readDataToEndOfFile(), + encoding: .utf8 + )? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let errorOutput = String( + data: errorPipe.fileHandleForReading.readDataToEndOfFile(), + encoding: .utf8 + )? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + if process.terminationStatus == 0 { + return .success(output.isEmpty ? "Sirious inherited sandbox helper ran successfully without output." : output) + } + + let message = errorOutput.isEmpty ? output : errorOutput + return .failure( + message.isEmpty + ? "Sirious inherited sandbox helper exited with status \(process.terminationStatus) without writing output." + : "Sirious inherited sandbox helper exited with status \(process.terminationStatus): \(message)" + ) + } +} + private enum AutomationHelperXPCDiagnostics { static func run(arguments: [String]) -> AutomationHelperCommandResult { let completion = AutomationHelperXPCDiagnosticCompletion(arguments: arguments) diff --git a/Sources/InheritedSandboxHelper/main.swift b/Sources/InheritedSandboxHelper/main.swift new file mode 100644 index 0000000..d4d45dd --- /dev/null +++ b/Sources/InheritedSandboxHelper/main.swift @@ -0,0 +1,4 @@ +import Foundation + +let bundleIdentifier = Bundle.main.bundleIdentifier ?? "unknown" +print("SiriousInheritedSandboxHelper is available. Bundle identifier: \(bundleIdentifier).") diff --git a/Sources/Support/Sirious.entitlements b/Sources/Support/Sirious.entitlements index 3efff2f..d4c716e 100644 --- a/Sources/Support/Sirious.entitlements +++ b/Sources/Support/Sirious.entitlements @@ -14,5 +14,9 @@ com.apple.security.files.user-selected.read-write + com.apple.security.temporary-exception.mach-lookup.global-name + + com.galewilliams.Sirious.AutomationHelper + diff --git a/Sources/Support/SiriousInheritedSandboxHelper.entitlements b/Sources/Support/SiriousInheritedSandboxHelper.entitlements new file mode 100644 index 0000000..794eada --- /dev/null +++ b/Sources/Support/SiriousInheritedSandboxHelper.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.inherit + + + diff --git a/Tests/SiriousTests/AutomationHelperDiagnosticsTests.swift b/Tests/SiriousTests/AutomationHelperDiagnosticsTests.swift index 6eb8881..7010646 100644 --- a/Tests/SiriousTests/AutomationHelperDiagnosticsTests.swift +++ b/Tests/SiriousTests/AutomationHelperDiagnosticsTests.swift @@ -15,6 +15,7 @@ struct AutomationHelperDiagnosticsTests { #expect(AutomationHelperDiagnostics.command(from: ["Sirious", "--automation-helper-register"]) == .register) #expect(AutomationHelperDiagnostics.command(from: ["Sirious", "--automation-helper-unregister"]) == .unregister) #expect(AutomationHelperDiagnostics.command(from: ["Sirious", "--automation-helper-xpc-status"]) == .xpcStatus) + #expect(AutomationHelperDiagnostics.command(from: ["Sirious", "--inherited-helper-status"]) == .inheritedHelperStatus) } @Test("automation helper diagnostic result factories map exit codes") diff --git a/project.yml b/project.yml index 44ea410..acb718e 100644 --- a/project.yml +++ b/project.yml @@ -41,6 +41,10 @@ targets: destination: wrapper subpath: Contents/Library/LaunchAgents dependencies: + - target: SiriousInheritedSandboxHelper + embed: true + copy: + destination: executables - target: SiriousAutomationHelper embed: true copy: @@ -93,6 +97,22 @@ targets: OTHER_CODE_SIGN_FLAGS: $(inherited) -i $(PRODUCT_BUNDLE_IDENTIFIER) PRODUCT_BUNDLE_IDENTIFIER: com.galewilliams.Sirious.AutomationHelper SKIP_INSTALL: YES + SiriousInheritedSandboxHelper: + type: tool + platform: macOS + sources: + - path: Sources/InheritedSandboxHelper + group: Inherited Sandbox Helper + settings: + base: + CODE_SIGN_ENTITLEMENTS: Sources/Support/SiriousInheritedSandboxHelper.entitlements + CODE_SIGN_IDENTITY: Apple Development + CODE_SIGN_INJECT_BASE_ENTITLEMENTS: NO + ENABLE_APP_SANDBOX: YES + ENABLE_HARDENED_RUNTIME: YES + OTHER_CODE_SIGN_FLAGS: $(inherited) -i $(PRODUCT_BUNDLE_IDENTIFIER) + PRODUCT_BUNDLE_IDENTIFIER: com.galewilliams.Sirious.InheritedSandboxHelper + SKIP_INSTALL: YES SiriousTests: type: bundle.unit-test platform: macOS diff --git a/scripts/manage-external-automation-helper.sh b/scripts/manage-external-automation-helper.sh new file mode 100755 index 0000000..58235da --- /dev/null +++ b/scripts/manage-external-automation-helper.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +REPO_ROOT=$(CDPATH= cd -- "$SELF_DIR/.." && pwd) + +ACTION=status +CONFIGURATION=Debug +DERIVED_DATA_PATH="$REPO_ROOT/.build/ExternalAutomationHelper/DerivedData" +INSTALL_ROOT="$HOME/Library/Application Support/Sirious/AutomationHelper" +LAUNCH_AGENT_DIR="$HOME/Library/LaunchAgents" +LABEL="com.galewilliams.Sirious.AutomationHelper" +HELPER_NAME="SiriousAutomationHelper" +SKIP_BUILD=0 +CHECK_XPC=0 + +usage() { + cat <<'USAGE' +Usage: scripts/manage-external-automation-helper.sh [action] [options] + +Install, inspect, or uninstall SiriousAutomationHelper as an external user +LaunchAgent. This is the local non-App-Store path for testing a non-sandboxed +Accessibility helper outside the sandboxed app bundle. + +Actions: + install Build the helper, install it under Application Support, write + ~/Library/LaunchAgents, and bootstrap it into the GUI session. + status Print helper file, launchd, direct CLI, and optional XPC status. + restart Boot out and bootstrap the installed LaunchAgent again. + uninstall Boot out the LaunchAgent and remove the installed plist/helper. + +Options: + --configuration NAME Xcode configuration to build. Default: Debug. + --derived-data PATH DerivedData path for the helper build. + --install-root PATH Directory for the helper executable. + --launch-agent-dir PATH Directory for the LaunchAgent plist. + --skip-build Reuse an already built helper for install. + --check-xpc Ask the built app to connect to the helper Mach service. + -h, --help Show this help. +USAGE +} + +log() { + printf '%s\n' "$*" +} + +fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +case "${1:-}" in + install|status|restart|uninstall) + ACTION=$1 + shift + ;; +esac + +while [ "$#" -gt 0 ]; do + case "$1" in + --configuration) + [ "$#" -ge 2 ] || fail "--configuration requires a value." + CONFIGURATION=$2 + shift 2 + ;; + --derived-data) + [ "$#" -ge 2 ] || fail "--derived-data requires a value." + DERIVED_DATA_PATH=$2 + shift 2 + ;; + --install-root) + [ "$#" -ge 2 ] || fail "--install-root requires a value." + INSTALL_ROOT=$2 + shift 2 + ;; + --launch-agent-dir) + [ "$#" -ge 2 ] || fail "--launch-agent-dir requires a value." + LAUNCH_AGENT_DIR=$2 + shift 2 + ;; + --skip-build) + SKIP_BUILD=1 + shift + ;; + --check-xpc) + CHECK_XPC=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "Unsupported argument '$1'. Run with --help for usage." + ;; + esac +done + +case "$INSTALL_ROOT" in + "$HOME"/*) + ;; + *) + fail "--install-root must be under $HOME for this user LaunchAgent workflow." + ;; +esac + +case "$LAUNCH_AGENT_DIR" in + "$HOME"/*) + ;; + *) + fail "--launch-agent-dir must be under $HOME for this user LaunchAgent workflow." + ;; +esac + +HELPER_PATH="$INSTALL_ROOT/$HELPER_NAME" +PLIST_PATH="$LAUNCH_AGENT_DIR/$LABEL.plist" +BUILT_HELPER_PATH="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION/$HELPER_NAME" +BUILT_APP_EXECUTABLE="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION/Sirious.app/Contents/MacOS/Sirious" +DOMAIN="gui/$(id -u)" +SERVICE_NAME="$DOMAIN/$LABEL" + +bootout_if_loaded() { + if launchctl print "$SERVICE_NAME" >/dev/null 2>&1; then + log "Booting out existing LaunchAgent $SERVICE_NAME." + launchctl bootout "$DOMAIN" "$PLIST_PATH" + fi +} + +write_plist() { + mkdir -p "$LAUNCH_AGENT_DIR" + cat > "$PLIST_PATH" < + + + + AssociatedBundleIdentifiers + + com.galewilliams.Sirious + + Label + $LABEL + LimitLoadToSessionType + Aqua + MachServices + + $LABEL + + + ProgramArguments + + $HELPER_PATH + + + +PLIST +} + +build_helper() { + if [ "$SKIP_BUILD" -eq 1 ]; then + return + fi + + log "Building SiriousAutomationHelper $CONFIGURATION into $DERIVED_DATA_PATH." + xcodebuild build \ + -project "$REPO_ROOT/Sirious.xcodeproj" \ + -scheme SiriousAutomationHelper \ + -configuration "$CONFIGURATION" \ + -destination "platform=macOS,arch=arm64" \ + -derivedDataPath "$DERIVED_DATA_PATH" +} + +install_helper() { + build_helper + [ -x "$BUILT_HELPER_PATH" ] || fail "Expected built helper at $BUILT_HELPER_PATH, but it does not exist or is not executable." + + log "Installing helper executable at $HELPER_PATH." + mkdir -p "$INSTALL_ROOT" + cp "$BUILT_HELPER_PATH" "$HELPER_PATH" + chmod 755 "$HELPER_PATH" + + log "Writing LaunchAgent plist at $PLIST_PATH." + write_plist + plutil -lint "$PLIST_PATH" + + log "Installed helper signature:" + codesign --verify --strict --verbose=2 "$HELPER_PATH" + + bootout_if_loaded + log "Bootstrapping LaunchAgent $SERVICE_NAME." + launchctl bootstrap "$DOMAIN" "$PLIST_PATH" + log "Kickstarting LaunchAgent $SERVICE_NAME." + launchctl kickstart -k "$SERVICE_NAME" +} + +print_status() { + if [ -x "$HELPER_PATH" ]; then + log "Installed helper executable: $HELPER_PATH" + "$HELPER_PATH" --status + else + log "Installed helper executable is missing at $HELPER_PATH." + fi + + if [ -f "$PLIST_PATH" ]; then + log "Installed LaunchAgent plist:" + plutil -p "$PLIST_PATH" + else + log "Installed LaunchAgent plist is missing at $PLIST_PATH." + fi + + if launchctl print "$SERVICE_NAME"; then + log "LaunchAgent $SERVICE_NAME is loaded." + else + log "LaunchAgent $SERVICE_NAME is not loaded." + fi + + if [ "$CHECK_XPC" -eq 1 ]; then + if [ "$SKIP_BUILD" -eq 0 ] || [ ! -x "$BUILT_APP_EXECUTABLE" ]; then + log "Building Sirious app so the sandboxed app executable can run the XPC diagnostic." + xcodebuild build \ + -project "$REPO_ROOT/Sirious.xcodeproj" \ + -scheme Sirious \ + -configuration "$CONFIGURATION" \ + -destination "platform=macOS,arch=arm64" \ + -derivedDataPath "$DERIVED_DATA_PATH" + fi + + log "XPC status from sandboxed Sirious app executable:" + "$BUILT_APP_EXECUTABLE" --automation-helper-xpc-status + fi +} + +uninstall_helper() { + bootout_if_loaded + + if [ -f "$PLIST_PATH" ]; then + log "Removing LaunchAgent plist at $PLIST_PATH." + rm -f "$PLIST_PATH" + fi + + if [ -e "$HELPER_PATH" ]; then + log "Removing helper executable at $HELPER_PATH." + rm -f "$HELPER_PATH" + fi + + if [ -d "$INSTALL_ROOT" ]; then + rmdir "$INSTALL_ROOT" 2>/dev/null || true + fi +} + +cd "$REPO_ROOT" + +case "$ACTION" in + install) + install_helper + print_status + ;; + status) + print_status + ;; + restart) + [ -f "$PLIST_PATH" ] || fail "Cannot restart missing LaunchAgent plist at $PLIST_PATH." + bootout_if_loaded + log "Bootstrapping LaunchAgent $SERVICE_NAME." + launchctl bootstrap "$DOMAIN" "$PLIST_PATH" + log "Kickstarting LaunchAgent $SERVICE_NAME." + launchctl kickstart -k "$SERVICE_NAME" + print_status + ;; + uninstall) + uninstall_helper + ;; + *) + fail "Unsupported action '$ACTION'. Run with --help for usage." + ;; +esac diff --git a/scripts/validate-installed-automation-helper.sh b/scripts/validate-installed-automation-helper.sh index cdad472..ed1770b 100755 --- a/scripts/validate-installed-automation-helper.sh +++ b/scripts/validate-installed-automation-helper.sh @@ -154,8 +154,10 @@ fi BUILT_APP_PATH="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION/Sirious.app" INSTALLED_HELPER_PATH="$INSTALL_APP_PATH/Contents/Library/HelperTools/SiriousAutomationHelper" +INSTALLED_INHERITED_HELPER_PATH="$INSTALL_APP_PATH/Contents/MacOS/SiriousInheritedSandboxHelper" INSTALLED_AGENT_PLIST="$INSTALL_APP_PATH/Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist" INSTALLED_APP_EXECUTABLE="$INSTALL_APP_PATH/Contents/MacOS/Sirious" +EXTERNAL_AGENT_PLIST="$HOME/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist" remove_installed_app() { if [ -e "$INSTALL_APP_PATH" ]; then @@ -244,6 +246,7 @@ if [ "$SKIP_BUILD" -eq 0 ]; then fi [ -d "$BUILT_APP_PATH" ] || fail "Expected built app at $BUILT_APP_PATH, but it does not exist." +[ -x "$BUILT_APP_PATH/Contents/MacOS/SiriousInheritedSandboxHelper" ] || fail "Expected article-shaped inherited sandbox helper at $BUILT_APP_PATH/Contents/MacOS/SiriousInheritedSandboxHelper, but it does not exist or is not executable." if [ "$PACKAGE_STYLE" -eq 1 ]; then install_validation_package @@ -252,6 +255,7 @@ else fi [ -x "$INSTALLED_APP_EXECUTABLE" ] || fail "Installed app executable is missing or not executable at $INSTALLED_APP_EXECUTABLE." +[ -x "$INSTALLED_INHERITED_HELPER_PATH" ] || fail "Installed inherited sandbox helper is missing or not executable at $INSTALLED_INHERITED_HELPER_PATH." [ -x "$INSTALLED_HELPER_PATH" ] || fail "Installed helper executable is missing or not executable at $INSTALLED_HELPER_PATH." [ -f "$INSTALLED_AGENT_PLIST" ] || fail "Installed LaunchAgent plist is missing at $INSTALLED_AGENT_PLIST." @@ -261,6 +265,23 @@ codesign --verify --deep --strict --verbose=2 "$INSTALL_APP_PATH" log "Installed helper signature:" codesign --verify --strict --verbose=2 "$INSTALLED_HELPER_PATH" +log "Installed inherited sandbox helper signature:" +codesign --verify --strict --verbose=2 "$INSTALLED_INHERITED_HELPER_PATH" + +log "Installed inherited sandbox helper entitlements:" +INHERITED_HELPER_ENTITLEMENTS=$(codesign -d --entitlements :- "$INSTALLED_INHERITED_HELPER_PATH" 2>/dev/null) +printf '%s\n' "$INHERITED_HELPER_ENTITLEMENTS" +case "$INHERITED_HELPER_ENTITLEMENTS" in + *"com.apple.security.app-sandbox"*"com.apple.security.inherit"*) + ;; + *) + fail "Installed inherited sandbox helper entitlements should contain only the App Sandbox and inheritance keys from Apple's embedded command-line helper recipe." + ;; +esac + +log "Inherited sandbox helper diagnostic through the sandboxed app:" +"$INSTALLED_APP_EXECUTABLE" --inherited-helper-status + log "Installed LaunchAgent plist:" plutil -p "$INSTALLED_AGENT_PLIST" @@ -271,6 +292,10 @@ log "ServiceManagement status from installed app:" STATUS_OUTPUT=$("$INSTALLED_APP_EXECUTABLE" --automation-helper-status) printf '%s\n' "$STATUS_OUTPUT" +if [ -f "$EXTERNAL_AGENT_PLIST" ]; then + log "External user LaunchAgent plist exists at $EXTERNAL_AGENT_PLIST. ServiceManagement status may reflect that same-label external agent rather than proving that the bundled LaunchAgent is discoverable." +fi + case "$STATUS_OUTPUT" in *notFound*) if [ "$RUN_REGISTRATION" -eq 1 ] || [ "$REQUIRE_SERVICE_FOUND" -eq 1 ]; then From 1166f7be4c4625719fcde57d2f12101daca0d17c Mon Sep 17 00:00:00 2001 From: Gale W Date: Wed, 17 Jun 2026 18:17:32 -0400 Subject: [PATCH 7/7] docs: capture helper socket fallback --- Docs/Architecture/RealAppTestingPlan.md | 2 ++ README.md | 8 ++++++++ ROADMAP.md | 3 +++ 3 files changed, 13 insertions(+) diff --git a/Docs/Architecture/RealAppTestingPlan.md b/Docs/Architecture/RealAppTestingPlan.md index 17d24f5..502abc7 100644 --- a/Docs/Architecture/RealAppTestingPlan.md +++ b/Docs/Architecture/RealAppTestingPlan.md @@ -178,6 +178,8 @@ Current local result: the copied app reports the expected app bundle path and bu External LaunchAgent result: `scripts/manage-external-automation-helper.sh install --check-xpc` successfully builds the non-sandboxed helper, installs it into Gale's user Application Support directory, bootstraps the user LaunchAgent, and verifies the sandboxed app can reach the helper over `NSXPCConnection`. Before adding the Mach lookup temporary exception, the concrete sandbox log was `deny(1) mach-lookup com.galewilliams.Sirious.AutomationHelper`; after adding the entitlement, the app diagnostic returned `SiriousAutomationHelper is available.` While the external same-label LaunchAgent is installed, `SMAppService.agent(plistName:)` status from a validation app may report `enabled`; treat that as the external agent being active, not proof that the bundled LaunchAgent path is recognized. +Localhost socket fallback plan: keep this as a backup only if App Store review, entitlement policy, or a future distribution lane makes the Mach lookup exception unacceptable. In that design, the external user LaunchAgent would bind to `127.0.0.1` on an ephemeral port, write a user-only endpoint file containing the port plus a short-lived authentication token under `~/Library/Application Support/Sirious/AutomationHelper`, and accept requests only from clients that present the token and pass the existing code-signing audit checks where available. The app would use `com.apple.security.network.client` for the outbound localhost connection, not the Mach lookup exception. The helper should rotate the token whenever it starts, reject stale protocol versions, keep the protocol request/response-shaped like `AutomationHelperCommandResult`, and treat Accessibility commands as explicitly permissioned operations with the same human-readable failures as the XPC path. This fallback costs more implementation surface than Mach XPC: port discovery, token storage, request authentication, stale server recovery, protocol framing, and local firewall or endpoint-collision diagnostics all become product responsibilities. + ## Initial Scenario Matrix | Scenario | Layer | Target | Expected Result | Automation Level | diff --git a/README.md b/README.md index ce5dea3..2f66f10 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,14 @@ scripts/validate-installed-automation-helper.sh --package-style --keep-installed The package-style probe installs the validation app through a local macOS Installer package into the current user's Application Support directory before checking the same helper shape and Service Management status. If either probe still reports `notFound`, do not add `--register`; registration can add a Login Items entry and may require local macOS approval. Add `--register` only when status moves to `notRegistered`, `requiresApproval`, or `enabled`. +For the currently working local external-helper lane, use: + +```sh +scripts/manage-external-automation-helper.sh install --check-xpc +``` + +That script installs the non-sandboxed helper as a user LaunchAgent, verifies the helper is loaded, and asks the sandboxed app to reach it over XPC. The app needs the narrow Mach lookup temporary exception for `com.galewilliams.Sirious.AutomationHelper`; `network.client` is only relevant if a future fallback replaces Mach XPC with a localhost socket transport. + Remove a retained validation copy with: ```sh diff --git a/ROADMAP.md b/ROADMAP.md index 2b1b647..5943790 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -95,6 +95,7 @@ In Progress - [x] Align the helper LaunchAgent plist with GUI-session launchd guidance by adding `LimitLoadToSessionType=Aqua` and explicit `ProgramArguments`, and capture that Service Management still reports `.notFound`. - [x] Add an article-shaped inherited sandbox helper probe and verify it works when spawned by the app, confirming that recipe is sandbox-inheriting rather than the unsandboxed Accessibility path. - [x] Add an external user LaunchAgent install path for the non-sandboxed automation helper and verify the sandboxed app can reach it over XPC with a narrow Mach lookup exception. +- [x] Capture a localhost socket fallback plan in repo docs in case the Mach lookup entitlement becomes unacceptable for a later distribution lane. - [ ] Decide whether the external user LaunchAgent helper becomes the local power-user lane, and what distribution/update/install UI should own it. ### Exit Criteria @@ -192,6 +193,7 @@ Planned - [x] Add a package-style installed-helper probe to determine whether Apple's Application Support install shape moves Service Management out of `.notFound` before registration, XPC connection, and helper Accessibility prompting. - [x] Add launchd GUI-agent compatibility keys to the bundled helper plist and verify that copied-app and package-style installed probes still report `.notFound`. - [x] Investigate why Service Management still reports `.notFound` after package-style Application Support installation verifies the app signature, HelperTools helper signature, LaunchAgent plist, and direct helper status; external LaunchAgent install works, while the bundled ServiceManagement path remains blocked. +- [x] Document the localhost socket backup transport and its extra authentication, endpoint discovery, and recovery costs. - [ ] Design the external helper install, update, uninstall, and version check surface for local distribution. - [ ] Decide which scenarios belong in a local `.xctestplan`, which should be manifest-gated, and which should remain manual supervised checks. @@ -234,3 +236,4 @@ Planned - Added launchd GUI-agent compatibility keys to the bundled helper plist and captured that the copied-app and package-style installed probes still report `.notFound`. - Added a sandbox-inherited embedded helper probe for Apple's command-line helper recipe and captured that it works only when spawned by the sandboxed app. - Added an external user LaunchAgent install path for the non-sandboxed automation helper and captured successful sandboxed-app XPC after adding the narrow Mach lookup exception. +- Added a localhost socket fallback plan as a backup if the Mach lookup entitlement becomes unacceptable for a future distribution lane.