Introduction
Since there are multiple topics (1, 2) where people have stated interest in network access for Swift Package Manager Plugins, I wanted to bring them together and draft a first pitch for a discussion.
Motivation
In the aforementioned discussions we have seen multiple projects which had to rely on workarounds or do not function at all without explicit network permission.
- tevelee is distributing modular binary xcframeworks to clients. For inter-module communication they rely on SPIs, where private swiftinterface files are stored in a remote artifact storage. They'd like to retrieve these SPIs as part of the build process of their package.
- 0xTim is deploying swift apps to AWS/GCP/Heroku with a Swift Plugin.
- hassila mentioned wanting to download static resources for processing from the internet as part of the build process.
- joshw came up with the idea to pull an OpenAPI specification and generate Swift types for it using a plugin.
- Personally, I created a Kotlin Multiplatform plugin for SPM so that it would run gradle and assemble the xcframework as part of the package building process. This is useful because a) it allows for code sharing between Android and iOS (business logic, local data storage, network access) and b) increases developer ergonomy for Swift developers relying on shared code, since they can now build the entire stack from Xcode. However, gradle requires to access the internet to pull binaries and artifacts.
Some of these use-cases were possible using Xcode 14, if you employed a .prebuildCommand
instead of a regular .buildCommand
. Apparently the sandbox was not 100 % airtight, which has (unfortunately) been fixed with Xcode 15.
Proposal
Extension of SwiftPM Plugin Definition
Swift Package Manager already offers an API to explicitly specify plugin permissions for .command
plugins, we could reuse this pattern extending it to .buildTool()
.
case command(
intent: PluginCommandIntent,
permissions: [PluginPermission] = []
)
// new buildTool
case buildTool(
intent: PluginBuildIntent,
permissions: [PluginBuildToolPermission] = []
)
I am proposing to explicitly duplicate PluginCommandIntent
and PluginPermission
, retaining the original names for compatibility reasons. Through duplication we can allow for the permissions and intents to diverge, which may be the case given the different use cases for command and build tool plugins.
enum PluginBuildIntent {
case documentationGeneration
case sourceCodeFormatting
case custom(description: String)
}
The PluginBuildToolPermission
features two permissions, which I perceived as desirable from the initial discussions. The access to the package folder is explicitly excluded here, since build commands already have a way of accessing the package folder indirectly through build outputs. The allowNetworkConnections
permission relies on the original PluginNetworkPermissionScope
which I found handy to reuse here. It already offers a great way to describe network access with convenience functions, that in my opinion also fit in nicely here.
enum PluginBuildToolPermission {
case allowNetworkConnections(
scope: PluginNetworkPermissionScope,
reason: String
)
case allowFileSystemAccess(
scope: PluginFileSystemPermissionScope,
reason: String
)
}
The PluginFileSystemPermissionScope
offers a granular way to specify access to a specific path and a read or readWrite permission. Additionally, we may think of an explicit file execution permission.
struct PluginFileSystemPermissionScope {
/// The path that permissions are requested for.
let path: Path
/// Specifies the permissions requested for the path.
let permission: Permission
}
extension PluginFileSystemPermissionScope {
enum Permission {
case read
case readWrite
}
}
The extension provides quick access to default folders and a way to add custom paths.
extension PluginFileSystemPermissionScope {
/// Requests permissions for the user's home folder.
static func userHome(
permission: Permission
) -> PluginFileSystemPermissionScope { }
/// Requests permissions for a custom path.
static func path(
_ path: Path,
permission: Permission
) -> PluginFileSystemPermissionScope { }
// Potentially add more default folders.
}
SwiftPM integration
From my perspective, SwiftPM's Sandbox
already offers everything we need to configure a sandbox according to the proposed design for build commands.
enum Sandbox {
public static func apply(
command: [String],
fileSystem: FileSystem,
strictness: Strictness = .default,
writableDirectories: [AbsolutePath] = [],
readOnlyDirectories: [AbsolutePath] = [],
allowNetworkConnections: [SandboxNetworkPermission] = []
)
}
The implementation effort should be limited to constructing the public api and making use of existing functionality to compose the correct function call to the Sandbox.apply
function.
Xcode integration
Xcode already asks for permission when running custom user command plugins. Xcode may reuse this pattern and ask for permission listing all requested permissions when the user first runs the plugin as part of a build started.
Backwards compatibility
Since these changes are likely to be introduced with a new swift tools version, paired with a new version of Xcode, there are no additional requirements to ensure backwards compatibility.
Open discussion points
Design with multiple PluginFileSystemPermissionScopes
I am not entirely confident in the design of PluginFileSystemPermissionScope
. While it roughly resembles its cousin PluginNetworkPermissionScope
, it makes for awkward syntax in the permissions: [PluginBuildToolPermission]
array if a plugin requests access to more than one folder:
let package = Package(
name: "AssetMover",
products: [.plugin(name: "AssetMover", targets: ["AssetMover"])],
targets: [
.plugin(
name: "AssetMover",
capability: .buildTool(
intent: .custom(description: "AssetMover moves xcasset catalogs"),
permissions: [
.allowFileSystemAccess(
scope: .userHome(permission: .read),
reason: "Read asset files in the home directory"
),
.allowFileSystemAccess(
scope: .path("/tmp/", permission: .readWrite),
reason: "Write asset files in the temp directory"
),
]
)
),
]
)
Alternatively, one could think about combining scopes of different paths, however the reason would have to move along with the path or be stated as one reason for all scopes.
File execution permission
Adding to the PluginFileSystemPermissionScope
permissions, we may want to add an additional execution permission for folders outside the sandbox to further elevate security. Since permissions are now no longer mutually exclusive, we may have to convert the enum to an OptionSet
struct.
extension PluginFileSystemPermissionScope {
struct Permission: OptionSet {
let rawValue: Int
/// Read only permission.
static let read: Permission
/// Write permission. Implicitly grants read as well.
static let write: Permission
/// Execution permission.
static let execute: Permission
}
}
Predefined values
The predefined values in PluginFileSystemPermissionScope
and PluginBuildToolIntent
are merely examples. If this discussion results in an SE proposal, we will have to compile a list of convenience functions and predefined values.
Permissions
Are file system access and network access permissions everything we require as of now? Are there other permissions you can think of?
Known workarounds
Disabling the sandbox
According to this post, you can disable the sandbox entirely by setting the appropriate Xcode defaults property.
defaults write com.apple.dt.Xcode IDEPackageSupportDisablePluginExecutionSandbox -bool YES
For obvious reasons, this cannot be a desirable solution for the above motivation.
Invoking a shortcut
You can create a shortcut in the shortcuts app that does the desired operation for you and invoke the shortcut from within the plugin execution through shortcuts run
and provide appropriate parameters. You may have to work around file system restrictions of the sandbox to retrieve the shortcut execution's results.
However, this solution will only work on computers with the respective shortcut installed. Shortcut distribution and manual setup is a prerequisite for this.