Skip to content

refactor(core): introduce a shared permission-aware system context broker #307

Description

@xcv58

Summary

Introduce a host-owned system-context broker that provides shared, permission-aware access to commonly requested macOS state.

The broker should eliminate duplicate observers and capture implementations across plugins while preserving:

  • Lazy acquisition
  • Clear ownership
  • Privacy boundaries
  • Permission behavior
  • PluginKit compatibility
  • Testability

This is an infrastructure refactor, not a new user-facing "context dashboard."

Problem

Several MacTools subsystems currently discover overlapping system state independently.

Examples include:

  • Frontmost application
  • Application activation changes
  • Focused window
  • Selected text
  • Display topology
  • Network path
  • Clipboard state
  • Finder selection

Independent implementations create several risks:

  • Duplicate NSWorkspace observers
  • Duplicate Accessibility observers
  • Duplicate pasteboard polling
  • Conflicting event-tap ownership
  • Different interpretations of the same context
  • Inconsistent permission behavior
  • Increased energy usage
  • Inconsistent caching and staleness
  • Sensitive values appearing in unrelated logs
  • More difficult testing and diagnostics

Goals

  • Establish one host-owned broker for shared system context.
  • Use independent providers rather than one monolithic polling object.
  • Acquire expensive or sensitive context only when requested.
  • Share low-cost event streams among multiple consumers.
  • Attach timestamps and availability reasons to context values.
  • Stop providers when no subscribers need them.
  • Keep sensitive values out of persistence and logs by default.
  • Preserve the current PluginKit ABI during the first migration phase.
  • Make providers independently mockable and testable.

Non-goals

  • Automatically passing all context to every action
  • Persisting a continuous history of user activity
  • Recording application usage analytics
  • Capturing keystrokes
  • Replacing plugin-specific domain models
  • Centralizing every macOS API behind one service
  • Prompting for every permission at application launch
  • Exposing sensitive context through Run Links or external clients
  • Creating a generic untyped dictionary API

Proposed context families

Low-sensitivity, event-driven context

  • Frontmost application
  • Running applications
  • Display topology
  • Main display
  • Network reachability and active interface
  • Power source and battery summary
  • Connected input-device summary
  • Connected audio-device summary

Permission-dependent UI context

  • Focused application UI element
  • Focused window
  • Focused window frame
  • Selected text
  • Selected Finder files
  • Current browser page context

Sensitive, on-demand context

  • Clipboard payload
  • Selected text contents
  • Browser URL and title
  • Finder file URLs
  • Calendar-event details

Sensitive payloads should be captured only for a declared request and should not remain cached longer than required.

Proposed data model

Use typed, bounded values rather than [String: Any].

Illustrative API:

public enum PluginSystemContextKey: String, Codable, Sendable {
    case frontmostApplication
    case focusedWindow
    case selectedText
    case displayTopology
    case networkPath
    case clipboardSummary
    case finderSelection
    case browserPage
}

public enum PluginSystemContextFreshness: Sendable {
    case cached(maxAge: Duration)
    case current
}

public struct PluginSystemContextRequest: Sendable {
    public let keys: Set<PluginSystemContextKey>
    public let freshness: PluginSystemContextFreshness
    public let purpose: String
    public let allowsPermissionPrompt: Bool
}

public struct PluginSystemContextUnavailable: Codable, Sendable {
    public let key: PluginSystemContextKey
    public let reason: String
}

public struct PluginSystemContextSnapshot: Sendable {
    public let capturedAt: Date
    public let values: [PluginSystemContextValue]
    public let unavailable: [PluginSystemContextUnavailable]
}

Typed context values

PluginSystemContextValue should be a typed enum containing bounded public structs, for example:

  • PluginApplicationContext
  • PluginWindowContext
  • PluginDisplayContext
  • PluginSelectedTextContext
  • PluginNetworkContext
  • PluginFinderSelectionContext
  • PluginBrowserPageContext

Do not expose raw AXUIElement, NSRunningApplication, NSScreen, or mutable AppKit objects across the public PluginKit boundary.

Broker architecture

SystemContextBroker is responsible for:

  • Resolving providers by context key
  • Coordinating snapshots
  • Enforcing freshness
  • Coalescing concurrent requests
  • Reference-counting subscriptions
  • Returning availability reasons
  • Redacting diagnostics
  • Cancelling abandoned requests

The broker itself should not contain platform-specific acquisition logic.

Provider protocol

Illustrative internal protocol:

protocol SystemContextProvider: AnyObject {
    var providedKeys: Set<SystemContextKey> { get }

    func snapshot(
        keys: Set<SystemContextKey>,
        freshness: SystemContextFreshness
    ) async -> SystemContextProviderResult

    func subscribe(
        keys: Set<SystemContextKey>,
        handler: @escaping @Sendable (SystemContextProviderUpdate) -> Void
    ) -> SystemContextSubscription
}

Providers may be @MainActor, actors, or sendable services depending on the underlying framework.

Initial providers

WorkspaceApplicationContextProvider

  • Frontmost application
  • Application activation and termination

DisplayContextProvider

  • Display topology and visible frames

NetworkContextProvider

  • Reachability and active interface

PowerContextProvider

  • Battery and power-source state

AccessibilityUIContextProvider

  • Focused window and selected text

ClipboardContextProvider

  • Clipboard summary or on-demand payload

FinderContextProvider

  • Selected Finder URLs

BrowserContextProvider

  • Reserved for future host-shipped browser connectors

PluginKit compatibility strategy

Do not add a required member to MacToolsPlugin.

Phase 1: host-only service

Introduce the broker internally and migrate host-owned consumers first:

  • Automation
  • App-wide search or presentation logic
  • Display topology handling
  • Shared permission refresh behavior

Phase 2: optional PluginKit bridge

Add a separate optional companion protocol:

@MainActor
public protocol PluginSystemContextConsuming: AnyObject {
    func setSystemContextAccessor(
        _ accessor: any PluginSystemContextAccessing
    )
}

The host detects conforming plugins and injects the accessor after construction.

This avoids modifying the existing MacToolsPlugin witness table.

Phase 3: PluginKit vNext

At the next intentional ABI version, evaluate adding a context accessor to PluginRuntimeContext.

Do not perform that change solely for the initial migration.

Privacy model

Every context key should declare:

  • Sensitivity classification
  • Whether it may be cached
  • Maximum default cache duration
  • Whether it requires a permission
  • Whether it may be logged
  • Whether it may be exposed to plugins
  • Whether it may be used by automatic rules
  • Whether it may cross an external-invocation boundary

Default policy

  • No user-content payload is persisted.
  • No selected text, clipboard text, browser URL, or file URL is logged.
  • Sensitive context is not automatically included in action history.
  • Sensitive context is not available to Run Links.
  • Permission prompts are not shown unless the requesting UI explicitly allows them.
  • Automatic rules cannot acquire sensitive context unless a future policy explicitly allows it.

Permission behavior

The broker should distinguish:

  • Permission not required
  • Permission not determined
  • Permission denied
  • Permission restricted
  • Provider unavailable
  • Context unsupported
  • No current value

The broker should not silently prompt from a background request.

For the initial version, plugin settings remain responsible for explaining and requesting their permissions. The broker returns structured unavailability.

A separate centralized permission coordinator may be considered later.

Lifecycle and efficiency

  • Start event observers only when at least one subscriber exists.
  • Stop observers when the final subscriber is released.
  • Coalesce identical concurrent snapshot requests.
  • Bound all cached values by time and size.
  • Invalidate relevant caches after app, display, network, or permission changes.
  • Prevent multiple providers from claiming the same exclusive event stream.
  • Cancel on-demand Accessibility capture when its consumer disappears.

Migration plan

Phase 0: audit

  • Inventory all NSWorkspace observers.
  • Inventory all Accessibility observers and snapshots.
  • Inventory all pasteboard monitors.
  • Inventory display, network, power, and device observers.
  • Identify which consumers require events versus snapshots.
  • Identify context currently persisted or logged.

Phase 1: broker foundation

  • Add context key and typed-value models.
  • Add provider registry.
  • Add snapshot coordination and freshness.
  • Add subscription lifecycle management.
  • Add redacted diagnostics.
  • Add test providers and deterministic clocks.

Phase 2: low-risk providers

  • Migrate frontmost-application observation.
  • Migrate display topology.
  • Migrate network reachability.
  • Migrate power context.
  • Verify that duplicate host observers are removed.

Phase 3: built-in plugin migration

  • Migrate AutoInput application context.
  • Migrate appropriate WindowSwitcher application context.
  • Migrate automation environment snapshots.
  • Preserve plugin-specific caches only where domain behavior requires them.

Phase 4: sensitive providers

  • Add focused-window context.
  • Add selected-text context.
  • Add clipboard summary/on-demand access.
  • Add Finder selection.
  • Document permission and privacy boundaries.

Phase 5: PluginKit bridge

  • Add the optional context-consuming protocol.
  • Inject the accessor into compatible plugins.
  • Add compatibility tests for older dynamic plugins.
  • Document supported keys and sensitivity rules.

Diagnostics

Provide developer-visible diagnostics for:

  • Active providers
  • Subscriber count by key
  • Last successful update time
  • Last unavailable reason
  • Cache age
  • Coalesced request count
  • Redacted provider errors

Do not display captured user content.

Testing

Add tests for:

  • Provider registration conflicts
  • Multiple subscribers sharing one observer
  • Observer shutdown after final unsubscribe
  • Cache freshness behavior
  • Concurrent request coalescing
  • Cancellation
  • Permission denial
  • Permission changes
  • Provider failure
  • Partial snapshots
  • Sensitive-value redaction
  • Plugin deactivation
  • Legacy plugin compatibility
  • No persistence of sensitive context

Acceptance criteria

  • At least frontmost application, displays, network, and power use the broker.
  • Migrated consumers do not create duplicate observers for the same event stream.
  • Providers stop when they have no active consumer.
  • Every returned value has a capture timestamp.
  • Unavailable context includes a structured reason.
  • Sensitive payloads are neither persisted nor logged.
  • Existing dynamic plugins continue to load without recompilation.
  • The service can be fully tested with mock providers.
  • Context acquisition does not bypass existing permission policy.
  • No action receives implicit context that it did not request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions