How the XPC Communication Layer Functions Between the Container CLI and Container-apiserver

The XPC communication layer in apple/container uses a type-safe Swift wrapper around macOS Mach services to enable asynchronous, authenticated RPC between the CLI and container-apiserver via the XPCMessage, XPCClient, and XPCServer types.

The apple/container project implements a secure XPC (Cross-Process Communication) architecture to bridge the Container CLI and the container-apiserver daemon on macOS. This layer provides asynchronous request/response handling with built-in caller authentication, thread-safe message encoding, and a pluggable routing table for dispatching container operations.

XPC Architecture Components

The communication stack isolates low-level XPC details behind Swift-native APIs defined in four primary files:

Message Protocol and Format

All traffic traverses the wire as dictionary-valued XPC objects with two reserved keys defined in XPCMessage:

public static let routeKey = "com.apple.container.xpc.route"
public static let errorKey = "com.apple.container.xpc.error"

The route value is a string identifying the RPC endpoint (e.g., "container.create"), while the error key contains a JSON-encoded ContainerXPCError when the server encounters failures. XPCMessage provides typed helpers for extracting and setting values (String, Bool, UInt64, Date, FileHandle) while holding a lock on the underlying xpc_object_t to guarantee thread safety.

Client-Side Flow

Creating Connections and Sessions

The CLI initiates communication by constructing an XPCClient with the service identifier that matches the server’s Mach service:

let client = XPCClient(service: "com.apple.container.apiserver")

For long-running commands, the CLI opens a persistent session through XPCClientSession, which registers a disconnect handler to invoke callbacks if the server terminates unexpectedly:

let session = client.openSession()
session.onDisconnect {
    print("Server disconnected")
}

Building and Sending Requests

Requests are constructed as XPCMessage instances with a route and payload:

var request = XPCMessage(route: "container.create")
request.set(key: "name", value: "my-container")
request.set(key: "image", value: "ubuntu:latest")

The client transmits the message using xpc_connection_send_message_with_reply under the hood, returning an XPCMessage representing the server’s response:

let reply = try await client.send(request, responseTimeout: .seconds(30))

Errors are propagated as ContainerizationError if the server sets the error key or if the underlying XPC connection fails.

Server-Side Flow

Starting the Listener

The container-apiserver instantiates XPCServer with its identifier and a routing table mapping route strings to async handlers:

let server = XPCServer(
    identifier: "com.apple.container.apiserver",
    routes: [
        "container.create": XPCServer.route(handleCreate)
    ],
    log: logger
)
try await server.listen()

The listen() method creates an AsyncStream of incoming xpc_connection_t objects, spawning tasks to handle each connection via handleClientConnection.

Authenticating Callers

Before dispatching any request, the server extracts the audit token from the incoming dictionary using xpc_dictionary_get_audit_token and verifies that the client’s effective UID matches the server’s UID. Mismatches result in an immediate ContainerizationError(.invalidState) reply, preventing unauthorized cross-user access.

Route Dispatch and Error Handling

The server extracts the route string using XPCMessage.routeKey and looks up the corresponding handler in its routes dictionary. If found, it wraps the raw XPC object in an XPCMessage and invokes:

let response = try await handler(message, session)

The server sends the reply via xpc_connection_send_message. Thrown ContainerizationError instances are caught and converted into standard error replies via replyWithError, while unexpected errors are wrapped in ContainerizationError(.unknown) before transmission.

Practical Code Examples

Creating a Container from the CLI

import ContainerXPC

let client = XPCClient(service: "com.apple.container.apiserver")
let request = XPCMessage(route: "container.create")
request.set(key: "name", value: "demo")
request.set(key: "image", value: "alpine:latest")

do {
    let reply = try await client.send(request, responseTimeout: .seconds(10))
    if let uuid = reply.string(key: "uuid") {
        print("Container created: \(uuid)")
    }
} catch {
    print("Failed: \(error)")
}

Registering a Handler on the Server

import ContainerXPC
import Logging

func handleCreate(_ request: XPCMessage, _ session: XPCServerSession) async throws -> XPCMessage {
    guard let name = request.string(key: "name"),
          let image = request.string(key: "image") else {
        throw ContainerizationError(.invalidArgument, message: "missing name or image")
    }
    
    let containerID = UUID().uuidString
    var response = XPCMessage(route: request.string(key: XPCMessage.routeKey) ?? "")
    response.set(key: "uuid", value: containerID)
    return response
}

let logger = Logger(label: "container.apiserver")
let server = XPCServer(
    identifier: "com.apple.container.apiserver",
    routes: ["container.create": XPCServer.route(handleCreate)],
    log: logger
)
try await server.listen()

Handling Disconnects

Client-side cleanup uses the session’s disconnect callback:

let session = client.openSession()
session.onDisconnect {
    print("Server went away – cleaning up local state")
}

Summary

  • The XPC layer uses XPCMessage as a type-safe wrapper around raw xpc_object_t dictionaries with reserved keys for routing and errors.
  • XPCClient and XPCClientSession in Sources/ContainerXPC/XPCClient.swift manage Mach service connections, timeouts, and disconnect handling for the CLI.
  • XPCServer in Sources/ContainerXPC/XPCServer.swift authenticates callers via xpc_dictionary_get_audit_token and UID verification before dispatching to pluggable route handlers.
  • Communication is asynchronous, using xpc_connection_send_message_with_reply and AsyncStream to handle concurrent connections with automatic error propagation as ContainerizationError.

Frequently Asked Questions

How does the XPC layer authenticate clients?

The server extracts the audit token from each incoming XPC dictionary using xpc_dictionary_get_audit_token and validates that the client’s effective UID matches the server’s UID. If the UIDs differ, the server immediately returns a ContainerizationError(.invalidState) without executing the requested route.

What happens if the container-apiserver disconnects during a CLI operation?

The XPCClientSession type registers a disconnect handler with the underlying XPC connection. When the server closes the connection or crashes, the handler fires, invoking any callbacks registered via session.onDisconnect, allowing the CLI to clean up local state and fail gracefully.

Can the CLI use short-lived connections instead of persistent sessions?

Yes. While the CLI typically uses XPCClientSession for long-running commands, you can send one-off requests directly through XPCClient.send(request:) without calling openSession(). This creates a transient connection that closes automatically after the reply or timeout.

How are server-side errors transmitted back to the CLI?

When a handler throws a ContainerizationError, the server catches it in XPCServer and encodes it into the reply dictionary under the com.apple.container.xpc.error key as a JSON-encoded ContainerXPCError. The client-side XPCClient checks for this key when parsing the reply and re-throws the error as a Swift exception.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →