Platform facts

The File Provider, OpenSSH and launchd facts the rest of the design rests on, each with what it forces on us. Minimum macOS is 14 Sonoma.

Minimum macOS

Everything the design needs exists on 13 (SMAppService, contentPolicy, an OpenSSH new enough for SSH_ASKPASS_REQUIRE), but File Provider behaviour differs enough between 13 and 14 that supporting 13 would mean a separate 13 VM in every release test, for a small user base. Develop and test on 14/15.

File Provider

Fact Consequence for us
A File Provider extension must ship inside an .app bundle and is loaded from Contents/PlugIns/*.appex. We need an app bundle even with no windows. It is an agent app (LSUIElement = true): no Dock icon, no menu bar, no windows.
The extension runs in its own sandboxed process, launched on demand and killed when idle. It cannot reach ~/.ssh, ssh-agent's socket, or spawn ssh. The extension does no networking. Every SSH connection, SFTP request and long-running watch stream lives in the background agent; the extension is an XPC client of it (the extension).
Each NSFileProviderDomain gets its own Finder sidebar entry (under "Locations") and is mounted under ~/Library/CloudStorage/. Domains persist across reboots. The system builds both names from the app name and displayName: the directory is <app name>-<displayName> with spaces removed, and the label is <app display name> - <displayName> (MQ-050). One domain per location, and displayName is the bare nickname. A displayName of SSH Drive - nas would mount at SSHDrive-SSHDrive-nas and read "SSH Drive - SSH Drive - nas" (the location model).
NSFileProviderReplicatedExtension (macOS 11+) keeps a system-side copy of the tree, calls us to fetch content, and marks items dataless until fetched. item(for:) is called constantly and must be answered from local state. On-demand download and offline browsing of seen folders come from the system. Items are always served from our index (item identifiers and the index), never by a network stat.
The system handles retry and backoff when we throw NSFileProviderError.serverUnreachable. It holds pending local changes and re-offers them; enumeratorForPendingItems() lists them, and the system refuses to evict an item with unsynced changes. Offline writes queue if we fail fast and correctly. "Pending uploads" in status comes from the system, not from a flag of ours.
NSFileProviderManager.evictItem(identifier:) and enumeratorForMaterializedItems() exist for the containing app. evictItem works recursively on a file, a directory and the root container (MQ-020). A refusal carries no reason: a pending upload and a kept item both return NSFileProviderErrorNonEvictable (-2008). Evicting a whole location is one call on its root container. The TTL loop still goes item by item, because a TTL is per file, and tolerates per-item failures, retrying next cycle (eviction).
Finder gives third-party domains "Download Now" and "Remove Download", one-off actions that leave the copy evictable; they are the only entries Finder contributes, and a provider's own actions land at the bottom of the same menu. Permanent "keep offline" is not a Finder feature for third parties: the extension declares it per item through contentPolicy (.downloadEagerlyAndKeepDownloaded, macOS 13+). Omitting allowsEvicting does not remove "Remove Download": the system reports the bit from isDownloaded, whatever we serve (MQ-025). The per-provider switches NSExtensionFileProviderAllowsUserControlledEviction = false and NSExtensionFileProviderAllowsContextualMenuDownloadEntry = 0 exist; we set neither. Pinning is ours to implement (pinning): store pin markers, return the eager policy for kept items (that, not the capability, refuses an eviction), keep kept subtrees polled, and skip kept items in TTL eviction.
An extension can add Finder context-menu entries (NSExtensionFileProviderActions in the appex Info.plist, handled by NSFileProviderCustomAction.performAction), each with a label and an NSPredicate activation rule over the selected items, including their userInfo. Finder's own "Remove Download" cannot be intercepted. Pin and unpin get their own entries, shown conditionally on pin state (pinning).
Items declare capabilities (allowsWriting, allowsRenaming, allowsDeleting, allowsTrashing, allowsAddingSubItems, …). Without allowsTrashing, Finder deletes immediately after a confirmation dialog. A Finder copy arrives as a plain createItem with content; there is no copy callback and no API for reporting remote free space. No trash, and permissions map to capabilities (names, permissions and attributes). Server-side copy and free space in Finder are not features we can offer.

SFTP and OpenSSH

Fact Consequence for us
SFTP has no change notifications and no stable file IDs. The SSH connection that carries it can also run commands when the account has shell access. Change detection is tiered (change detection): SFTP polling always works; an exec channel unlocks a remote find sweep or our own helper. We keep our own path → identifier index (item identifiers and the index).
Servers differ in which SFTP extensions they offer (posix-rename, fsync, statvfs, limits) and in whether exec is allowed. Every server-dependent feature has a fallback, and sshdrive status shows which tier each feature runs at and what would upgrade it (the CLI).
SFTP v3 has nine status codes, and OpenSSH folds errno onto them through a fixed table: ENOENT, ENOTDIR, ELOOP → NO_SUCH_FILE; EPERM, EACCES → PERMISSION_DENIED; everything else (ENOSPC, EDQUOT, EEXIST, ENOTEMPTY, EXDEV included) → FAILURE with the literal message "Failure". The agent never learns why a request failed beyond those classes. A collision is confirmed with an lstat, a full disk is inferred from statvfs@openssh.com, and no error mapping on these pages assumes an errno the wire cannot carry (the SFTP client).
macOS ships OpenSSH (9.x on macOS 14+). ssh -s host sftp opens the SFTP subsystem on stdio, ControlMaster multiplexes further channels over one connection, and SSH_ASKPASS_REQUIRE=force (OpenSSH ≥ 8.4) routes every prompt, host-key confirmations and user-presence notices included, to a program of ours with no tty, tagging it with SSH_ASKPASS_PROMPT (confirm for yes/no questions, none for notifications, unset for secrets). The transport is the system's /usr/bin/ssh, spawned by absolute path, never through PATH (SSH process management). We implement the SFTP wire protocol ourselves (the SFTP client): it is small and gives us every extension and request type. Our askpass handles all three prompt kinds (secrets).
ssh builds ProxyJump hops itself, as <argv[0]> -W '[%h]:%p' … <jump>: the hop reads the config files but gets none of the parent's -o options, and when argv[0] is not an executable path the hop is found through PATH. The agent never lets ssh build the chain. A proxyjump from ssh -G becomes a ProxyCommand of the agent's own with the same overrides on every hop, and every ssh is spawned with argv[0] set to /usr/bin/ssh (SSH process management).
A mux client asked to open a session (ssh -S <socket> … <host> <command>) does not fail on a missing socket: it logs that at debug level and connects directly, reading the config files as usual. Only the -O control commands fail on a missing socket. Mux clients never read a config file, never prompt and cannot connect on their own: they run with -F /dev/null, BatchMode=yes and ProxyCommand=/usr/bin/false, so a lost master shows up as an immediate, recognisable exit rather than a second, unsupervised connection (SSH process management).

launchd, privacy and signing

Fact Consequence for us
A launchd agent does not get the user's shell environment: PATH is the system default and SSH_AUTH_SOCK is the system ssh-agent's. An export SSH_AUTH_SOCK=… for 1Password in .zshrc, or a ProxyCommand calling a Homebrew tool, is invisible to it. The agent takes PATH and SSH_AUTH_SOCK from a snapshot of the user's login shell (SSH process management), and add verifies the location through the agent, never from the terminal's environment (secrets).
A launchd agent that dials a local-network address draws the Local Network privacy prompt, in the app's name (MQ-069). A NAS on the user's network raises it on first connect. No entitlement suppresses it and a launchd agent has no window to put it over, so the install docs say to expect it (packaging and install).
keychain-access-groups is a restricted entitlement: it needs a provisioning profile, and only a bundle can embed one. A bare executable in Contents/MacOS cannot. The agent, as the bundle's main executable, is the only process that touches the keychain. The CLI and askpass hold no secrets and no restricted entitlements; they are XPC clients (components and identifiers, secrets).
App groups, keychain sharing and File Provider entitlements require a real Team ID; installs outside the App Store need Developer ID and notarization. SMAppService.agent registers a login agent from the calling app's own bundle. Team RWGDZAYBM8 is in place, and every identifier derives from it and org.shirls (components and identifiers). The app registers itself when launched once (packaging and install).