← Back to Index
Architecture

PermissionOracle & System Detection

The single source of truth for macOS permissions.

Detect. Decide. Validate.

The Oracle asks the OS for permission state, applies a strict priority hierarchy, and surfaces a tri-state result that the rest of the system can trust without second-guessing.

Detect

Apple APIs first, TCC database fallback.

G D ?

Decide

Granted, denied, or unknown tri-state.

Validate

Pull-based via InstallerEngine.inspectSystem().

The Permission Check Flow

Apple APIs are authoritative. The TCC database is a necessary fallback for .unknown cases only. PermissionOracle sits at the center and delegates to specialized checkers.
CALLER InstallerEngine.inspectSystem() ACTOR · SINGLE SOURCE OF TRUTH PermissionOracle Step 1 APPLE API IOHIDCheckAccess .granted / .denied TRUST THIS RESULT .unknown Proceed to TCC fallback Step 2 FALLBACK TCC Database InputMonitoring Accessibility FullDiskAccess Green path: authoritative result. Yellow path: TCC fallback for .unknown only.
Core invariant. Given the same system state, PermissionOracle always returns the same result. There is no caching, no timing dependency, and no hidden state. The Oracle asks the OS every time.

Key Components

Four types form the permission detection and validation pipeline.
PermissionOracle Actor
The single source of truth for all permission detection. An actor with a .shared singleton. Returns Status values (.granted, .denied, .unknown, .error) and aggregates them into a Snapshot covering both KeyPath and Kanata.
public actor PermissionOracle {
    public static let shared = PermissionOracle()

    public enum Status: Equatable, Sendable {
        case granted, denied, unknown
        case error(String)
    }

    public struct Snapshot: Sendable {
        let keyPath: PermissionSet
        let kanata: PermissionSet
        var isSystemReady: Bool { keyPath.accessibility.isReady && kanata.hasAllPermissions }
    }
}
FullDiskAccessChecker Singleton
Best-effort FDA detection via file-readability probe against the system TCC database (/Library/Application Support/com.apple.TCC/TCC.db). Results are cached for 10 seconds. FDA is needed to read TCC entries for Kanata's permission state.
public final class FullDiskAccessChecker {
    // Probes system TCC.db readability as a heuristic
    public func hasFullDiskAccess() -> Bool
    func refresh() -> Bool
}
SystemInspector Pure Function
Validates permissions as part of system inspection. Checks permission state from the snapshot and produces WizardIssue values for any missing permissions. Used by the wizard to decide which permission page to show.
InstallerEngine.inspectSystem() Facade
The pull-based entry point for all validation. Gathers a SystemSnapshot (including permission state from PermissionOracle) and returns it as a pure value struct. No component queries permissions on its own.
let engine = InstallerEngine()
let context = await engine.inspectSystem() // Pure value struct
if context.permissions.inputMonitoring != .granted { /* show wizard page */ }

Permission Check Lifecycle

From request to resolution, every permission check follows the same path.
Check requested
InstallerEngine.inspectSystem() calls PermissionOracle.shared to gather permission state
Apple API returns
IOHIDCheckAccess returns .granted or .denied — Oracle trusts this immediately and returns
API returns unknown
IOHIDCheckAccess returns .unknown — Oracle falls back to TCC database query (requires Full Disk Access)
Wizard page shown
SystemInspector finds missing permission → WizardRouter returns .inputMonitoring or .accessibility page
User grants access
User enables permission in System Settings → wizard re-inspects → Oracle re-queries OS → .granted → advance

Design Rules

Five rules that keep permission detection reliable and deterministic.
1
Apple APIs ALWAYS take precedence over TCC database
This is the fundamental rule. TCC is a cache that can be stale. IOHIDCheckAccess reflects actual system state. When the Apple API gives a definitive answer, trust it unconditionally.
2
Tri-state dispatch: .granted/.denied are authoritative, .unknown triggers TCC
IOHIDCheckAccess returning .granted or .denied — trust this result. Only .unknown proceeds to the TCC database fallback path.
3
Never bypass PermissionOracle.shared
All permission checks go through the Oracle. No direct IOHIDCheckAccess calls from UI code, no ad-hoc TCC queries. One path, one source of truth.
4
Never check permissions from root process
Root processes get unreliable results from both Apple APIs and TCC. All permission detection runs in the GUI app context (user session), never from the LaunchDaemon.
5
Validation is pull-based via InstallerEngine.inspectSystem()
No component queries permissions on its own. InstallerEngine.inspectSystem() gathers a complete SystemSnapshot including permissions. Consumers read the snapshot, never re-query.

Related Architecture