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
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.
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:
This is an infrastructure refactor, not a new user-facing "context dashboard."
Problem
Several MacTools subsystems currently discover overlapping system state independently.
Examples include:
Independent implementations create several risks:
NSWorkspaceobserversGoals
Non-goals
Proposed context families
Low-sensitivity, event-driven context
Permission-dependent UI context
Sensitive, on-demand context
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:
Typed context values
PluginSystemContextValueshould be a typed enum containing bounded public structs, for example:PluginApplicationContextPluginWindowContextPluginDisplayContextPluginSelectedTextContextPluginNetworkContextPluginFinderSelectionContextPluginBrowserPageContextDo not expose raw
AXUIElement,NSRunningApplication,NSScreen, or mutable AppKit objects across the public PluginKit boundary.Broker architecture
SystemContextBrokeris responsible for:The broker itself should not contain platform-specific acquisition logic.
Provider protocol
Illustrative internal protocol:
Providers may be
@MainActor, actors, or sendable services depending on the underlying framework.Initial providers
WorkspaceApplicationContextProviderDisplayContextProviderNetworkContextProviderPowerContextProviderAccessibilityUIContextProviderClipboardContextProviderFinderContextProviderBrowserContextProviderPluginKit 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:
Phase 2: optional PluginKit bridge
Add a separate optional companion protocol:
The host detects conforming plugins and injects the accessor after construction.
This avoids modifying the existing
MacToolsPluginwitness 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:
Default policy
Permission behavior
The broker should distinguish:
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
Migration plan
Phase 0: audit
NSWorkspaceobservers.Phase 1: broker foundation
Phase 2: low-risk providers
Phase 3: built-in plugin migration
Phase 4: sensitive providers
Phase 5: PluginKit bridge
Diagnostics
Provide developer-visible diagnostics for:
Do not display captured user content.
Testing
Add tests for:
Acceptance criteria