Skip to content

Conversation

malpern
Copy link
Contributor

@malpern malpern commented Aug 22, 2025

feat(macos): Add permission checking and remote restart via TCP

Summary

Adds macOS system permission checking and remote process restart functionality via TCP server. External applications can now check Accessibility and Input Monitoring permissions and restart Kanata programmatically.

🎯 Motivation

macOS requires explicit permissions for keyboard monitoring and app control. This enables automated permission checking and remote restart without manual intervention, improving the setup experience.

🔧 Changes

  • src/macos_permissions.rs - New module with FFI bindings to macOS frameworks
  • src/tcp_server.rs - TCP handlers for permission checking and restart
  • tcp_protocol/src/lib.rs - New CheckMacosPermissions and Restart message types
  • Cross-platform safe with graceful fallback for non-macOS systems

📡 TCP Commands

Check Permissions:

echo '{"CheckMacosPermissions": {}}' | nc localhost 13331

Response: {"MacosPermissions": {"accessibility": "granted", "input_monitoring": "denied"}}

Restart Process:

echo '{"Restart": {}}' | nc localhost 13331  

Response: {"status": "Ok"} (then restarts)

🔒 Permission States

  • granted / denied - Permission status
  • not_applicable - Non-macOS systems
  • error - System call failed

✅ Technical Notes

  • Safety: All unsafe FFI properly wrapped with error handling
  • Compatibility: Fully backwards compatible, no breaking changes
  • Cross-platform: Conditional compilation prevents issues on other platforms
  • Type Safety: Strongly typed enum with serde serialization
  • Testing: Passes cargo check and clippy with no warnings

🚀 Usage

Enables integration with setup tools, system monitors, and automated workflows that need to verify Kanata's macOS permissions and restart the process when permissions are granted.

This adds macOS system permission checking and remote process restart
functionality via TCP server. External applications can now check
Accessibility and Input Monitoring permissions and restart Kanata
programmatically.

## Changes

- **New macos_permissions module**: FFI bindings to macOS frameworks
  - Accessibility permission via ApplicationServices framework
  - Input Monitoring permission via IOKit framework
  - Cross-platform safe with graceful fallback for non-macOS systems

- **TCP protocol extensions**: New message types for permission management
  - CheckMacosPermissions: Returns permission status as typed enums
  - Restart: Restarts process (macOS only) with ACK-first pattern
  - PermissionState enum: granted|denied|not_applicable|error

- **Enhanced TCP server**: Platform-conditional restart handling
  - macOS: Sends ACK, flushes stream, schedules background restart
  - Other platforms: Returns descriptive error message

- **Documentation updates**: Usage examples and configuration guide

## TCP Commands

Check permissions: echo '{"CheckMacosPermissions": {}}' | nc localhost 13331
Restart process: echo '{"Restart": {}}' | nc localhost 13331

## Technical Notes

- All unsafe FFI properly wrapped with error handling
- Fully backwards compatible, no breaking changes
- Cross-platform conditional compilation prevents issues
- Type-safe enum serialization with serde
- Comprehensive error reporting and logging
@malpern malpern force-pushed the macos-permissions-via-tcp branch from 929e8ce to c7e479e Compare August 22, 2025 20:56
@malpern
Copy link
Contributor Author

malpern commented Aug 25, 2025

Withdrawing this PR - IOHIDCheckAccess() is unreliable for root processes on macOS

After extensive testing, I'm withdrawing this PR because IOHIDCheckAccess() returns false negatives for root processes on macOS, even when kanata actually has permission and is successfully capturing keyboard events.

The Problem

# Kanata working perfectly, capturing keys:
tail /var/log/kanata.log
# [DEBUG] key press Enter ✅

# But this PR reports:
echo '{"CheckMacosPermissions": {}}' | nc 127.0.0.1 54141  
# {"input_monitoring":"denied"} ❌ Wrong!

Root Cause

Apple's TCC system has "responsible process" rules that make preflight permission checks (IOHIDCheckAccess()) unreliable for root processes, even though the actual HID access works fine when granted.

Better Architecture

The industry-standard solution is a split architecture:

  • GUI (user session): Handles permission checking and user guidance (reliable)
  • Daemon (root): Focuses purely on keyboard functionality
  • IPC communication: Provides comprehensive status

This matches how Karabiner-Elements and other successful macOS keyboard tools work.

Future Direction

I'm exploring this split approach for a cross-platform GUI solution where:

  • Platform-native GUIs handle permissions correctly per OS
  • Kanata stays focused as a cross-platform core engine
  • Enhanced IPC protocol (possibly UDP alongside TCP) provides high-performance, secure communication

Thanks for the feedback! This has been a valuable learning experience about macOS TCC architecture. 🙏


TL;DR: Root processes can't reliably self-check TCC permissions on macOS. Permission checking belongs in GUI layer, not daemon layer.

@malpern malpern closed this Aug 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant