All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
Background script timeout documentation (
CHANGELOG.md,Preferences.swift,ItemProcessing.swift, closes #79): the 4.3.0 notes and code comments saidbackground_script_timeouthad no default and that Outset waits indefinitely when it is unset. The actual default is 30 seconds; set the key to0to disable the timeout. Behaviour is unchanged — only the documentation has been corrected. -
DMG-delivered packages were never installed (
DMG.swift,Packages.swift):mountDmgreturned the entire multi-line output ofhdiutil attach(device nodes and content hints) as if it were the mount point, so the DMG's contents were never found and no packages inside it were installed. This persisted after the 4.3.0 DMG fix. The image is now attached with-plistand the mount point is read from thesystem-entitiesarray. If an image cannot be mounted, the item is logged and skipped instead of being processed with an invalid path. Image and package paths are now passed as arguments rather than interpolated into the command string, so paths containing spaces work. -
wait_for_networkenabled whenever the key existed (Preferences.swift): in the root context,wait_for_networkwas evaluated as "key is present" rather than by its value, so an explicitfalsestill enabled the network wait. Because every--bootrun (including the one performed by the installer postinstall) writes all preferences back to/Library/Preferences, the key was present on every installation and the network wait was effectively always on. Waiting unloads and reloadscom.apple.loginwindow, and a reloaded loginwindow does not perform automatic login, so installing Outset silently broke auto-login. The key's boolean value is now used, falling back to the default (false) when the key is absent or not a boolean.
-
Managed profile payload processing (
Login.swift,Payloads.swift, closes #64): three related bugs prevented scripts delivered via MDM managed profiles from running correctly when local Outset directories were empty. (1) Thelogin-privilegedtrigger was only created when the locallogin-privileged-once/everydirectories were non-empty, so privileged scripts delivered exclusively via payload were silently skipped. The condition now also checks whether the payload itself contains privileged scripts. (2)processPayloadScripts()returnedtrue(suppressing local directory fallback) even when the scripts dictionary was present but empty; it now tracks whether any script was actually dispatched. (3) Boot-once run-once tracking was written via a redundant manualwriteRunOncePlistcall afterprocessScripts(), bypassing the deduplication check insideprocessScripts; the redundant call has been removed. -
DMG package installation (
installPackage): corrected a variable assignment bug where the DMG mount path was used as the package path instead of the actual.pkg/.mpkgfile path. Also fixed a loop that overwrote a single variable on each iteration, meaning only the last package in a DMG was ever installed. Packages found inside a mounted DMG are now collected into an array and each is installed in turn. The DMG is detached synchronously after all packages have been processed (no change in behaviour —runShellCommandhas always been synchronous). -
Network timeout (
Boot.swift): the configurednetwork_timeoutpreference value was being divided by 10 before being passed towaitForNetworkUp, causing the actual wait to be ten times shorter than configured. For example, a configured value of 300 seconds would only wait 30 seconds. The value is now passed directly. -
Privileged login trigger (
Login.swift): the condition that fires thelogin-privilegedlaunchd trigger used||(OR) instead of&&(AND), meaning the trigger would only fire when both thelogin-privileged-onceandlogin-privileged-everydirectories were non-empty. The corrected condition fires the trigger when either directory has content. -
Array bounds crash (
computeChecksum): accessingfiles[0]before checking whether the array was empty could cause an index-out-of-bounds crash when called with an empty argument list. An early return guard has been added. -
Log file permissions (
writeFileLog): newly created log files were given world-writable permissions (0o666). Permissions are now0o644. -
Log file path is now context-aware (
Globals.swift,writeFileLog): Outset runs in both root context (boot, login-privileged, on-demand-privileged) and user context (login-every, login-once, on-demand). The previous single log path (/usr/local/outset/logs/outset.log) is in a root-owned directory, so user-context runs could not write to it. The log path is now determined at runtime: root context continues to log to/usr/local/outset/logs/outset.log; user-context runs log to~/Library/Logs/outset.log. Log file permissions are0o644in both cases (previously0o666). -
Force unwraps (
writeFileLog,runShellCommand): force-unwrappedString(data:encoding:)andData(string.utf8)calls have been replaced with nil-coalescing fallbacks, removing potential crash points if encoding unexpectedly fails. -
LaunchDaemon
Programkey (io.macadmins.Outset.on-demand-privileged.plist, closes #72): theProgramkey was incorrectly typed as an<array>instead of a<string>. All LaunchDaemon plists now use the correct<string>type for theProgramkey as required by launchd.
- Console user is no longer a global variable:
consoleUserwas previously stored as a mutable global, making it difficult to reason about and impossible to substitute in tests. It is now captured once at startup inOutset.run()and passed explicitly through the call chain to all functions that require it (processLoginTasks,processLoginPrivilegedTasks,processLoginEveryTasks,processLoginOnceTasks,processOnDemandTasks,processOnDemandPrivilegedTasks,processItems,processScripts,processPayloadScripts). Two additional globals that were only ever used insideBoot.swift(loginwindowState,continueFirstBoot) have been moved to local variables in that file.
-
Unit test target (
OutsetTests): a newOutsetTeststest target has been added to the Xcode project using Swift Testing (@Suite,@Test,#expect). 27 tests cover:runIfNotIgnoredUser— ignored-user skip logic and action executioncheckFileExists/checkDirectoryExists/folderContents— core file utilitiescreateTrigger/pathCleanup— trigger file creation and directory cleanupsha256/verifySHASUMForFile/computeChecksum— checksum utilities including the empty-array guardOutsetPreferences— default values,CodingKeysnames, and JSON round-trip
-
Test coverage documentation (
UntestableFunctionality.swift): a source file in the test target documents every function not covered by automated tests, with a specific reason for each exclusion (REQUIRES_ROOT,REQUIRES_FILESYSTEM_FIXTURE,REQUIRES_REAL_SHELL,REQUIRES_SYSTEM_STATE,REQUIRES_MDM,SIDE_EFFECTS_ONLY). -
Background script execution (
ItemProcessing.swift,Shell.swift,Preferences.swift): scripts whose filename begins with an underscore (e.g._my-task.sh) are now dispatched concurrently on background threads while the remaining foreground scripts continue to execute sequentially. Outset waits for all background tasks to finish before it exits, so the overall run is not complete until every script has returned. Key details:- Background scripts are dispatched before foreground scripts begin, so they start immediately.
- All log output from background scripts is tagged
[BG:pid=N]and streamed line-by-line in real time, allowing background and foreground log lines to interleave naturally. - Run-once semantics are fully supported for background scripts; the run-once record is written only on successful exit, with thread-safe access via a serial dispatch queue.
- A per-script timeout watchdog terminates a background script that exceeds the configured limit and logs an error.
- A new optional preference key
background_script_timeout(integer, seconds, default 30) sets the per-script timeout. Set it to0to wait indefinitely for background scripts to exit.
-
Ed25519 script signing (
Checksum.swift,ItemProcessing.swift,Preferences.swift,Outset.swift): scripts can now be signed with an Ed25519 private key, with the signature embedded directly in the script as a# ed25519: <base64sig>comment. When an MDM-delivered public key (manifest_signing_keypreference) is present, every script must carry a valid embedded signature — scripts without a valid signature are refused with an error log. Key details:- The signed payload is the script content with any existing
# ed25519:comment line stripped, so the signature is stable across re-signing. - The public key is delivered via MDM only (as a base64-encoded 32-byte raw Ed25519 key in
manifest_signing_key). The private key never leaves the admin workstation. - Signing is fully self-contained: scripts with embedded signatures are version-control friendly and require no separate manifest file.
- Applies to all script processing paths including MDM payload scripts (which carry their embedded signature in the base64-encoded script body).
- New CLI commands:
--generate-keypairgenerates a fresh Ed25519 keypair and prints both keys;--sign-script-file <path> --signing-key <private-key-base64>signs one or more scripts in place;--verify-script <path> --public-key <public-key-base64>verifies embedded signatures without executing the script (exits non-zero if any file fails, suitable for CI).
- The signed payload is the script content with any existing
- Initial automated build of Outset