This is just a super quick post and some notes, about my experiences with SMAppService.

Apple introduced the SMAppService API in macOS Ventura (13) to replace the older SMJobBless and SMLoginItemSetEnabled APIs. SMAppService should be used now to register any new Login Item, Launch Agent or Daemon.

The API is super easy to use, even I could learn it from the developer docs, which is a big thing, and it means that it is indeed really easy as I suck as a developer.

For example registering a new launch daemon is simple as is:

 SMAppService* service = [SMAppService daemonServiceWithPlistName:@"mysuperherodaemon.plist"];
    
 [service registerAndReturnError:NULL];

I remember how painful it was to use SMJobBless with doing all the authorization, etc… now that is abstracted away, and the system will take care of that on our behalf.

When we call the above API, the users will have to approve a background item first, and if they do so they will be prompted for authentication, as installing a daemon requires root privileges.

Querying the status from our app is also super simple.

SMAppService* service = [SMAppService daemonServiceWithPlistName:HELPER_PLIST];
if(service.status == SMAppServiceStatusNotRegistered) {
    self.helperStatus.stringValue = @"The service hasn’t registered with the Service Management framework, or the service attempted to reregister after it was already registered.";
}
else if (service.status == SMAppServiceStatusEnabled) {
    self.helperStatus.stringValue = @"The service has been successfully registered and is eligible to run.";
}
else if(service.status == SMAppServiceStatusRequiresApproval) {
    self.helperStatus.stringValue = @"The service has been successfully registered, but the user needs to take action in System Preferences.";
}
else if(service.status == SMAppServiceStatusNotFound) {
    self.helperStatus.stringValue = @"An error occurred and the framework couldn’t find this service.";
}

Once the user approved it, we can verify that indeed it was installed. We can either use launchctl as shown below.

csaby@max ~ % sudo launchctl list | grep Script
-	0	com.csabafitzl.ScriptRunner.helper

Or we can use sfltool to dump the BTM (BackgroundTaskManagement) database. Its output is shown below.

csaby@max ~ % sudo sfltool dumpbtm 
========================
 Records for UID -2 : FFFFEEEE-DDDD-CCCC-BBBB-AAAAFFFFFFFE
========================

 #17:
                 UUID: B6DF59C7-508F-4057-B529-0C0F64BBC6A4
                 Name: ScriptRunner
       Developer Name: Csaba Fitzl
      Team Identifier: 33YRLYRBYV
                 Type: app (0x2)
          Disposition: [disabled, allowed, visible, notified] (10)
           Identifier: identifier "com.csabafitzl.ScriptRunner" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Csaba Fitzl (RQGUDM4LR2)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */
                  URL: file:///Applications/ScriptRunner.app/
           Generation: 8
    Bundle Identifier: com.csabafitzl.ScriptRunner
  Embedded Item Identifiers:
    #1: com.csabafitzl.ScriptRunner.helper

 #18:
                 UUID: AA964DA1-9116-4D15-AF27-44848A030133
                 Name: com.csabafitzl.ScriptRunner.helper
       Developer Name: (null)
                 Type: daemon (0x10)
          Disposition: [enabled, allowed, visible, notified] (11)
           Identifier: com.csabafitzl.ScriptRunner.helper
                  URL: Contents/Library/LaunchDaemons/com.csabafitzl.ScriptRunner.helper.plist
      Executable Path: Contents/Resources/com.csabafitzl.ScriptRunner.helper
           Generation: 21
    Parent Identifier: identifier "com.csabafitzl.ScriptRunner" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Csaba Fitzl (RQGUDM4LR2)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */

========================
 Records for UID 501 : 682D46F7-0B3D-4439-9602-F02250338E0D
========================

 #21:
                 UUID: B6DF59C7-508F-4057-B529-0C0F64BBC6A4
                 Name: ScriptRunner
       Developer Name: Csaba Fitzl
      Team Identifier: 33YRLYRBYV
                 Type: app (0x2)
          Disposition: [disabled, allowed, visible, notified] (10)
           Identifier: identifier "com.csabafitzl.ScriptRunner" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Csaba Fitzl (RQGUDM4LR2)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */
                  URL: file:///Applications/ScriptRunner.app/
           Generation: 3
    Bundle Identifier: com.csabafitzl.ScriptRunner

I think it worth a closer look. We will find that we have entries for the main app for both my regular user and the root, which are disabled. We also find an entry for the launch daemon for the root user, which is enabled. You might notice that the PLIST and executable path is relative. This is because from now on launch daemons and their associated plist files are expected to be within the application bundle itself, and they won’t get moved to /Library/Launch* and /Library/PrivilegedHelperTools/. This is very important as security tools should expect looking for persistence also inside the app bundles.

Now if we unregister the daemon, the daemon’s status will be changed to disabled.

                 UUID: AA964DA1-9116-4D15-AF27-44848A030133
                 Name: com.csabafitzl.ScriptRunner.helper
       Developer Name: (null)
                 Type: daemon (0x10)
          Disposition: [disabled, allowed, visible, notified] (10)
           Identifier: com.csabafitzl.ScriptRunner.helper
                  URL: Contents/Library/LaunchDaemons/com.csabafitzl.ScriptRunner.helper.plist
      Executable Path: Contents/Resources/com.csabafitzl.ScriptRunner.helper
           Generation: 22
    Parent Identifier: identifier "com.csabafitzl.ScriptRunner" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Csaba Fitzl (RQGUDM4LR2)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */

It will also go away from launchd’s list.

csaby@max ~ % sudo launchctl list | grep Script 
csaby@max ~ % 

Interestingly, if the daemon was approved once, unregistering and registering it again doesn’t require further authentication.

If we enable it bask, and now go to System Settings and disable it from there we will find the following.

 #18:
                 UUID: AA964DA1-9116-4D15-AF27-44848A030133
                 Name: com.csabafitzl.ScriptRunner.helper
       Developer Name: (null)
                 Type: daemon (0x10)
          Disposition: [enabled, disallowed, visible, notified] (9)
           Identifier: com.csabafitzl.ScriptRunner.helper
                  URL: Contents/Library/LaunchDaemons/com.csabafitzl.ScriptRunner.helper.plist
      Executable Path: Contents/Resources/com.csabafitzl.ScriptRunner.helper
           Generation: 26
    Parent Identifier: identifier "com.csabafitzl.ScriptRunner" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Csaba Fitzl (RQGUDM4LR2)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */

It will show as “enabled” but at the same time “disallowed”. It will also go away from launchd.

There is a bug in macOS Ventura 13.6, the launchd job won’t be disable even if the user disables the service. I submitted a bug to Apple, the Feedback ID is FB13206906.

The Service Management developers docs are available here: SMAppService | Apple Developer Documentation

At the time of this writing none of my apps, which has a registered daemon appears to use this API as their daemons are still in the old (now legacy) location and not inside the bundle.