# How to Use the Plugin Architecture for Extensible Runtime in apple/container

> Learn how to use the plugin architecture in apple/container to build extensible runtimes. Discover, register, and expose extension capabilities via XPC without daemon restarts.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: how-to-guide
- Published: 2026-06-24

---

**The apple/container repository provides a plugin system that discovers extensions placed in the user-plugins directory, automatically registers them with launchd, and exposes their capabilities via XPC services without requiring daemon restarts.**

This guide explains how to leverage the **plugin architecture for extensible runtime** in apple/container to add custom runtime, network, core, or auxiliary services. The system allows you to drop bundles into a designated directory and have them automatically loaded by the daemon, making it ideal for extending container functionality without recompiling the core engine.

## Core Concepts of the Plugin System

The plugin implementation is contained within the `ContainerPlugin` module and consists of five primary abstractions that work together to enable dynamic extension.

### Plugin

The `Plugin` struct in [`Sources/ContainerPlugin/Plugin.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/Plugin.swift) is the value type representing a discovered extension. It holds the binary URL, an optional `PluginConfig` instance, and resource paths. This type provides helper methods to compute **launchd labels**, generate **Mach-service names**, and launch the binary process.

### PluginConfig

Defined in [`Sources/ContainerPlugin/PluginConfig.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginConfig.swift), this `Codable` struct mirrors the [`config.toml`](https://github.com/apple/container/blob/main/config.toml) or [`config.json`](https://github.com/apple/container/blob/main/config.json) file found in each plugin directory. It captures metadata such as the abstract description and author, plus a critical `ServicesConfig` block that declares which service types the plugin publishes (runtime, network, core, or auxiliary).

### PluginFactory

The `PluginFactory` protocol in [`Sources/ContainerPlugin/PluginFactory.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginFactory.swift) abstracts the discovery logic for different bundle formats. The repository ships with two concrete implementations:

- **DefaultPluginFactory**: Expects a plain folder containing a `bin/<name>` executable.
- **AppBundlePluginFactory**: Handles macOS-style `.app` bundles.

### PluginLoader

Located in [`Sources/ContainerPlugin/PluginLoader.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginLoader.swift), this orchestrator handles the full lifecycle. It scans directories via `findPlugins()`, performs lookups via `findPlugin()`, and manages launchd registration through `registerWithLaunchd()`. The loader also creates filtered environment variables and generates launchd plists under `<app-root>/plugin-state/<name>/service.plist`.

### PluginsHarness

The `PluginsHarness` class in [`Sources/Services/ContainerAPIService/Server/Plugin/PluginsHarness.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Server/Plugin/PluginsHarness.swift) serves as the runtime holder used by the API server to maintain a collection of active plugins and route incoming requests to the appropriate extension.

## Plugin Discovery Mechanism

When the daemon initializes, the `PluginLoader` executes a multi-stage discovery process:

1. **Directory scanning**: `PluginLoader.findPlugins()` walks every path supplied in `pluginDirectories`, defaulting to `<install-root>/libexec/container-plugins`.
2. **Factory resolution**: For each subdirectory, the loader attempts instantiation through registered factories, trying `DefaultPluginFactory` first, then `AppBundlePluginFactory`.
3. **Validation**: The first factory that successfully produces a `Plugin` wins; if none succeed, the directory is skipped and a warning is logged.
4. **Deduplication**: Shadowed plugins (same name appearing earlier in the search order) are automatically skipped to prevent conflicts.

## Launchd Registration and XPC Services

When a plugin advertises services via `servicesConfig`, the loader invokes `registerWithLaunchd` to create a launchd job:

- **Label**: Automatically generated as `com.apple.container.<name>` (or `…/<instanceId>` for multi-instance plugins).
- **Arguments**: Includes the binary path, optional `--resources <path>`, optional `--debug` flag, and any `servicesConfig.defaultArguments`.
- **Mach services**: Generated by `Plugin.getMachServices`, producing entries like `com.apple.container.runtime.<name>` for runtime plugins.

The generated plist is written to `<app-root>/plugin-state/<name>/service.plist` and handed to `ServiceManager.register`, which communicates directly with launchd to establish the XPC endpoint.

## Implementing a Custom Runtime Plugin

To extend the container runtime with a custom plugin, follow this directory structure and configuration:

### Filesystem Layout

Place your plugin under the user-plugins directory:

```

/usr/local/libexec/container-plugins/my-runtime/
├── bin/
│   └── my-runtime                # executable binary

└── config.toml

```

### Configuration File

Create a [`config.toml`](https://github.com/apple/container/blob/main/config.toml) that declares the runtime service:

```toml
abstract = "My custom runtime"
author = "Acme Corp"

[servicesConfig]
loadAtBoot = true
runAtLoad = true
defaultArguments = []

[[servicesConfig.services]]
type = "runtime"
description = "XPC API for a single container"

```

When the daemon restarts, it discovers the plugin, creates the launchd job, and exposes the XPC API at `com.apple.container.runtime.my-runtime`.

## Loading Plugins Programmatically

You can interact with the plugin system directly from Swift code to manually load and register extensions:

```swift
import ContainerPlugin
import Logging

let logger = Logger(label: "example")
let loader = try PluginLoader(
    appRoot: URL(fileURLWithPath: "/Applications/Container"),
    installRoot: URL(fileURLWithPath: "/usr/local"),
    logRoot: nil,
    pluginDirectories: [PluginLoader.userPluginsDir(installRoot: URL(fileURLWithPath: "/usr/local"))],
    pluginFactories: [DefaultPluginFactory(logger: logger), AppBundlePluginFactory(logger: logger)],
    log: logger)

let plugins = loader.findPlugins()          // Discovers all valid plugins
let runtime = plugins.first { $0.hasType(.runtime) }

if let runtime = runtime {
    try loader.registerWithLaunchd(plugin: runtime)   // Creates launchd job
}

```

To connect to the plugin's XPC service from a client:

```swift
import XPC
import Foundation

let serviceName = "com.apple.container.runtime.my-runtime"
let connection = xpc_connection_create_mach_service(serviceName, nil, UInt32(XPC_CONNECTION_MACH_SERVICE_PRIVILEGED))
xpc_connection_set_event_handler(connection) { event in
    // Handle reply and errors
}
xpc_connection_resume(connection)

let request = xpc_dictionary_create(nil, nil, 0)
xpc_dictionary_set_uint64(request, "operation", 1)
xpc_connection_send_message_with_reply(connection, request, DispatchQueue.main) { reply in
    // Process reply
}

```

## Advanced: Custom Plugin Factories

For non-standard bundle layouts, implement the `PluginFactory` protocol:

```swift
struct MySpecialFactory: PluginFactory {
    func create(installURL: URL) throws -> Plugin? {
        // Custom discovery logic for installURL
        return nil
    }
    
    func create(parentURL: URL, name: String) throws -> Plugin? {
        // Alternative creation path
        return nil
    }
}

```

Register your custom factory when constructing the `PluginLoader`:

```swift
let loader = try PluginLoader(
    appRoot: appRoot,
    installRoot: installRoot,
    logRoot: nil,
    pluginDirectories: pluginDirs,
    pluginFactories: [
        DefaultPluginFactory(logger: logger),
        AppBundlePluginFactory(logger: logger),
        MySpecialFactory()
    ],
    log: logger)

```

## Summary

- **Plugin discovery** is handled by `PluginLoader.findPlugins()` in [`Sources/ContainerPlugin/PluginLoader.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginLoader.swift), which scans directories and delegates to registered factories.
- **Configuration** uses the `PluginConfig` struct to parse [`config.toml`](https://github.com/apple/container/blob/main/config.toml) files, supporting runtime, network, core, and auxiliary service types.
- **Registration** occurs automatically via `registerWithLaunchd()`, which generates launchd plists and Mach service names based on the plugin configuration.
- **Extensibility** requires no daemon recompilation; simply place a binary and config file in the user-plugins directory and restart the daemon.
- **Customization** is possible by implementing the `PluginFactory` protocol to support alternative bundle formats beyond the default directory or `.app` structures.

## Frequently Asked Questions

### Where does the daemon look for plugins?

By default, the daemon scans `<install-root>/libexec/container-plugins` (the `userPluginsDir`). You can specify additional paths by passing custom URLs to the `PluginLoader` initializer. The `PluginLoader` class searches these directories in order, with earlier paths taking precedence in cases of name collisions.

### What service types can a plugin declare?

According to the `PluginConfig` implementation in [`Sources/ContainerPlugin/PluginConfig.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginConfig.swift), a plugin can declare services of type **runtime**, **network**, **core**, or **auxiliary**. Each type determines the Mach service name prefix and how the `PluginsHarness` routes requests to the plugin.

### Do I need to restart the daemon after adding a plugin?

Yes. The `PluginLoader` performs discovery during initialization. While the system supports dynamic registration with launchd via `registerWithLaunchd()`, the initial discovery phase that populates the plugin registry requires a daemon restart to recognize new bundles in the filesystem.

### How do I debug a plugin that fails to load?

Check the daemon logs for warnings emitted during `findPlugins()`. Common failures include missing [`config.toml`](https://github.com/apple/container/blob/main/config.toml) files, binaries not found at the expected `bin/<name>` path, or factory validation errors. Enable the `--debug` argument in your `servicesConfig.defaultArguments` to receive verbose output from the plugin process once it is registered with launchd.