Skip to content
Pace
Esc
navigateopen⌘Jpreview
On this page

Infra & patterns — pace

Infra & patterns — pace

Cross-cutting code mechanisms that back Pace’s privacy and reliability claims: how a URL gets proven local, how a file write survives a crash, how third-party tools get bridged in, how the app updates and stores secrets, and how the menu-bar-only shell and Swift 6 concurrency hold together. Concepts with an external spec (loopback guard, MCP protocol) are explained once in new-things.md — this page documents the code that enforces them.


Local endpoint guard

  • What: A fail-closed validator that only lets a configured URL through if its host is localhost, 127.0.0.0/8, or ::1 — anything else is rejected and the caller falls back to a hardcoded loopback default.
  • Why here: The single choke point that makes “0 bytes off this Mac” true in code, not just in docs — every planner/VLM/TTS sidecar URL flows through it before a request is ever sent.
  • Where: PaceLocalEndpointGuard.swiftPaceLocalEndpointGuard.resolvedLocalOpenAICompatibleBaseURL / isLoopbackHost. Two sibling validators intentionally live in the same file with different rules: validatedCloudBridgeURL (still loopback-only) and validatedDirectAPIURL (allows https to any host for consented BYO-key egress, but refuses plaintext http to a non-loopback host).
  • Source: internal — no external spec; concept explained in new-things.md.

Atomic temp-file + rename persistence

  • What: Writing a new file version to a temp path in the same directory, then atomically renaming/replacing it over the real file, so a crash mid-write can never leave a half-written or corrupted file on disk.
  • Why here: Every piece of durable local state Pace owns (flows, thread memory, MCP config, meeting-note profiles) uses this pattern instead of an in-place write(to:) — the app has no server-side backup to recover from a truncated file.
  • Where: PaceFlowStore.swiftsave(_:) writes to a sibling temp file (FileManager.default.temporaryDirectory-adjacent) then calls FileManager.default.replaceItemAt(_:withItemAt:), falling back to moveItem when there’s nothing to replace. PaceThreadMemoryStore.swift uses the simpler one-line form, data.write(to:options:.atomic), which does the same temp-file-then-rename under the hood on the same volume. PaceMCPCatalogInstaller.swift (below) is the same pattern a third time, for JSON merges.
  • Source: standard Unix atomic-write pattern (internal usage — no single canonical spec).

MCP stdio bridge

  • What: The concrete code that spawns a configured MCP server as a subprocess, speaks newline-delimited JSON-RPC to it (initializenotifications/initializedtools/call), and returns the result as a string observation.
  • Why here: How Pace’s approval/observation loop stays agnostic to which third-party tool actually ran — PaceMCPStdioClient.callTool is the one place a planner tool call becomes a real subprocess request.
  • Where: PaceMCPClient.swiftPaceMCPStdioClient.callTool, PaceMCPServerRegistry.loadConfiguredServers (reads ~/.config/pace/mcp-servers.json or ~/.pace/mcp-servers.json, accepting either a servers or mcpServers root key). PaceMCPClientEnvironmentBuilder.buildSpawnEnvironment layers a stored Keychain secret over an empty-string env sentinel at spawn time, so a server like Composio never needs its API key written into the JSON config file.
  • Source: MCP protocol → pointer to new-things.md.

MCP catalog + installer

  • What: A fixed list of ready-made MCP server entries the Settings UI can install with one click, plus a pure-file-I/O installer that merges a new entry into mcp-servers.json without disturbing anything already there.
  • Why here: Removes the “hand-edit a JSON file” barrier for the common integrations users actually ask for, while keeping the merge safe for users who’ve already customized the file by hand.
  • Where: PaceMCPServerCatalog.swiftPaceMCPServerCatalog.bundledCatalog (currently four entries: filesystem, fetch, applescript, composio — github/slack/linear were retired in favor of the Composio OAuth bridge, tracked in PaceMCPServerCatalog.supersededBySlug) and PaceMCPCatalogInstaller.install/uninstall, which decode the existing config, merge in one key, and call the same atomic temp-file + rename write described above (atomicallyWriteMCPServers).
  • Source: internal — no external spec.

Sparkle auto-updates

  • What: Third-party Swift framework (Sparkle) that checks a hosted “appcast” XML feed for new versions, verifies each update’s authenticity with an EdDSA signature, and drives the download/install UI.
  • Why here: Pace ships outside the Mac App Store, so Sparkle is the update channel — no notarization-gated App Store review loop, but still a signed, verified update path rather than an unsigned binary swap.
  • Where: PaceAutoUpdateController.swiftPaceAutoUpdateController wraps SPUStandardUpdaterController(startingUpdater: true, ...), constructed lazily by CompanionAppDelegate at launch. Info.plist supplies SUFeedURL (a GitHub-hosted appcast.xml) and SUPublicEDKey (the public half of the release-machine-held signing key); the private key never ships in the app.
  • Source: https://sparkle-project.org/

Keychain storage for API keys

  • What: Apple’s Security framework API (kSecClassGenericPassword, SecItemAdd/SecItemCopyMatching) for storing small secrets in the macOS encrypted keychain instead of a plaintext file.
  • Why here: Direct-API BYO-key planner turns need a real API key persisted somewhere — Pace’s rule is that key never touches UserDefaults, a plist, or a log line, so Keychain is the only legal home for it.
  • Where: PaceKeychainStore.swiftPaceKeychainStore (enum) wraps SecItemAdd/SecItemCopyMatching/SecItemUpdate behind storeAPIKey/loadAPIKey-style calls, all keyed off kSecClassGenericPassword.
  • Source: https://developer.apple.com/documentation/security/keychain-services
  • What: LSUIElement=true in Info.plist removes the Dock icon and default menu bar entirely; the app then owns its own always-on-top, non-activating NSPanel windows instead of a standard NSWindow or NSStatusItem.
  • Why here: Pace’s entire visible surface (the black notch capsule, the floating companion panel, the cursor overlay) is these custom panels — there is deliberately no Dock icon and no main window to alt-tab to.
  • Where: PaceMenuBarOverlay.swiftPaceMenuBarOverlayManager owns the capsule in a PaceMenuBarOverlayPanel (NSPanel subclass, canBecomeKey overridden to false so it never steals focus). MenuBarPanelManager.swift owns the floating companion panel in a KeyablePanel (.nonactivatingPanel style mask, canBecomeKey overridden to true so its text field can still receive keystrokes without activating the app).
  • Source: https://developer.apple.com/documentation/bundleresources/information-property-list/lsuielement

CI on a hardware-boundary-free runner

  • What: Three concrete lessons from making the GitHub Actions macOS Test step both fast and honest, all rooted in one fact — a hosted macos-latest runner has no microphone and no speaker, so anything that touches real audio hardware behaves differently there than on a developer Mac.
  • Why here: The Test step went from 30m34s to 0m54s (commit 611fa23, “ci: make the Test step fast and honest”) by fixing three separate ways the hardware boundary was silently costing time or hiding failures, not by disabling coverage.
  • Where:
    1. xcpretty is blind to Swift Testing. The suite is ~1300 import Testing (Swift Testing) cases; xcpretty only understands the legacy XCTest console format and silently swallows the entire stream — a 13s no-op run once printed green with zero tests executed. Fix: .github/workflows/ci.yml’s Test step drops xcpretty entirely, pins an -resultBundlePath, and parses the executed-test count back out with xcresulttool get test-results summary, failing the step if that count is zero even when xcodebuild itself reports success.
    2. AVAudioEngine.start() hangs forever with no microphone. The always-listening wake-word spotter’s tests bring up a real AVAudioEngine; on a developer Mac the CoreAudio HAL answers instantly, but on the mic-less runner it retries on a ~30-second loop and wedges the whole step. Fix: PaceTestEnvironment.isRunningInCI gates the handful of hardware-bound tests (they still run on developer machines and via scripts/test-pace.sh, which never sets the CI flag).
    3. Env vars need the TEST_RUNNER_ prefix to reach the xctest host. xcodebuild test-without-building runs tests inside a separate test-host process that does not inherit the calling shell’s environment — setting PACE_CI=1 directly in the job’s env: block would never be seen by the test code. ci.yml sets TEST_RUNNER_PACE_CI: "1" instead; xcodebuild strips the prefix and injects PACE_CI=1 into the test host, where PaceTestEnvironment.isRunningInCI reads it.
  • Source: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners (macOS runner hardware) and xcodebuild man page (TEST_RUNNER_ prefix, under -testPlan/environment variable passing).

Swift 6 concurrency idioms

  • What: Three recurring annotations that satisfy the Swift 6 strict-concurrency checker — @MainActor for UI-touching classes, nonisolated for pure value types with no shared mutable state, and @unchecked Sendable as an escape hatch when a class’s thread-safety is proven by hand (a lock, a serial queue) rather than by the compiler.
  • Why here: Pace’s async/await-everywhere codebase (planner calls, TTS synthesis, MCP subprocess I/O) needs the compiler’s data-race checking to actually pass, without forcing every small struct onto the main actor.
  • Where: PaceThreadMemory.swift pairs @MainActor final class PaceThreadMemory (owns the live conversation state, mutated only from the main actor) with nonisolated struct PaceThreadMemoryConfiguration and PaceThreadMemorySnapshot (plain Equatable/Codable value types safely passed across actor boundaries for persistence). PaceAPIAuditLog.swift shows the escape hatch: nonisolated final class PaceAPIAuditLog: @unchecked Sendable guards its mutable _currentTurnId with an explicit NSLock instead of actor isolation, because the audit log is written from many concurrent subsystems (planner, VLM, TTS, MCP, executor) and actor-hopping there would add latency the audit path can’t afford.
  • Source: https://developer.apple.com/documentation/swift/concurrency

See also: README.md.

Was this page helpful?